Skip to content

Instantly share code, notes, and snippets.

@valeriansaliou
Last active September 21, 2023 07:34
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save valeriansaliou/9632580178e83da1dabce1bbe2cb6a76 to your computer and use it in GitHub Desktop.
Save valeriansaliou/9632580178e83da1dabce1bbe2cb6a76 to your computer and use it in GitHub Desktop.
HTTP/HTTPS DOS shield w/ IPTables
# Those rules protect HTTP/HTTPS services for both IPv4 and IPv6 sources as such:
# 1. Prevent a /32 IPv4 or /64 IPv6 to open more than 10 HTTPS?/TCP connections per second (the limit is high, but this still shield against some attacks) — DROP TCP packets in this case, to avoid generating egress traffic sending a RST
# 2. Limit ingress bandwidth to HTTPS? services to 32KB/sec (adjust to your needs, in my case it is used to shield a WebSocket backend against incoming WebSocket message floods)
# 3. Limit the number of simultaneous ongoing connections to HTTPS? to 40 (also, high limit, adjust to your needs)
# The protections those rules offer:
# 1. Prevent crypto-DOS (ie. a client that proceed too many key exchanges and thus exhaust server CPU)
# 2. Prevent WebSocket floodings (eg. I use this for Socket.IO, which has no efficient way to rate-limit received messages before they get parsed)
# 3. Prevent ephemeral TCP port exhaustion due to a client holding too many TCP connections
# 4. Prevent IPv6 rotation attacks where a client that possesses a /64 IPv6 block (most hold a /5x or /6x block) uses different ephemeral IPv6 on this /64 subnet to bypass any /128 rate-limiter. It is always safer to rate-limit on the allocated block, hence the /64 (for IPv4 the issue doesn't exist, as IPv4 are expensive and thus people are allocated /32 blocks in 99% cases).
# IPTables (IPv4)
:FIREWALL
-A INPUT -i eth0 -p tcp -m tcp -m multiport --dports 80,443 -m state --state NEW -m hashlimit --hashlimit-above 10/sec --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-name http_rate -j DROP
-A INPUT -i eth0 -p tcp -m tcp -m multiport --dports 80,443 -m hashlimit --hashlimit-above 32kb/s --hashlimit-mode srcip --hashlimit-name http_bandwidth -j DROP
-A INPUT -i eth0 -p tcp -m tcp -m multiport --dports 80,443 -m connlimit --connlimit-above 40 --connlimit-mask 32 --connlimit-saddr -j REJECT --reject-with tcp-reset
# IPTables (IPv6)
:FIREWALL
-A INPUT -i eth0 -p tcp -m tcp -m multiport --dports 80,443 -m state --state NEW -m hashlimit --hashlimit-above 10/sec --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-name http_rate --hashlimit-srcmask 64 -j DROP
-A INPUT -i eth0 -p tcp -m tcp -m multiport --dports 80,443 -m hashlimit --hashlimit-above 32kb/s --hashlimit-mode srcip --hashlimit-name http_bandwidth --hashlimit-srcmask 64 -j DROP
-A INPUT -i eth0 -p tcp -m tcp -m multiport --dports 80,443 -m connlimit --connlimit-above 40 --connlimit-mask 64 --connlimit-saddr -j REJECT --reject-with tcp-reset
@tszynalski
Copy link

tszynalski commented Jun 17, 2020

Valerian,
The rule: -A INPUT -i eth0 -p tcp -m tcp -m multiport --dports 80,443 -m hashlimit --hashlimit-above 10/sec --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-name http_rate -m state --state NEW -j DROP is not doing what you think it's doing. Iptables modules are executed in the order they are given in the rule. Because in the above rule, -m hashlimit comes before -m state, hashlimit will process every TCP packet, not just new packets, and every TCP packet will count towards the 10/sec limit. At the beginning, there is no difference, because all packets are for NEW connections, but later on, if you have an open connection that keeps sending packets, those packets will keep depleting the hashlimit counters. So you may not be able to open a new TCP connection, even though the hashlimit rate says you should be able to.

Instead, you want to first check if a packet is TCP, then check if it's for a NEW connection, and only then send it to hashlimit to be counted.

This is a really common mistake. You can see the wrong order in many places on the Web and I used it myself for years, wondering why my second or third SSH connection would sometimes fail to open. Then I looked at the internals of hashlimit (cat /proc/net/ipt_hashlimit) and realized my counters were being decremented all the time by my active SSH connection, which was of course sending some data now and again. Took me a long time to figure out what was going on. To my knowledge, this behavior is not explicitly documented anywhere, and I haven't been able to find any discussions about it. So I'm posting corrections in a few places on the Web to save people some frustration.

I think a good rule of thumb is to always put -m hashlimit last in your iptables rule.

@valeriansaliou
Copy link
Author

Thanks for the feedback. I'll make the change today.

@valeriansaliou
Copy link
Author

@tszynalski done, fixed. Does it look good to your now?

@tszynalski
Copy link

Looks like -m hashlimit comes after -m state now, so it should be OK.

@Mecanik
Copy link

Mecanik commented Nov 13, 2022

May I ask why did you not limit the bucket size?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment