Detecting Tor users in nginx on Linux


Warren GuyWarren Guy
3 November 2014

This is a slightly hacky method of detecting Tor users in nginx. I'm using it on this website to advise Tor users of the existence of a hidden service that can alternatively be used to access this website. You can visit this website via Tor to see it in action.

We'll be using iptables and ipset to match the IP of incoming connections to our nginx server against Tor exits that allow access to our site, and redirecting those connections to a different ip:port. In your nginx config, you can then add custom rules, headers, or use an alternative server block, or whatever you like. This method could be applied to services other than nginx.

But: please don't block Tor users :(.

Configure iptables/ipset

  • Install ipset:
apt-get install ipset
  • Create an ipset list for our list of tor exits:
ipset destroy torexits
ipset create torexits iphash
  • Grab the list of known tor exits that allow connections to your web server, in this case, exits that can reach warrenguy.me (176.58.108.237) on port 443 (make sure you customise the URL here to match your own ip:por):
curl "https://check.torproject.org/cgi-bin/TorBulkExitList.py?ip=176.58.108.237&port=443" > /tmp/torexits
  • Iterate over it, adding all IPs to our new list (this can take a few seconds; there are likely more than 1,000 exits to add):
for i in $( egrep -v "^#" /tmp/torexits ); do ipset add torexits $i; done
  • Add a firewall rule to redirect incoming traffic to your nginx servers ip:port to a new ip:port:
iptables -t nat -A PREROUTING -m set --match-set torexits src -d 176.58.108.237 -p tcp --dport 443  -j DNAT --to-destination 176.58.108.237:10443

We should now be redirecting all Tor users accessing the website at 176.58.108.237:443 to 176.58.108.237:10443 instead.

Configure nginx

You have two main options here. Either:

  1. Add the new ip:port to your existing server block and match it with rules; or
  2. Add a new server block and do whatever you need there.

I'm going to run with option 1 here, as it's what I'm doing. In this case, I'm using nginx as a reverse proxy to a Ruby application backend. In nginx, I'll add a header (X-Tor-User) to the backend request, and my ruby app can then create custom content dependent on the value of this header.

Relevant configuration:

http {
  map "$server_addr:$server_port" $is_tor {
    "176.58.108.237:10443" 'true';
    default 'false';
  }

  ...

  server {
    listen 176.58.108.237:443 ssl default_server;
    listen 176.58.108.237:10443 ssl default_server;

    ...

    location @app {
      ...
      proxy_set_header X-Tor-User $is_tor;
      ...
    }
  }
}

Keeping the Tor exit list updated

To keep the list up-to-date, you could run a script like this from cron every 30-60 minutes or so:

ipset destroy torexits-update
ipset create torexits-update iphash
curl "https://check.torproject.org/cgi-bin/TorBulkExitList.py?ip=176.58.108.237&port=443" > /tmp/torexits
for i in $( egrep -v "^#" /tmp/torexits ); do ipset add torexits-update $i; done
ipset swap torexits-update torexits
ipset destroy torexits-update

TAGS: SysAdmin, Tor, nginx, Linux, Debian, ipset, iptables

Next post: Reverse proxying Tor hidden services
Previous post: Regenerating an RSA private key with Python

Related posts:


View all posts