LinuxNetworking

How to route traffic to the machine hosting your Wireguard server through the Wireguard tunnel

I have been using Wireguard for some time for private use and I like it so much that I even installed the Android app for it on my phone to connect to my Wireguard server from anywhere.

However, I had a bit of a maybe weird request for Wireguard: I wanted clients to be able to route traffic to the machine that hosts the Wireguard server via that same tunnel but other ports than the one used by Wireguard.

Think of it this way: you have a multi-purpose Linux machine which hosts multiple servers (e.g. Apache, MySQL, ES or whatever) on top of a Wireguard server listening on port 56000. As a client of that VPN, you want to encrypt traffic sent to e.g. port 443 used by Apache. The problem is that by default and unless you customize it with additional PostUp and PreDown rules, this won’t work out of the box even if you add the Wireguard server’s IP in the AllowedIPs configuration of the client.

But even after spending hours of research into Wireguard, iptables, ip route, ip rule, fwmark and so on I just could not get it to work. The funny thing though is that it works out of the box with the Android app: you add the Endpoint IP to the AllowedIPs list and traffic sent to it sure enough is routed through the tunnel. I even analyzed the behavior with Termux on Android to compare ip rule and ip route outputs before and after enabling the VPN but still, I could not quite figure out how to make it work on a “normal” Linux client (VM or desktop).

That’s when, after hours searching for existing threads online covering the same issue that I finally found the answer I was looking for on Reddit and specifically on this post.

This was the configuration to be added in [Interface] on top of adding Wireguard’s server IP in AllowedIPs:

Table = 55999
FwMark = 55999
PostUp = ip rule add not fwmark 55999 table 55999
PostUp = ip rule add table main suppress_prefixlength 0
PreDown = ip rule del table main suppress_prefixlength 0
PreDown = ip rule del not fwmark 55999 table 55999

Some explanations:

  • Table = 55999 means that ip route commands created by using AllowedIPs are adding routes in a new table with the ID 55999.
  • FwMark = 55999 means that UDP packets created by Wireguard are specifically marked and thus separated from other packets that are not.
  • PostUp = ip rule add not fwmark 55999 table 55999 means that a rule is added so that any packet not marked with fwmark 55999 uses the route table with the ID 55999.
  • PostUp = ip rule add table main suppress_prefixlength 0 (by the way a rule automatically added when you add 0.0.0.0/0 to AllowedIPs to have all your traffic routed through the Wireguard tunnel) means, according to ip rule’s man page, that we add a rule in the main route table that “rejects routing decisions that have a prefix length of 0 or less”. In other words this “will ignore any /0 prefix from the main routing table, ie, the ‘default’ default route”.
  • The PreDown commands cancel the effects of the PostUp commands.

Maybe showing the difference in the output of ip rule show table main can help make more sense of this:

# Before enabling the vpn:    

32766:  from all lookup main 

# After enabling the vpn:    

32764:  from all lookup main suppress_prefixlength 0    
32766:  from all lookup main

As you can see, the rule pertaining to the default route in the main table is not actually deleted but its priority is decreased by having the new rule added right before it (lower number means higher priority). If you then run ip rule show, this is what you’ll see:

root@debian-test:~# ip rule show
0:      from all lookup local
32764:  from all lookup main suppress_prefixlength 0
32765:  not from all fwmark 0xdabf lookup 55999
32766:  from all lookup main
32767:  from all lookup default

Here’s how you make sense of it. Let’s assume for the sake of this explanation that your Wireguard server IP is 3.56.100.55. Run the following commands:

root@debian-test:~# ip route get 3.56.100.55
3.56.100.55 dev wg0 table 55999 src 10.67.67.2 uid 0 cache

root@debian-test:~# ip route get 3.56.100.55
3.56.100.55 via 192.168.0.1 dev enp0s18 src 192.168.0.207 mark 0xdabf uid 0 cache

A different route is used depending on whether or not packets are marked with 55999! So basically this is what happens:

  1. Let’s say you hit the Apache server on the same machine as your Wireguard server with port 443.
  2. Main table is checked but without the default route (rule 32764).
  3. Because the default route is ignored (0.0.0.0/0) and there can’t be a match, the next rule is up (rule 32765) and the table 55999 is checked since packets sent to port 443 on 3.56.100.55 are not marked.
  4. Ah! There’s a route for exactly this use case! Look:
root@debian-test:~# ip route show table 55999
10.67.67.0/24 dev wg0 scope link
3.56.100.55 dev wg0 scope link

Because there’s a hit, this is the route that gets chosen and your packets are sent through the tunnel. If for instance we were doing the same exercise but with a completely other IP like 8.8.8.8, there wouldn’t be a match in this table and the next rule (32766) would be up where the default route (0.0.0.0/0) would be back in business. This would let it go through the normal process of masquerade to exit the LAN without going through the tunnel.

This works like a charm and I’m really happy I eventually found a working implementation even though I wasn’t the one to figure it out. Maybe this post will help others running into the same issue find a solution to this problem. I sure learnt a couple of new things thanks to this so I don’t see these spent hours as wasted but rather as helpful!

Leave a Reply

Your email address will not be published. Required fields are marked *