Per-IP limits instead of a CDN
This blog runs on my own VPS. A CDN would hide the server behind another company’s network. I chose not to do that. I added per-IP limits on the public ports instead.
What the rules do
The rules cover ports 80, 443 and 853. Port 853 is DNS over TLS.
- A source IP may hold at most 100 open TCP connections. The next one gets a TCP reset.
- A source IP may open at most 50 new connections per second, with a burst of 100. The extra packets are dropped. This applies to TCP and to UDP, so QUIC is covered.
The mechanism
The rules use iptables and ip6tables. Two match modules do the work:
connlimitcounts open connections per source address.hashlimitlimits the rate of new connections per source address. Each protocol gets its own named table.
The IPv6 rules use a mask of 128, and the IPv4 rules use 32. Each limit then applies to a single address.
Why DOCKER-USER
My services run in Docker. Docker publishes ports through NAT, and those
packets skip the normal INPUT chain. Rules there never see them. Docker
gives one chain for this case, DOCKER-USER, and it runs before Docker’s own
rules. My script puts a jump to its own EDGE-LIMITS chain there, only for
the public network interface.
The script is idempotent. It creates the chain or flushes it, then adds the rules. It adds the jump only if the jump does not exist. A systemd one-shot unit runs it after Docker starts. Docker restarts re-run it too.
What it does not do
A single source cannot flood the server any more. But the rules cannot stop a flood that fills the network link. By then the packets have already arrived. Only an upstream provider can stop that.
Still open
I picked the numbers 100 and 50 as sensible limits, not from measurement. I would watch the counters for a few weeks and then tune them. A busy office behind one shared address could also hit the limits. I have not tested that.