DDoS solution for Tor nodes - nftables variant implemented
I eventually managed to have it [1] for nftables too and will continue to maintain both variants. The iptables Readme is moved to [2]. Give it a try. Thx -- Toralf [1] https://github.com/toralf/torutils#the-rule-set [2] https://github.com/toralf/torutils/blob/main/README-iptables.md
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 Hello. On 2026-09-13 08:44, Toralf Förster via tor-relays wrote:
I eventually managed to have it [1] for nftables too and will continue to maintain both variants.
I wrote an nftables port as well, with some adjustments. It relies on populating an nftables set with the list of known relays (overriding the default /0 whitelist) using a script in cron.daily: #!/bin/sh test -s /var/lib/tor/cached-consensus || exit 1 runuser -u debian-tor awk ' /^r / { v4 = v4 (v4 ? ", " : "") $7 } /^a / { match($2 ,/\[[^]]+\]/); v6 = v6 (v6 ? ", " : "") substr($2, RSTART+1, RLENGTH-2) } BEGIN { print "flush set inet filter known_relays4" print "flush set inet filter known_relays6" } END { if (v4) print "add element inet filter known_relays4 { " v4 " }" if (v6) print "add element inet filter known_relays6 { " v6 " }" } ' /var/lib/tor/cached-consensus | nft -f - Below is the nftables include file, which gets referenced in the input chain with "tcp dport 9001 jump tor_input". The script is similar to yours but it implicitly trusts Tor relays and is more aggressive with non-Tor connections, batching together entire /24s. It doesn't seem like it's realistically causing many false positives, since it's highly unlikely that more than 8 new connections will be initiated from within the same /24 within a single minute legitimately (or 32 in two hours): define mask4_24 = 255.255.255.0 define mask6_64 = ffff:ffff:ffff:ffff:: set tor_ddos4 { type ipv4_addr flags timeout timeout 24h } set tor_ddos6 { type ipv6_addr flags timeout timeout 24h } set tor_connlimit4 { type ipv4_addr } set tor_connlimit6 { type ipv6_addr } set known_relays4 { type ipv4_addr flags interval auto-merge elements = { 0.0.0.0/0 } } set known_relays6 { type ipv6_addr flags interval auto-merge elements = { ::/0 } } define tor_trusted4 = { # snowflake servers 141.212.118.18, 193.187.88.42, 193.187.88.43, 193.187.88.44, 193.187.88.45, 193.187.88.46, # directory authorities 45.66.35.11, 66.111.2.131, 128.31.0.39, 131.188.40.189, 171.25.193.9, 193.23.244.244, 199.58.81.140, 204.13.164.118, 216.218.219.41, 217.196.147.77 } define tor_trusted6 = { # snowflake servers 2a0c:dd40:1:b::42, 2607:f018:600:8:be30:5bff:fef1:c6fa, # directory authorities 2001:470:164:2::2, 2001:638:a000:4140::ffff:189, 2001:678:558:1000::244, 2001:67c:289c::9, 2610:1c0:0:5::131, 2620:13:4000:6000::1000:118, 2a02:16a8:662:2203::1 } chain tor_input4 { ip saddr $tor_trusted4 accept ip saddr @known_relays4 jump { add @tor_connlimit4 { ip saddr ct count over 16 } drop accept } tcp flags syn / syn,ack jump { meter tor_ddos_fast4 { ip saddr & $mask4_24 timeout 2m limit rate over 8/minute burst 8 packets } update @tor_ddos4 { ip saddr & $mask4_24 } meter tor_ddos_slow4 { ip saddr & $mask4_24 timeout 1h limit rate over 32/hour burst 32 packets } update @tor_ddos4 { ip saddr & $mask4_24 } } ip saddr & $mask4_24 @tor_ddos4 drop tcp flags syn / syn,ack add @tor_connlimit4 { ip saddr & $mask4_24 ct count over 20 } drop accept } chain tor_input6 { ip6 saddr $tor_trusted6 accept ip6 saddr @known_relays6 jump { add @tor_connlimit6 { ip6 saddr ct count over 16 } drop accept } tcp flags syn / syn,ack jump { meter tor_ddos_fast6 { ip6 saddr & $mask6_64 timeout 2m limit rate over 8/minute burst 8 packets } update @tor_ddos6 { ip6 saddr & $mask6_64 } meter tor_ddos_slow6 { ip6 saddr & $mask6_64 timeout 1h limit rate over 32/hour burst 32 packets } update @tor_ddos6 { ip6 saddr & $mask6_64 } } ip6 saddr & $mask6_64 @tor_ddos6 drop tcp flags syn / syn,ack add @tor_connlimit6 { ip6 saddr & $mask6_64 ct count over 20 } drop accept } chain tor_input { # requiring syn effectively disables loose connection tracking (nf_conntrack_tcp_loose) for this chain ct state new tcp flags syn jump { meta nfproto ipv4 jump tor_input4 meta nfproto ipv6 jump tor_input6 } drop } chain tor_kill_established { type filter hook input priority filter - 1; policy accept; ct state established tcp dport 9001 jump { ip saddr & $mask4_24 @tor_ddos4 drop ip6 saddr & $mask6_64 @tor_ddos6 drop } } In my testing, this reduced load on relays under attack slightly more than the original, primarily because it could get away with stricter thresholds without worrying about accidentally blocking relays. Regards, forest -----BEGIN PGP SIGNATURE----- iHUEARYKAB0WIQQtr8ZXhq/o01Qf/pow+TRLM+X4xgUCaqZrXQAKCRAw+TRLM+X4 xh4CAP9/9SQeJZE/Um/6EeJOU8JIwEm1Fjpk+VRuzfFJLnDxzgD+OKyklSOoeULx LyEng4AzFxOJqlAVdORCOw97CLIlzwY= =0YYx -----END PGP SIGNATURE-----
On Sunday, 13 September 2026 10:44 Toralf Förster via tor-relays wrote:
I eventually managed to have it [1] for nftables too and will continue to maintain both variants. Yay, I've been wondering for years why you aren't using nftables. ;-)
2 hints: - Use `auto-merge` and `interval` flag in dynamic sets. https://wiki.nftables.org/wiki-nftables/index.php/Sets - Block crazy packets like this in line 172, in chain 'ingress' before conntrack: tcp flags & (syn|rst|ack|fin) != syn ct state new counter name "badflags" drop https://samuel.forestier.app/blog/security/nftables-hardening-rules-and-good... https://wiki.nftables.org/wiki-nftables/index.php/Simple_ruleset_for_a_serve... Example: table netdev filter { # List of ipv4 addresses to block. # add new IP: nft add element netdev filter blocklist_v4 \{ 192.168.20.20 \} # add new subnet: nft add element netdev filter blocklist_v4 \{ 192.168.22.0/24 \} # list elements: nft list set netdev filter blocklist_v4 set blocklist_v4 { type ipv4_addr flags interval counter auto-merge elements = { 37.187.92.9, 81.201.60.130, 173.212.234.100 } } # List of ipv6 addresses to block. set blocklist_v6 { type ipv6_addr flags interval counter auto-merge } chain ingress { type filter hook ingress devices = { enp4s0f0, enp4s0f1 } priority -500; # Drop all IP FRAGMENTS ip frag-off & 0x1fff != 0 counter drop # Drop DDoS IPs ip saddr @blocklist_v4 counter drop ip6 saddr @blocklist_v6 counter drop # Drop TCP XMAS packets. tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|psh|ack|urg counter drop # Drop TCP NULL packets. tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop # Drop uncommon TCP MSS values. tcp flags syn tcp option maxseg size 1-535 counter drop } } - block INVALIDs in prerouting: Example: table inet mangle { # List of known ipv4 addresses to allow. set whitelist_v4 { type ipv4_addr flags interval auto-merge elements = { 185.220.100.0/22 } } # List of known ipv6 addresses to allow. set whitelist_v6 { type ipv6_addr flags interval auto-merge elements = { 2a0b:f4c2::/40 } } chain prerouting { type filter hook prerouting priority -150; # Allow loopback traffic. iifname lo accept # Allow whitelisted IPs ip saddr @whitelist_v4 counter accept ip6 saddr @whitelist_v6 counter accept # CT INVALID ct state invalid counter drop # TCP SYN (CT NEW) tcp flags & (fin|syn|rst|ack) != syn ct state new counter drop } } -- ╰_╯ Ciao Marco! Debian GNU/Linux It's free software and it gives you freedom!
I wrote it in a confusing way:
- Block crazy packets like this in line 172, in chain 'ingress' before conntrack: Block crazy packets in chain 'ingress' before conntrack & defrag and like this in line 172, in mangle (prerouting) tcp flags & (syn|rst|ack|fin) != syn ct state new counter name "badflags" drop
# enum for the IPv4 protocol. enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_RAW_BEFORE_DEFRAG = -450, NF_IP_PRI_CONNTRACK_DEFRAG = -400, NF_IP_PRI_RAW = -300, NF_IP_PRI_SELINUX_FIRST = -225, NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0, NF_IP_PRI_SECURITY = 50, NF_IP_PRI_NAT_SRC = 100, NF_IP_PRI_SELINUX_LAST = 225, NF_IP_PRI_CONNTRACK_HELPER = 300, NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, NF_IP_PRI_LAST = INT_MAX, }; -- ╰_╯ Ciao Marco! Debian GNU/Linux It's free software and it gives you freedom!
On 13/09/2026 12:42, boldsuck via tor-relays wrote: Thanks for the reply
- Use `auto-merge` and `interval` flag in dynamic sets.
This gives "(Operation not supported)" here, likely due to the dynamic flag.
- Block crazy packets like this in line 172, in chain 'ingress' before conntrack:
This would be a larger task, for the future. For now I like to stabilize the current setup. Since today I was proven (again) that without the ruleset my tiny relays are walking deads. With the ruleset they do all perform. -- Toralf
On 13/09/2026 11:22, forest via tor-relays wrote:
chain tor_kill_established { type filter hook input priority filter - 1; policy accept;
ct state established tcp dport 9001 jump { ip saddr & $mask4_24 @tor_ddos4 drop ip6 saddr & $mask6_64 @tor_ddos6 drop } }
IMO this breaks the ruleset in a silent way - visible only after 24 hours of runtime. The origin ([1]) blocks for *at least* 1 day after the last malicious packet arrived (see [2] too) whereas the variant above blocks for only 1 day after the first packet arrived. [1] https://github.com/toralf/torutils#the-rule-set [2] https://github.com/toralf/torutils/blob/main/nftables-ingress.conf#L157 -- Toralf
participants (3)
-
boldsuck -
forest -
Toralf Förster