The TOR network is a service that can provide anonymity for people that want it, unfortunately, it can also provide anonymity for malicious actors to hide their identities. This page will not debate the merits of blocking traffic from the TOR network but will equip those that wish to do so with the knowledge to do exactly that.
When a user on the TOR network accesses services on the “clearnet” (your website for example) it has to pass through what is called a “TOR Exit Node”. When traffic from the TOR network arrives at your website, it will show these Exit Node’s IP addresses as the source.
In this tutorial, we will leverage the power of several utilities to block traffic from the TOR network, as well as configuring automated blacklist updates.
Preparing a directory
Before we start, let’s change to the root user and create a directory that we will use to run commands in:
# sudo su
# cd /root/
# mkdir torBlocker
# cd torBlocker
Obtaining a list of TOR exit nodes
The maintainers of the TOR network are kind enough to publish a list of all these exit nodes. We can download it with the following command:
wget https://check.torproject.org/exit-addresses
If we look at the contents of the downloaded file, we can see that the list does not contain only IP addresses, but extra information we need to get rid of.
# cat exit-addresses
ExitNode 4273E6D162ED2717A1CF4207A254004CD3F5307B
Published 2021-07-15 17:02:03
LastStatus 2021-07-16 11:00:00
ExitAddress 176.10.99.200 2021-07-16 11:54:28
ExitNode 0D12D8E72DED99EE31BB0C57789352BED0CEEEFF
Published 2021-07-15 17:59:26
LastStatus 2021-07-16 11:00:00
ExitAddress 109.70.100.28 2021-07-16 11:50:50
ExitAddress 109.70.100.40 2021-07-14 23:46:46
To extract the IPs from the file, we can use the egrep utility with a regular expression and write the output to a new file.
# egrep -o '([0-9]{1,3}\.){3}[0-9]{1,3}' exit-addresses > exit-addresses.ipset
Now we have a clean list of only IP addresses.
# cat exit-addresses.ipset
176.10.99.200
109.70.100.28
109.70.100.40
...
Using iptables to block traffic from specific addresses
To block traffic from specific IP addresses we can load rules into iptables.
To view the rules we run:
# iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
The above example shows the default rules for iptables, if you have ufw enabled there will be many more entries, this will not be a problem.
# iptables -S
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT ACCEPT
-N ufw-after-forward
-N ufw-after-input
-N ufw-after-logging-forward
-N ufw-aft…
To block an individual IP address we run the command below. The ‘1’ instructs the interpreter to insert the new rule on position #1 and shift the other rules down 1 slot.
# iptables -I INPUT 1 -s <ip_address> -j DROP
For example, if we would like to block 250.251.252.253 we run the command:
# iptables -I INPUT 1 -s 250.251.252.253 -j DROP
If we view our ruleset again, we can see the added rule.
# iptables -S
…
-A INPUT -s 250.251.252.253/32 -j DROP
…
To delete a rule, we replace the -I (insert) switch with the -D (delete) switch.
# iptables -D INPUT -s 250.251.252.253 -j DROP
Manually loading a rule for every IP is just not practical, we can leverage the power of the FOR loop to automate this process:
(For explanation only, do not run this command yet)
# for IP in $(cat <my_ipset_file>); do iptables -I INPUT 1 -s $IP -j DROP; done
This will work and immediately block all traffic from the TOR network, but it will make it very difficult in the future to flush and update the list.
Using a ‘chain’ to block traffic from the TOR network
To make our ruleset serviceable, we must separate the TOR entries from the rest of the rules. This will make updating the list easy and automatable in the future. We will now create a chain, populate this chain with our DROP entries and tell the main INPUT chain to evaluate all the rules in our user-defined chain before considering other entries. User-defined chains are denoted with the ‘-N’ switch, which is not very intuitive, I remember it by calling it a ‘Network’. User-defined chains function like subroutines that can be called from the main (-P or Primary) chain called INPUT.
We create a chain (called torBlocker in this example) by running the command:
# iptables -N torBlocker
# iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N torBlocker
…
Now that the chain is created we can instruct iptables to run this chain as the very first INPUT rule. The -j switch will cause the ruleset to “jump” to the specified chain.
# iptables -I INPUT 1 -j torBlocker
# iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N torBlocker
…
-A INPUT -j torBlocker
…
To add a rule to the top of the chain individually we run the command below:
# iptables -I <chain_name> 1 -s <ip_address> -j DROP
But as stated before, doing this one-by-one is not practical, so we adjust the FOR loop to do this for us.
# for IP in $(cat exit-addresses.ipset); do iptables -I torBlocker 1 -s $IP -j DROP; done
# iptables -S
…
-A torBlocker -s 109.70.100.40/32 -j DROP
-A torBlocker -s 109.70.100.28/32 -j DROP
-A torBlocker -s 176.10.99.200/32 -j DROP
…
Iptables functions like a sequential programming language, so we will require a RETURN statement at the very end of the chain. Instead of using the ‘-I torBlocker 1’ statement (which INSERTS a rule at position 1) we use the -A switch (which APPENDS a rule to the end of the chain).
# iptables -A torBlocker -j RETURN
# iptables -S
…
-A torBlocker -s 109.70.100.40/32 -j DROP
-A torBlocker -s 109.70.100.28/32 -j DROP
-A torBlocker -s 176.10.99.200/32 -j DROP
-A torBlocker -j RETURN
If we now try to access our website from a TOR browser, we will just get the ‘spinning wheel’ and will eventually time out.

Keeping the blocklist updated
From the perspective of the TOR network, our website is now completely unresponsive, job done! But not so quick… As stated before, the list of exit nodes changes over time and we need to keep the list updated. To update the list, we need to download and prepare the list again, flush the old entries from the chain and repopulate it with the new entries.
To get an updated list, we run the commands we did at the beginning of the exercise again.
# wget https://check.torproject.org/exit-addresses
# egrep -o '([0-9]{1,3}\.){3}[0-9]{1,3}' exit-addresses > exit-addresses.ipset
Next, we need to FLUSH (delete) all of our old entries from the chain.
# iptables -F torBlocker
Then, we need to repopulate the chain and add the RETURN statement to the end.
# for IP in $(cat exit-addresses.ipset); do iptables -I torBlocker 1 -s $IP -j DROP; done
# iptables -A torBlocker -j RETURN
This process will become tedious over time. If we can add all of the instructions to a shell script it will save us a lot of time.
Creating the script
Create a .sh file and add the commands.
# nano torBlocker.sh
#/bin/bash
cd /root/torBlocker/
wget https://check.torproject.org/exit-addresses
egrep -o '([0-9]{1,3}\.){3}[0-9]{1,3}' exit-addresses > exit-addresses.ipset
iptables -F torBlocker
for IP in $(cat exit-addresses.ipset); do iptables -I torBlocker 1 -s $IP -j DROP; done
iptables -A torBlocker -j RETURN
After you saved and exited, remember to make the script executable.
# chmod +x torBlocker.sh
Now, updating the ruleset is as simple as running the script.
# ./torBlocker.sh
Making the script run automatically
We can add a crontab entry to automatically run the script.
# crontab -e
To have the script run daily (at 01:15 in this example) we edit the file to look like this:
…
# For more information see the manual pages of crontab(5) and cron(8)
# m h dom mon dow command
15 01 * * * /root/torBlocker/torBlocker.sh
Choose to Exit and Save.
Our ruleset will now automatically update every morning at 01:15 AM.
Making the ruleset persist after restarts
By default, iptables are not persistent, which means we will lose our rules every time we restart. To solve this problem we install the iptables-persistent package and set netfilter-persistent to load on startup
# apt install iptables-persistent -y
# systemctl enable netfilter-persistent
You can select to save the iptables on the first install. The list is saved in /etc/iptables/rules.v4.
To update the saved list we must run the iptables-save command and output it to the save file.
# iptables-save > /etc/iptables/rules.v4
To save the ruleset on every update we just add the above command to the torBlocker.sh script we created.
# nano /root/torBlocker/torBlocker.sh
…
for IP in $(cat exit-addresses.ipset); do iptables -I torBlocker 1 -s $IP -j DROP; done
iptables -A torBlocker -j RETURN
iptables-save > /etc/iptables/rules.v4