Usually when you setup a VPN connection, you notice that all your traffic is routed through the VPN. In some cases, this may not be what you want. For example, I wanted to setup a VPN server which is used only when requesting certain IPs. For other traffic, it’s routed normally with my ISP. To achieve such purpose, I found that I could use policy-based routing with WireGuard (or any other L3 VPN). Additionally, I wanted to make the whole thing transparent so I did this setup on my router. Thus, any device connected in my home network would benefit from such setup. In this post, I will describe how I made it work.
Introduction to policy-based routing
For those of you who are not familiar with routing, let me first introduce what policy-based routing is. Normally with a single routing table, we would match an destination IP to a certain rule which determines where to send this packet to. Therefore, if we want to selectively route certain packets via a particular network interface, we could just insert many rules into the routing table. However, this is not really maintainable as you create more and more specific routing rules.
A better way to handle this is to use policy-based routing which allows you to create additional routing tables and use this new table when certain condition is met. Now, for our purpose, what we need to do is to group the destination IPs which we want to route differently, add a mark to those packets having such destination IPs, and tell the system to use the additional routing table if the packet contains this mark.
Let’s see how to do this in Linux.
Adding routing rules
As mentioned above, we want to use a different routing table when a certain mark is found in a packet. First let’s create the routing table.
$ sudo ip route add 0.0.0.0/0 dev <iface> table 12345
This add a routing table with table id
12345 and the content is saying that any packet routed using
this table will go out from the interface
<iface> (your VPN interface). Next, we need to add a rule to make use of this
new routing table for certain packets. Assuming that we are going to add a mark
12345 to the packets,
we need to do
$ sudo ip rule add fwmark 12345 table 12345
Ok, we have the routing rules setup. However, none of the packets floating around has the
How do we do that? Remember, we want to apply this to a list of IPs. Let’s create an
ipset first. This
allows you to create a logical “group” and add IPs into it. Later we could then match against this group instead
of individual IPs when adding the mark. To create such a group manually, we can do
$ ipset create specialgrp hash:ip family inet hashsize 1024 maxelem 65536 $ ipset add specialgrp 220.127.116.11 $ ipset add specialgrp 18.104.22.168 $ ipset add specialgrp 22.214.171.124
This works if you know the IPs. What can you do if let’s say you want to route all IPs belonging to a domain?
We could utilize
dnsmasq’s dynamic ipset feature to achieve this. Assuming you have it installed, you can add
the following lines into the configuration file
With such configuration,
dnsmasq will add all the resolved IPs belonging to
b.net into the
specialgrp ipset. Remember to use this DNS for your devices in the network! The IPs will only be added to the
ipset when you actually use the DNS to resolve those domains.
Marking the packets
We’ve finally come to the step where we mark the packets based on the group we created earlier. This is
iptables provides the ability to match the IP against an ipset in a rule, exactly
what we need! Let’s create the rules
$ iptables -t mangle -N SMARTROUTE $ iptables -t mangle -A PREROUTING -j SMARTROUTE $ iptables -t mangle -A SMARTROUTE -d 0.0.0.0/8 -j RETURN $ iptables -t mangle -A SMARTROUTE -d 10.0.0.0/8 -j RETURN $ iptables -t mangle -A SMARTROUTE -d 127.0.0.0/8 -j RETURN $ iptables -t mangle -A SMARTROUTE -d 169.254.0.0/16 -j RETURN $ iptables -t mangle -A SMARTROUTE -d 172.16.0.0/12 -j RETURN $ iptables -t mangle -A SMARTROUTE -d 192.168.0.0/16 -j RETURN $ iptables -t mangle -A SMARTROUTE -d 126.96.36.199/4 -j RETURN $ iptables -t mangle -A SMARTROUTE -d 240.0.0.0/4 -j RETURN $ iptables -t mangle -A SMARTROUTE -i <iface> -m set --match-set specialgrp dst -j MARK --set-mark 12345
Here we create a chain called
SMARTROUTE in the
mangle table and let the
PREROUTING chain jump to this chain
instead. This allows us to disable the rules by removing the jump rule if we need to. Inside this
it’s really the last line that marks the packets if the destination IP belongs to the
specialgrp ipset. The
rules above is to make sure you don’t mark any local packets by accident.
You’ve got your packets marked. What’s left? Well, don’t forget to enable IP masquerade. This changes the
source IP address for the packets going out from the VPN interface so that the reply packets could come
back. This is also done with
iptables by updating the
nat table. Here the
<iface> is also your VPN interface.
$ iptables -t nat -A POSTROUTING -o <iface> -j MASQUERADE
In this post I described how you could achieve “smart” and transparent routing on your router with policy-based routing. Personally I find it really handy because all your home devices can automatically utilize such configuration without the need to setup anything on the end devices. It could also work if you are not using your router to setup all these, but you need some additional tweaks like changing the gateway, etc. I hope you find my post useful if you are trying to do a similar setup. Let me know your experiences!