All new Registrations are manually reviewed and approved, so a short delay after registration may occur before your account becomes active.
Mitigating the ongoing DDoS attack against Tor relays
There is an ongoing DDoS attack against Tor relays, and there's some discussion of mitigations on the tor-relays mailing list. The attack mostly comes from Contabo and overload the relay's CPU (it's a layer 7 attack, not layer 4). Tor operators here may have noticed that their relays may be undergoing CPU spikes or overloads.
For those using relatively stock setups and iptables, there is a simple script made by toralf which generates a firewall to largely mitigate the damage. It works by:
- exempting directory authorities and snowflake distributors from any filtering
- blacklisting IPs that make more than 8 new connections over a 2 minute period for 24 hours
- blacklisting IPs that make more than 16 new connections over a 1 hour period for 24 hours
- dropping attempts to connect more than 8 simultaneous connections
If you don't want to use nftables or don't run a Tor relay, you can stop reading here. I've made a port of this mitigation tool to nftables. While it lacks persistence (blacklists are cleared on reboot), auto-updating of directory authorities and snowflake IPs (which should rarely change anyway), and only supports servers that run a single Tor process, it does additionally give more leniency to known relays, which allows it to be significantly more strict on non-relay connections which, unlike relays that may have multiple Tor processes per server with the same IP, never have a legitimate reason to open 8 simultaneous connections, so we limit it to 4.
Step 1 (create the filter)
The following is to be put in /etc/nftables.d/tor-ddos.conf:
# set of ipv4 addresses to be dropped for one day
set tor_ddos4 {
type ipv4_addr
flags timeout
timeout 24h
size 262144
}
# set of ipv6 addresses to be dropped for one day (may include whole subnets)
set tor_ddos6 {
type ipv6_addr
flags timeout
timeout 24h
size 262144
}
# simply used to keep track of existing simultaneous ipv4 connections
set tor_connlimit4 {
type ipv4_addr
size 262144
}
# simply used to keep track of existing simultaneous ipv6 connections
set tor_connlimit6 {
type ipv6_addr
size 262144
}
# known ipv4 relay addresses, populated from tor's cached-consensus
set known_relays4 {
type ipv4_addr
flags interval
auto-merge
}
# known ipv6 relay addresses, populated from tor's cached-consensus
set known_relays6 {
type ipv6_addr
flags interval
auto-merge
}
# trusted ipv4 addresses that should never be denied
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
}
# trusted ipv6 addresses that should never be denied
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
}
# hosters that allocate /64 subnets to customers from attacks seen in the wild
define hosters64 = {
2a01:4f8::/32,
2a01:4f9::/32,
2a01:4ff:ff01::/48,
2a03:4000::/32,
2a06:be80::/29,
2a0b:f4c2::/40,
2a11:e980::/29,
2a12:2240::/29
}
# hosters that allocate /80 subnets to customers from attacks seen in the wild
define hosters80 = {
2001:67c:e60::/48,
2607:f1c0::/32,
2a00:1a68::/32,
2a00:da00:8000::/34,
2a0d:7f00::/29
}
chain tor_input4 {
# accept trusted tor servers (authorities etc)
ip saddr $tor_trusted4 accept
# limit filtering on known relays, never blacklist and allow up to 16 connections
ip saddr @known_relays4 jump {
add @tor_connlimit4 { ip saddr ct count over 16 } drop
accept
}
# blacklist any ip that makes more than 8 connections/minute over a two minute window or 16hour over an hour window
meter tor_ddos_fast4 { ip saddr timeout 2m limit rate over 8/minute burst 8 packets } update @tor_ddos4 { ip saddr }
meter tor_ddos_slow4 { ip saddr timeout 1h limit rate over 16/hour burst 16 packets } update @tor_ddos4 { ip saddr }
ip saddr @tor_ddos4 drop
# limit number of simultaneous connections
add @tor_connlimit4 { ip saddr ct count over 4 } drop
# accept the connection so we don't fall through
accept
}
chain tor_input6 {
# accept trusted tor servers (authorities etc)
ip6 saddr $tor_trusted6 accept
# limit filtering on known relays, never blacklist and allow up to 16 connections
ip6 saddr @known_relays6 jump {
add @tor_connlimit6 { ip6 saddr ct count over 16 } drop
accept
}
# is the connection from a host that offers an entire /64? (otherwise fall through)
ip6 saddr $hosters64 jump {
# blacklist any /64 that makes more than 8 connections/minute over a two minute window or 16hour over an hour window
meter tor_ddos_fast6_64 { ip6 saddr & ffff:ffff:ffff:ffff:: timeout 2m limit rate over 8/minute burst 8 packets } update @tor_ddos6 { ip6 saddr & ffff:ffff:ffff:ffff:: timeout 24h }
meter tor_ddos_slow6_64 { ip6 saddr & ffff:ffff:ffff:ffff:: timeout 1h limit rate over 16/hour burst 16 packets } update @tor_ddos6 { ip6 saddr & ffff:ffff:ffff:ffff:: timeout 24h }
ip6 saddr & ffff:ffff:ffff:ffff:: @tor_ddos6 drop
# limit number of simultaneous connections from a /64
add @tor_connlimit6 { ip6 saddr & ffff:ffff:ffff:ffff:: ct count over 4 } drop
# accept the connection so we don't fall through
accept
}
# is the connection from a host that offers an entire /80? (otherwise fall through)
ip6 saddr $hosters80 jump {
# blacklist any /80 that makes more than 8 connections/minute over a two minute window or 16hour over an hour window
meter tor_ddos_fast6_80 { ip6 saddr & ffff:ffff:ffff:ffff:ffff:: timeout 2m limit rate over 8/minute burst 8 packets } update @tor_ddos6 { ip6 saddr & ffff:ffff:ffff:ffff:ffff:: timeout 24h }
meter tor_ddos_slow6_80 { ip6 saddr & ffff:ffff:ffff:ffff:ffff:: timeout 1h limit rate over 16/hour burst 16 packets } update @tor_ddos6 { ip6 saddr & ffff:ffff:ffff:ffff:ffff:: timeout 24h }
ip6 saddr & ffff:ffff:ffff:ffff:ffff:: @tor_ddos6 drop
# limit number of simultaneous connections from an /80
add @tor_connlimit6 { ip6 saddr & ffff:ffff:ffff:ffff:ffff:: ct count over 4 } drop
# accept the connection so we don't fall through
accept
}
# blacklist any /128 that makes more than 8 connections/minute over a two minute window or 16hour over an hour window
meter tor_ddos_fast6_128 { ip6 saddr timeout 2m limit rate over 8/minute burst 8 packets } update @tor_ddos6 { ip6 saddr timeout 24h }
meter tor_ddos_slow6_128 { ip6 saddr timeout 1h limit rate over 16/hour burst 16 packets } update @tor_ddos6 { ip6 saddr timeout 24h }
ip6 saddr @tor_ddos6 drop
# limit number of simultaneous connections from a /128
add @tor_connlimit6 { ip6 saddr ct count over 4 } drop
# accept the connection so we don't fall through
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
}
Then, in your main /etc/nftables.conf, add include "/etc/nftables.d/tor-ddos.conf" at the beginning of your filter table and replace the rule that allows your ORPort with tcp dport 9001 jump tor_input (with 9001 being whatever your ORPort is). As usual, you should accept established and related connections prior. Here is an example minimal /etc/nftables.conf that allows SSH on port 22 and DDoS-protected Tor on 9001:
flush ruleset
table inet filter {
include "/etc/nftables.d/tor-ddos.conf"
chain input {
type filter hook input priority filter; policy drop;
# allow local traffic and replies to outgoing traffic
iif lo accept
ct state vmap { established : accept, related : accept, invalid : drop }
# allow tor (with ddos protection) and ssh
tcp dport 9001 jump tor_input
tcp dport 22 accept
# allow rate-limited icmp (other necessary icmp types like pmtu discovery already match ct state related)
icmp type echo-request limit rate 25/second accept
icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } limit rate 25/second accept
}
chain output {
type filter hook output priority filter; policy accept;
}
chain forward {
type filter hook forward priority filter; policy drop;
}
}
Step 2 (prepare auto-updating of known relays)
You might notice that there are two empty sets that are only referenced but never populated. They are to hold an up-to-date list of all known relays. The script that populates them is /etc/cron.daily/update_tor_nft_sets:
#!/bin/bash
test -s /var/lib/tor/cached-consensus || exit 1
(echo "flush set inet filter known_relays4"
echo "flush set inet filter known_relays6"
runuser -u debian-tor -- awk '
/^r / { v4 = v4 (v4 ? ",\n" : "") $7 }
/^a / { match($2, /\[[^]]+\]/); v6 = v6 (v6 ? ",\n" : "") substr($2, RSTART+1, RLENGTH-2) }
END {
if (v4) print "add element inet filter known_relays4 {\n" v4 "\n}"
if (v6) print "add element inet filter known_relays6 {\n" v6 "\n}"
}
' /var/lib/tor/cached-consensus) | nft -f -
The awk command is run as the debian-tor user for security reasons (reduces the amount of untrusted data the unprivileged Tor process can force root to parse). If you aren't using Debian, change the name of the Tor user there. If your main nftables table isn't called "filter" (which is the default in Debian), you'll have to replace the hardcoded names in the above script with whatever you called it.
Now, this will run once a day to keep the list of relays up to date, but it'll be empty when nftables starts on boot. To fix that, run the following as root to trigger the script when nftables is first started and whenever it's reloaded:
systemctl edit --stdin nftables.service << EOF
[Service]
ExecStartPost=-/etc/cron.daily/update_tor_nft_sets
ExecReload=-/etc/cron.daily/update_tor_nft_sets
EOF
Note that this assumes you already have Tor installed and the consensus is already cached. If you've just started Tor for the first time, you might have to run /etc/cron.daily/update_tor_nft_sets manually after Tor has synced to the network, as it is only able to get the list of known relays by parsing Tor's own cached consensus.
If you ever want to reload the configuration, you have to use systemctl reload nftables. Do not reload it with nft -f /etc/nftables.conf as that will not restore the set of known relays from the consensus file.
Step 3 (apply configuration tweaks, optional)
Add the following to /etc/sysctl.d/tor-relay.conf to tweak network settings:
# remain in FIN_WAIT_2 for only 10 seconds
net.ipv4.tcp_fin_timeout = 10
# send keepalives every 20 minutes
net.ipv4.tcp_keepalive_time = 1200
# allow TIME_WAIT sockets to be reused for new connections
net.ipv4.tcp_tw_reuse = 1
# increase the source port range for outgoing connections
net.ipv4.ip_local_port_range = 10000 64999
# allow larger accept backlog
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
# keep inactive conntrack entries for 1 day instead of 5
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
# increase the number of conntrack entries that can be kept at once
net.netfilter.nf_conntrack_max = 131072
net.netfilter.nf_conntrack_buckets = 32768
Because you're writing to NetFilter sysctls, it's necessary to tell the kernel to load the module early. This can easily be accomplished by running this as root:
echo nf_conntrack >> /etc/modules-load.d/modules.conf
Step 4 (reduce memory pressure, optional)
Tor uses the system's default memory allocator (the library that implements memory-related functions such as malloc() and free()). For glibc systems, this is ptmalloc3. Unfortunately Tor's RSS (resident set memory) gradually grows due to memory fragmentation. Considering the ongoing DDoS attack also increases memory usage, it's important for low-resource relays to reduce memory usage. While you can reduce memory usage by setting DirCache 0 in your torrc, that disqualifies you from being a guard or serving as a directory cache. Luckily, tests were done that show that the jemalloc2 memory allocator is far better at resisting memory fragmentation. To enable it, install libjemalloc2 and then run, as root:
systemctl edit --stdin [email protected] << EOF
[Service]
Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
EOF
If you aren't using Debian, verify that you've set the correct path for libjemalloc.so.2. That will automatically select jemalloc2 as a drop-in replacement memory allocator. You should also enable zswap (add zswap.enabled=1 to the kernel command line in /etc/default/grub and run update-grub) for further memory savings. Unlike zram, zswap has a physical backing device, but only uncompressible pages are swapped out there. Anything that compresses well will be kept in memory for faster access.
To ensure the stored swap is encrypted, install cryptsetup and systemd-cryptsetup and run, as root:
echo "swap SWAP_DEVICE /dev/urandom swap,cipher=aes-cbc-essiv:sha256,size=128,sector-size=4096" >> /etc/crypttab
echo "/dev/mapper/swap swap swap defaults 0 0" >> /etc/fstab
Replace SWAP_DEVICE with a swap file or existing swap partition. Make sure you disable any existing unencrypted swap in /etc/fstab first.
Once you've done all this, reboot. You should see a decrease in your CPU usage during times of DDoS and your relay will be able to handle more legitimate connections. This is especially important for low-end relays that are already only barely able to handle the load they're given.
Disclaimer: This is still a WIP. I'm in the process of deploying it on over 50 relays, so I may very well post updates with improvements or bug fixes. Additionally, it's not designed to protect against high-PPS floods. It's purely designed to limit the damage of a specific class of application-layer DDoS attacks against the Tor protocol. This is the reason why there are no micro-optimizations and why the filtering is not done in prerouting. Still, it should result in a significant reduction of CPU and memory strain caused by the ongoing attack.
Some miscellaneous ideas for the future:
- Periodically update the authority and snowflake list (see
__fill_trustlistin toralf's original scripts). - Save and restore the blacklist across reboot, perhaps with
ExecStopPreandExecStartPost. - Log a message (but take no action) if there are many connections from the same v4 /24 or v6 /64.
- Don't blacklist IPs if Tor was just installed and there's no consensus file to obtain known relays from.


Comments
Do you have consider have website or blog to preserve your article/though? I kinda like stuff like this but, I hopefully can following on my RSS (sorry, I am RSS Junkie).
I don't have any website. I just write things here and on Stack Exchange.