Transparent access to .onion websites

This is a very basic setup but I’m sharing mine in hopes of saving someone a bunch of googling.

<insert the usual drill about TOR and anonymity and why it’s important>

So my goal was not to anonymize my every move, but rather to be able to key in an *.onion website into the URL bar of any device on the local network and have that delivered transparently. I happen to own a fancy Asus router with AsusWRT-Merlin on board, but the approach should work on pretty much any Linux box that can route traffic.

Step 1: Get TOR installed. Use the package manager available for your router/Linux box. Here’s my config from the router:

SocksPort 9050
Log notice file /tmp/torlog
AutomapHostsOnResolve 1
TransPort 9040
DNSPort 9053
RunAsDaemon 1
DataDirectory /tmp/.tordb
AvoidDiskWrites 1

A client must first request the domain to be resolved through TOR’s DNS (which in our case will be available at TOR will respond with an address from the subnet. The browser will then attempt to connect to said IP address, which our router should intercept and redirect to

Step 2: Figure out DNS resolution. We want to resolve *.onion domains through TOR and resolve everything else through our regular DNS server. My router uses dnsmasq, so adding this line to the config file should do the trick:


Kill and restart dnsmasq for this to take effect.

Step 3: Intercept and redirect. Easy:

iptables -t nat -A PREROUTING -d -i br0 -p tcp -m tcp -j REDIRECT --to-ports 9040

At this point you should be able to open an *.onion website in your browser and see it just work!

Step 4: Making the changes persistent. This one will depend on how your router firmware handles that. For mine, I created two new files in the persistent partition (a.k.a. JFFS).



iptables -t nat -A PREROUTING -d -i br0 -p tcp -m tcp -j REDIRECT --to-ports 9040

And finally:

chmod a+rx /jffs/scripts/*

That’s it. Read more on user scripts in AsusWRT-Merlin here if you feel like it.

How to: route VM traffic based on destination port

Suppose you need to route all outbound traffic to a certain destination port though a different interface (VPN, GRE, you name it). Not something you have to do every day, but if you have found this article you probably know what you are doing at this point. Suppose your virtualization host has a public IP of and the host you want to route the traffic through has a public IP of

Set up your GRE tunnel

This section is here for the sake of completeness. If you feel confident setting up your own tunnels or VPNs, just skip ahead.
Let’s make sure the ip_gre module is present in the system and set it to autoload:

lsmod | grep gre
modprobe ip_gre
echo ip_gre >> /etc/modules

Next step, let’s create the tunnel on the VM host and assign a local IP to it:

ip tunnel add gre01 mode gre remote local ttl 255
ip link set gre01 up
ip addr add dev gre01

Tunnel creation on the remote host is identical:

ip tunnel add gre01 mode gre remote local ttl 255
ip link set gre01 up
ip addr add dev gre01

At this point the tunnel is up and we should be able to ping the remote host from the VM host:


Time to mangle some traffic

Create a routing table on the VM host and point it to your remote host. I used number 12 here, that’s arbitrary as long as it doesn’t conflict with existing tables on your system.

ip rule add fwmark 12 table 12
ip route add default via table 12
ip route flush cache

Add the iptables rule that will set the forwarding mark on the VM host:

iptables -t mangle -A OUTPUT -p tcp --dport 8888 -j MARK --set-mark 12

This assumes that your VMs have bridged interfaces. If you’re running a NAT setup, you’ll need to put this into the prerouting chain instead:

iptables -t mangle -A PREROUTING -p tcp --dport 8888 -j MARK --set-mark 12

Now, to make sure the remote host accepts this:

iptables -t nat -A POSTROUTING -o gre01 -j SNAT --to-source

Finally, we need to loosen up the reverse path filter on the GRE interface of the VM host. Obviously, substitute gre01 with whatever interface you have configured:

sysctl -w net.ipv4.conf.gre01.rp_filter=2

Now, just to add a masquerade rule on the remote host, and we are done:

iptables -t nat -A POSTROUTING -s -j MASQUERADE


We have configured a set of rules that will allow our VMs “use” a different public IP address for their outbound connections depending on the destination port. The important bit is that this is completely transparent for the VMs.

You will probably want to make these changes permanent by configuring persistent rules and interfaces on both hosts. We have also used a trick called loose mode reverse filtering, which you can read up on here: