<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Phaedon V</title>
    <description>The latest articles on DEV Community by Phaedon V (@phaedonv).</description>
    <link>https://dev.to/phaedonv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F894036%2F108e63d7-c1cb-4a81-8670-c6c3e47cf320.jpeg</url>
      <title>DEV Community: Phaedon V</title>
      <link>https://dev.to/phaedonv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phaedonv"/>
    <language>en</language>
    <item>
      <title>My Router's Hidden Door for Burglars</title>
      <dc:creator>Phaedon V</dc:creator>
      <pubDate>Wed, 14 Oct 2026 17:12:54 +0000</pubDate>
      <link>https://dev.to/phaedonv/my-routers-hidden-door-for-burglars-1c7a</link>
      <guid>https://dev.to/phaedonv/my-routers-hidden-door-for-burglars-1c7a</guid>
      <description>&lt;h2&gt;
  
  
  The port I never opened
&lt;/h2&gt;

&lt;p&gt;While poking at my own home network one evening, I found an open door to the internet that I never created. Not a forward I'd set in the router's admin page, not a rule I'd forgotten about — a permanent hole punched straight through my NAT, pointing at the open internet, reachable by any host on the planet, with no authentication and no expiry. And it was aimed at a device that wasn't even powered on at the time.&lt;/p&gt;

&lt;p&gt;The culprit was &lt;strong&gt;UPnP&lt;/strong&gt; (Universal Plug and Play) — a convenience feature switched on by default on almost every consumer router shipped in the last fifteen years.&lt;/p&gt;

&lt;p&gt;This is a walk-through of how I found it, how I &lt;em&gt;proved&lt;/em&gt; it was genuinely reachable from outside my network (every endpoint mine — the only honest way to test this), and what you can do to check for and close the same door on your own network. There's no exotic exploit here. It's the feature working exactly as designed, which is precisely the problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every public IP, MAC address and hostname in this article has been redacted or replaced with documentation-range values. Ironically, leaking those details is exactly the kind of self-own the article is about.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What UPnP actually does
&lt;/h2&gt;

&lt;p&gt;To see why one stray port forward matters, you need the mental model of why your home network is normally safe at all.&lt;/p&gt;

&lt;p&gt;Under the hood, your router does &lt;strong&gt;NAT&lt;/strong&gt; (Network Address Translation). Your devices have private addresses (&lt;code&gt;192.168.x.x&lt;/code&gt;) that don't exist on the public internet; the router rewrites their traffic to share one public IP. A useful side effect is that &lt;em&gt;unsolicited&lt;/em&gt; inbound traffic from the internet has nowhere to go — the router doesn't know which internal device to hand it to, so it drops it. NAT isn't a firewall, but it behaves like one by accident. That's why a random stranger can't reach the laptop in your bedroom.&lt;/p&gt;

&lt;p&gt;UPnP exists to poke holes in exactly that protection — on purpose, on demand. Through its &lt;strong&gt;IGD&lt;/strong&gt; (Internet Gateway Device) service, an application on your LAN can ask the router: &lt;em&gt;"forward external port 9308 to me."&lt;/em&gt; The router creates the mapping, and now inbound traffic from the internet to that port lands on that device. This is genuinely useful for things that need unsolicited inbound connections: online games, consoles, VoIP, BitTorrent, some smart-home gear.&lt;/p&gt;

&lt;p&gt;Here's the whole problem in one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The router grants these requests to &lt;strong&gt;anything&lt;/strong&gt; on the LAN that asks — no authentication, no prompt to the user, usually no logging.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's no "are you sure?". There's no password. The router simply trusts that any device on the local network is allowed to open a door to the outside world. Your games console and a compromised IoT bulb have exactly the same privilege. You, the owner, are never asked and often can't even see what was opened without going looking — which is what this whole exercise was.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd815x6r5ba7q61yhsi0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd815x6r5ba7q61yhsi0w.png" alt="UPnP flow: a LAN device opens the door, the internet walks in" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The small request on top is the whole cause; the internet-facing inbound path on the bottom is the effect — and the device making the request can be anything on your LAN.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The discovery
&lt;/h2&gt;

&lt;p&gt;It started with an ordinary sweep of my own subnet to see what was on the network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nmap &lt;span class="nt"&gt;-sn&lt;/span&gt; 192.168.0.0/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five hosts: the Arris gateway (&lt;code&gt;.1&lt;/code&gt;), an LG smart TV (&lt;code&gt;.3&lt;/code&gt;), an iPhone (&lt;code&gt;.8&lt;/code&gt;), my laptop (&lt;code&gt;.11&lt;/code&gt;), and a second Arris interface (&lt;code&gt;.252&lt;/code&gt;). All accounted for. A service scan of the gateway showed its web admin UI on lighttpd, and one more thing on port 5000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT     STATE  SERVICE  VERSION
80/tcp   open   http     lighttpd
443/tcp  open   ssl/http lighttpd
5000/tcp open   upnp     MiniUPnPd 1.9 (UPnP 1.1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;MiniUPnPd 1.9&lt;/code&gt; is the UPnP daemon — and it's a release from around 2014, well over a decade old, baked into the ISP firmware where I can't update it. An old service that any LAN device can command is worth a closer look. So instead of guessing from the web UI, I asked the daemon directly what port forwards it was holding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; miniupnpc
upnpc &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relevant line in the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; i protocol exPort-&amp;gt;inAddr:inPort  description              remoteHost leaseTime
 0 UDP  9308-&amp;gt;192.168.0.9:9308  '192.168.0.9:9308 to 9308'  ''         0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoded, that single line says a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;External UDP port 9308&lt;/strong&gt; on my public IP is forwarded to &lt;code&gt;192.168.0.9:9308&lt;/code&gt; on my LAN.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;&lt;code&gt;remoteHost&lt;/code&gt; field is empty&lt;/strong&gt; — the forward accepts connections from &lt;em&gt;any&lt;/em&gt; host on the internet, not a specific one.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;lease time is &lt;code&gt;0&lt;/code&gt;&lt;/strong&gt; — meaning permanent. It never expires on its own.&lt;/li&gt;
&lt;li&gt;The auto-generated description is the format an &lt;em&gt;application&lt;/em&gt; produces when it opens its own mapping. Nobody set this by hand; some software on &lt;code&gt;.9&lt;/code&gt; asked for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the kicker: &lt;code&gt;192.168.0.9&lt;/code&gt; wasn't in my host sweep. The device that opened this hole was &lt;strong&gt;offline&lt;/strong&gt; — but because the lease is permanent, its internet-facing forward outlived it. A classic orphaned mapping, sitting open, pointing at a device that comes and goes (a phone, a console, an IoT gadget), and reachable from the whole internet the entire time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving the door is real
&lt;/h2&gt;

&lt;p&gt;Listing a mapping only tells you the router &lt;em&gt;intends&lt;/em&gt; to forward a port. It doesn't prove the outside world can actually reach in — ISP-level filtering or carrier-grade NAT can quietly make an "open" forward a dead end. The only honest test is to open a port to a listener I control and connect to it from a device that is genuinely off my network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every endpoint in this test is mine:&lt;/strong&gt; my router, my laptop, my public IP, my phone. That's what keeps it a legitimate security test rather than an attack. Do not do this against infrastructure you don't own.&lt;/p&gt;

&lt;p&gt;First, a listener on my laptop (LAN &lt;code&gt;.11&lt;/code&gt;), so an incoming probe hits something instead of a closed port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-lvnp&lt;/span&gt; 8099
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, using the same UPnP mechanism an app would, I opened a forward to it — with a one-hour lease so it auto-expires if I forget it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;upnpc &lt;span class="nt"&gt;-a&lt;/span&gt; 192.168.0.11 8099 8099 TCP 3600
upnpc &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mapping now showed up in the list alongside the mystery one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1 TCP  8099-&amp;gt;192.168.0.11:8099  'libminiupnpc'  ''  3597
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the real test, from my phone on &lt;strong&gt;mobile data with Wi-Fi off&lt;/strong&gt; — a connection coming from the public internet, nowhere near my LAN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-vn&lt;/span&gt; &amp;lt;YOUR-PUBLIC-IP&amp;gt; 8099
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it connected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connection to 198.51.100.23 8099 port [tcp/*] succeeded!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is the whole proof. A TCP handshake completed from a device out on the internet, through my public IP, through the UPnP forward, to a listener on my laptop. Typing into the phone showed the text arriving live in the listener window back on the laptop — a full, unauthenticated, bidirectional channel from the open internet into my machine, opened by nothing more than a LAN-side request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg97qdu2wuk04giksyd34.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg97qdu2wuk04giksyd34.png" alt="Connection from an off-network device succeeding" width="800" height="1778"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From a phone on mobile data, off the home network: the connection to the public IP on the forwarded port completes. The IP is redacted.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The listener confirmed the inbound connection from an off-network address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ncat: Listening on 0.0.0.0:8099
Ncat: Connection from 198.51.100.200.
Ncat: Connection from 198.51.100.200:56732.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc191lvt0muc3h7yzhu9h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc191lvt0muc3h7yzhu9h.png" alt="Text typed on the phone appearing on the laptop over the tunnel" width="800" height="1778"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Typed on the phone, landing live on the laptop — a two-way channel straight from the open internet into the machine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The instant the demo was done, I tore the hole down — which is the one step you must never skip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;upnpc &lt;span class="nt"&gt;-d&lt;/span&gt; 8099 TCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this is genuinely dangerous
&lt;/h2&gt;

&lt;p&gt;The demo above is harmless — I opened a door and closed it. But the same mechanism, outside your control, is where the risk lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Malware self-exposure.&lt;/strong&gt; This is the big one. A compromised device — a PC, a phone, an IoT gadget — can open its own inbound forward and hand an attacker a direct line back in, bypassing NAT entirely. NAT &lt;em&gt;would&lt;/em&gt; have blocked that unsolicited inbound traffic; UPnP opens the door from the inside, on the malware's request. The victim does nothing and sees nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exposing a vulnerable service you never meant to publish.&lt;/strong&gt; A forward leads to a service on the internal device. If that service has a known flaw — an old admin panel, a debug port, a torrent client with an RCE, an IP camera with hardcoded credentials — it's now reachable from anywhere on earth instead of just your living room. The mapping itself is harmless; what sits behind it may not be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open to the whole world, forever.&lt;/strong&gt; Remember that mystery mapping: &lt;code&gt;remoteHost&lt;/code&gt; empty (any source IP) and lease &lt;code&gt;0&lt;/code&gt; (permanent). That combination produces holes that outlive the device that made them — orphaned forwards nobody is tracking, exactly like the one I found pointing at an offline device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attacks on the UPnP daemon itself.&lt;/strong&gt; This is where an old MiniUPnPd matters. Historically, badly configured routers exposed their UPnP &lt;em&gt;control interface&lt;/em&gt; to the WAN side, letting remote attackers add mappings from across the internet — research years ago found tens of millions of such devices. There's also &lt;strong&gt;CallStranger (CVE-2020-12695)&lt;/strong&gt;, which abuses the UPnP event-subscription mechanism for DDoS reflection, data exfiltration and internal port scanning. A decade-old daemon is more likely to carry that kind of baggage.&lt;/p&gt;

&lt;p&gt;A fair word of calibration: UPnP is not inherently a breach. It ships on by default, and millions of people run it for years without incident — a legitimate app opening a legitimate port is the normal case. The real cost is &lt;strong&gt;loss of visibility and control&lt;/strong&gt;. You don't get to approve what opens, so a bad actor or a sloppy device inherits the same power your games console has. Put bluntly: UPnP converts &lt;em&gt;"an attacker must breach the perimeter"&lt;/em&gt; into &lt;em&gt;"an attacker must breach any single device on the LAN, and the perimeter opens itself."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The other layer: getting onto the LAN in the first place
&lt;/h2&gt;

&lt;p&gt;Everything above assumes an attacker is already on your network. On this same router, getting there took about a minute.&lt;/p&gt;

&lt;p&gt;The gateway had &lt;strong&gt;WPS still enabled&lt;/strong&gt; (WPS 2.0), and WPS is the weak link in most home WiFi. Using an AC1200 USB adapter (Realtek RTL8812AU) with the wifite3 toolkit, the &lt;strong&gt;WPS PIN fell in roughly a minute&lt;/strong&gt; — a Pixie Dust attack recovered it offline, and the PIN handed over the WiFi passphrase directly. No wordlist, no waiting. WPS is sold as convenience; in practice it's a side door that sidesteps even a strong WPA2 password.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tve69g3ge1iyrw6mp9m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tve69g3ge1iyrw6mp9m.png" alt="wifite3: WPS PIN and WPA2 passphrase recovered, PMF disabled" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The run end to end: PMF disabled, WPS 2.0 enabled, the WPS PIN recovered and the WPA2 passphrase with it. Network name, addresses, PIN and password redacted.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two things made it worse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PMF (Protected Management Frames) was disabled&lt;/strong&gt; — that's what lets an attacker deauthenticate a client and capture the WPA handshake to begin with.&lt;/li&gt;
&lt;li&gt;A strong WiFi password is no help while WPS is on, because the PIN is a parallel route straight to the passphrase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now connect the two halves. &lt;strong&gt;Someone only has to be in WiFi range once&lt;/strong&gt; — a neighbour, a car parked outside. WiFi gets them onto the LAN; from the inside, UPnP lets them open a permanent internet-facing door to anything that stays online — the router’s own admin page, a NAS, an IP camera, or a backdoor dropped on an always-on device. They then walk away with remote access from anywhere, and the forward survives reboots and outlives their visit. That’s why “nobody’s on my WiFi” isn’t the reassurance it sounds like: a weak wireless door plus UPnP isn’t two small problems — it’s a chain that turns one brief moment of proximity into lasting remote access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check and shut it down on your own network
&lt;/h2&gt;

&lt;p&gt;Fifteen minutes on your own gear. All of this is read-and-remediate on hardware you own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;List what UPnP is currently holding open.&lt;/strong&gt; This is the single most revealing command — it prints your external IP and every active forward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; miniupnpc   &lt;span class="c"&gt;# or: dnf install miniupnpc&lt;/span&gt;
upnpc &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the mappings carefully. For each one, ask: do I recognise the internal device, did I mean to expose it, and why is the lease permanent? A clean list is fine. Anything you can't explain is your finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identify every device on the network&lt;/strong&gt;, so nothing behind a forward is a mystery. The vendor from the MAC address usually names it instantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nmap &lt;span class="nt"&gt;-sn&lt;/span&gt; 192.168.0.0/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Delete any stale or unwanted mapping.&lt;/strong&gt; It takes effect immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;upnpc &lt;span class="nt"&gt;-d&lt;/span&gt; &amp;lt;externalPort&amp;gt; &amp;lt;TCP|UDP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Turn UPnP off at the router&lt;/strong&gt; unless you specifically rely on it. This is the real fix — deleting a mapping without disabling UPnP just lets whatever opened it punch the hole again. It's in the gateway's admin page, usually under Advanced, Firewall, or NAT settings. On an old daemon, disabling UPnP entirely is the cleaner call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change the router admin password&lt;/strong&gt; while you're in there. A weak or default gateway password is still the single most common way home routers get taken over — it outranks UPnP by a wide margin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disable WPS.&lt;/strong&gt; If your router offers WPS — PIN mode especially — turn it off. A WPS PIN can be recovered in minutes (Pixie Dust, or a straight brute force) no matter how strong your WiFi password is. Where the router supports it, switch WiFi to &lt;strong&gt;WPA3 and enable PMF&lt;/strong&gt;, which also closes the handshake-capture path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify from the outside.&lt;/strong&gt; Everything above is the inside view. What actually matters is what the internet sees, and your LAN can't tell you that. Scan your own public IP from off the network — a phone on mobile data, a cheap VPS, or an external port-check service — to confirm nothing is exposed that shouldn't be. (If your router's WAN IP is in the &lt;code&gt;100.64.x.x&lt;/code&gt;–&lt;code&gt;100.127.x.x&lt;/code&gt; range, you're behind carrier-grade NAT and inbound forwards won't work from outside anyway — worth knowing before you test.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule, and the takeaways
&lt;/h2&gt;

&lt;p&gt;Everything here was done on my own network, against my own devices, from my own phone. That line is not optional. Capturing, scanning or opening ports against networks and devices you don't own is illegal in most places, full stop. The entire value of this exercise is understanding &lt;em&gt;your own&lt;/em&gt; exposure — keep it there, or take it to a lab you control end to end.&lt;/p&gt;

&lt;p&gt;The things worth carrying away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NAT is not a firewall.&lt;/strong&gt; It blocks unsolicited inbound traffic as a side effect, and UPnP exists specifically to undo that, on request, with no authentication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UPnP trades control for convenience.&lt;/strong&gt; You can't approve what gets opened, so every device on your LAN — trusted or compromised — holds the same power to expose you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The perimeter is only as strong as your weakest device.&lt;/strong&gt; With UPnP on, breaching any single LAN device is enough; the perimeter then opens itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility beats assumptions.&lt;/strong&gt; &lt;code&gt;upnpc -l&lt;/code&gt; took seconds and showed me a permanent, internet-facing hole I never knew existed. You can't defend what you can't see.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My own loose end from all this: a stale forward to a device that's currently offline, which I'll identify from the router's DHCP list before deciding whether to kill the mapping and switch UPnP off for good. If you run &lt;code&gt;upnpc -l&lt;/code&gt; on your own network after reading this, I'd genuinely like to know what you find.&lt;/p&gt;

</description>
      <category>nmap</category>
      <category>wifit3</category>
      <category>upnp</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>That SSH bot rotating IPs in your logs? Ban the whole subnet, then let the ban expire</title>
      <dc:creator>Phaedon V</dc:creator>
      <pubDate>Fri, 02 Oct 2026 09:39:38 +0000</pubDate>
      <link>https://dev.to/phaedonv/that-ssh-bot-rotating-ips-in-your-logs-ban-the-whole-subnet-then-let-the-ban-expire-g4i</link>
      <guid>https://dev.to/phaedonv/that-ssh-bot-rotating-ips-in-your-logs-ban-the-whole-subnet-then-let-the-ban-expire-g4i</guid>
      <description>&lt;p&gt;It started with an SSH alert I've seen a thousand times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sshd: Invalid user taow from 203.0.113.239 port 50612
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then again. And again. &lt;strong&gt;Dozens of times&lt;/strong&gt;, from a different IP almost every time, but always inside the same &lt;code&gt;/24&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Was it dangerous? No. That server only accepts SSH keys, and I double-checked from another box:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh user@server
user@server: Permission denied &lt;span class="o"&gt;(&lt;/span&gt;publickey&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No password prompt, nothing to guess. The bot was trying usernames against a door that doesn't have a keyhole.&lt;/p&gt;

&lt;p&gt;But it was still &lt;strong&gt;noise&lt;/strong&gt;: alerts piling up in the SIEM, auth logs filling up, and CPU spent rejecting the same junk over and over. When the real alert eventually arrives, I want to see it, not dig for it under 500 copies of &lt;code&gt;Invalid user oracle&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the usual quick fixes didn't feel right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Banning single IPs&lt;/strong&gt; is pointless against a bot that rotates through a whole range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ufw deny from 203.0.113.0/24&lt;/code&gt;&lt;/strong&gt; looks right but often does nothing. UFW &lt;em&gt;appends&lt;/em&gt; the rule after your existing &lt;code&gt;allow 22&lt;/code&gt;, so the allow matches first and the deny never fires. You need &lt;code&gt;ufw insert 1 ...&lt;/code&gt;, and you have to remember that every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A raw &lt;code&gt;iptables -I&lt;/code&gt; rule&lt;/strong&gt; works, but then it lives there forever. Six months later, someone finds a mystery DROP rule with no comment, no date and no owner, and nobody dares delete it.&lt;/p&gt;

&lt;p&gt;What I actually wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ban a &lt;strong&gt;whole subnet&lt;/strong&gt;, not an IP&lt;/li&gt;
&lt;li&gt;have it &lt;strong&gt;expire by itself&lt;/strong&gt; (most of these bots move on after a day or two)&lt;/li&gt;
&lt;li&gt;optionally make it &lt;strong&gt;permanent&lt;/strong&gt; when a range keeps coming back&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;not touch&lt;/strong&gt; the existing firewall setup (UFW, firewalld, whatever's there)&lt;/li&gt;
&lt;li&gt;leave a &lt;strong&gt;readable trail&lt;/strong&gt; for the next admin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I wrote a small tool for it: &lt;strong&gt;&lt;a href="https://github.com/phaedonv/netban" rel="noopener noreferrer"&gt;netban&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/phaedonv" rel="noopener noreferrer"&gt;
        phaedonv
      &lt;/a&gt; / &lt;a href="https://github.com/phaedonv/netban" rel="noopener noreferrer"&gt;
        netban
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      netban is a single Bash script for blocking whole networks (for example a /24) on a Linux host, either for a fixed time or permanently. 
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/phaedonv/netban/assets/netban-logo.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fphaedonv%2Fnetban%2FHEAD%2Fassets%2Fnetban-logo.svg" width="220" alt="netban — subnet IP range ban"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;
  Manual IPv4 network bans for Linux, built on nftables.&lt;br&gt;
  Temporary bans that expire on their own. Permanent bans that survive reboots
&lt;/p&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/10756c3a40b587a6228871d2860c92bc85e113b1a2c6c7e1e3f39b74f0bc165f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f626173682d342532422d3245463541303f6c6f676f3d676e7562617368266c6f676f436f6c6f723d7768697465"&gt;&lt;img alt="Bash" src="https://camo.githubusercontent.com/10756c3a40b587a6228871d2860c92bc85e113b1a2c6c7e1e3f39b74f0bc165f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f626173682d342532422d3245463541303f6c6f676f3d676e7562617368266c6f676f436f6c6f723d7768697465"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/b0276388a01b9ea3d075799c0ddcd5d04a95cfc108f2d57f8ea8077adae9aef0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6669726577616c6c2d6e667461626c65732d324546354130"&gt;&lt;img alt="nftables" src="https://camo.githubusercontent.com/b0276388a01b9ea3d075799c0ddcd5d04a95cfc108f2d57f8ea8077adae9aef0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6669726577616c6c2d6e667461626c65732d324546354130"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/c242fdfe42a6ecc4f06afb4b530f7a4f860563f038d39e41daa2434b06a9ff14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d324546354130"&gt;&lt;img alt="License: MIT" src="https://camo.githubusercontent.com/c242fdfe42a6ecc4f06afb4b530f7a4f860563f038d39e41daa2434b06a9ff14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d324546354130"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;netban&lt;/code&gt; is a single Bash script for blocking whole networks (for example a
&lt;code&gt;/24&lt;/code&gt;) on a Linux host, either for a fixed time or permanently. It's meant as
a &lt;strong&gt;manual fallback&lt;/strong&gt; next to automated defences such as Wazuh active
response, Suricata, fail2ban or CrowdSec: you use it when those miss something
or when you need to stop traffic &lt;em&gt;right now&lt;/em&gt; without editing firewall configs.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;$ netban 203.0.113.57/24
banned 203.0.113.0/24 for 24h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;A typical case: a server exposes SSH with key-only auth, and a bot keeps
hammering it from a rotating pool of IPs inside one subnet:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;sshd: Invalid user taow from 203.0.113.239 port 50612
sshd: Invalid user admin from 203.0.113.12 port 41877
sshd: Invalid user oracle from 203.0.113.190 port 39020
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It isn't a real threat, because passwords…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/phaedonv/netban" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;netban 203.0.113.239/24
&lt;span class="go"&gt;banned 203.0.113.0/24 for 24h
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can paste the IP straight from the alert with &lt;code&gt;/24&lt;/code&gt; on the end. The host bits get masked, so it bans the network.&lt;/p&gt;

&lt;p&gt;Need a different duration, or a permanent ban with a note for future you?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netban 203.0.113.0/24 6h
netban 198.51.100.0/24 perm &lt;span class="s2"&gt;"returns daily, ssh user enum"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run it with no arguments for an interactive prompt, which is handy when you're triaging a few ranges at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;netban
&lt;span class="gp"&gt;netban&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;203.0.113.0/24
&lt;span class="go"&gt;banned 203.0.113.0/24 for 24h
&lt;/span&gt;&lt;span class="gp"&gt;netban&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;198.51.100.7/24 2d
&lt;span class="go"&gt;banned 198.51.100.0/24 for 2d
&lt;/span&gt;&lt;span class="gp"&gt;netban&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;list
&lt;span class="gp"&gt;netban&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to check it's actually doing something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ netban -l
--- temporary ---
    elements = { 203.0.113.0/24 timeout 1d expires 23h41m12s }
--- permanent (live) ---
    elements = { 198.51.100.0/24 }
--- permanent (/etc/netban/permanent.list) ---
198.51.100.0/24  # added 2026-10-01 - returns daily, ssh user enum
--- drops ---
@perm counter packets 1843 bytes 110580
@temp counter packets 212 bytes 12720
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The counters climb, the log goes quiet, and the SIEM stops shouting.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works (it's mostly nftables doing the work)
&lt;/h2&gt;

&lt;p&gt;The whole trick is that nftables already supports &lt;strong&gt;sets with per-element timeouts&lt;/strong&gt;. netban just wraps that in something you can type at 2am without looking anything up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;table inet netban
 ├─ set perm   (interval)            permanent networks
 ├─ set temp   (interval, timeout)   temporary networks, auto-expire
 └─ chain input  hook input priority -10
      ip saddr @perm counter drop
      ip saddr @temp counter drop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design choices that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Its own table.&lt;/strong&gt; netban never edits UFW, firewalld or &lt;code&gt;/etc/nftables.conf&lt;/code&gt;. Everything lives in &lt;code&gt;inet netban&lt;/code&gt;, so removing it is one command and can't break anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Priority -10.&lt;/strong&gt; UFW and firewalld hook &lt;code&gt;input&lt;/code&gt; at priority 0. netban's chain runs &lt;em&gt;before&lt;/em&gt; them, so a banned range is dropped no matter what allow rules sit further down. That fixes the "my deny never matched" problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kernel handles expiry.&lt;/strong&gt; A temporary ban is just a set element with &lt;code&gt;timeout 24h&lt;/code&gt;. There's no cron job, no cleanup script and no state to forget about. When the timer runs out, it's gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permanent bans are plain text.&lt;/strong&gt; They go into &lt;code&gt;/etc/netban/permanent.list&lt;/code&gt; with a date and your note, and a small systemd oneshot (&lt;code&gt;netban.service&lt;/code&gt;) reloads them at boot. It's also &lt;code&gt;PartOf=nftables.service&lt;/code&gt;, so if someone restarts nftables and flushes the ruleset, the permanent bans come straight back.&lt;/p&gt;

&lt;p&gt;The core of a temporary ban is really just this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nft add element inet netban temp &lt;span class="s1"&gt;'{ 203.0.113.0/24 timeout 24h }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else is input validation, CIDR masking, and making it pleasant to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;It's a single Bash script. You need Linux with &lt;code&gt;nft&lt;/code&gt;, Bash 4+ and sudo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://raw.githubusercontent.com/phaedonv/netban/main/netban
&lt;span class="nb"&gt;sudo &lt;/span&gt;bash netban &lt;span class="nt"&gt;--install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That puts &lt;code&gt;netban&lt;/code&gt; in &lt;code&gt;/usr/local/bin&lt;/code&gt;, creates &lt;code&gt;/etc/netban/permanent.list&lt;/code&gt;, and enables the boot service. Re-running &lt;code&gt;--install&lt;/code&gt; updates in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is &lt;em&gt;not&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;I want to be upfront about this, because it's a small tool and I'd rather you know its limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's not automatic.&lt;/strong&gt; It doesn't watch logs. For automatic per-IP banning, use fail2ban, CrowdSec or Wazuh active response. netban is the &lt;strong&gt;manual fallback&lt;/strong&gt; for when those miss something or you need to act right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's not real protection on its own.&lt;/strong&gt; The actual SSH defence is key-only auth (&lt;code&gt;PasswordAuthentication no&lt;/code&gt;) and limiting who can reach port 22. netban reduces noise and load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's IPv4 only.&lt;/strong&gt; For now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's host-level.&lt;/strong&gt; If you control the edge router or cloud firewall, blocking there saves bandwidth for every machine behind it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where it fits in a typical layered defence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SIEM / IDS / fail2ban / CrowdSec&lt;/td&gt;
&lt;td&gt;automatic, per IP&lt;/td&gt;
&lt;td&gt;first line, reacts to alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geo blocking (optional)&lt;/td&gt;
&lt;td&gt;automatic, per country&lt;/td&gt;
&lt;td&gt;broad rotation across many ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;netban&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;manual, per network, temp or perm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;fallback when the above miss it&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it, break it, tell me
&lt;/h2&gt;

&lt;p&gt;netban is MIT-licensed and lives here: &lt;strong&gt;&lt;a href="https://github.com/phaedonv/netban" rel="noopener noreferrer"&gt;github.com/phaedonv/netban&lt;/a&gt;&lt;/strong&gt;. It was written at &lt;a href="https://catalink.eu" rel="noopener noreferrer"&gt;Catalink&lt;/a&gt; as part of a growing set of small blue-team tools we use day to day.&lt;/p&gt;

&lt;p&gt;If you try it, I'd love to hear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How do you handle subnet-rotating bots today?&lt;/strong&gt; fail2ban with a custom action, CrowdSec, edge firewall, or just ignore them?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IPv6 support:&lt;/strong&gt; worth adding, or do you mostly see this noise over v4?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that broke&lt;/strong&gt; on your distro. Issues and PRs are very welcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it saved you a few minutes of log-scrolling, a ⭐ on the repo helps other admins find it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The tool and this post were written with help from an AI assistant. The problem, the servers and the decisions are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>bash</category>
    </item>
  </channel>
</rss>
