Smart and transparent routing with policy-based routing

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

Grouping IPs

Ok, we have the routing rules setup. However, none of the packets floating around has the 12345 mark. 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 1.1.1.1
$ ipset add specialgrp 2.2.2.2
$ ipset add specialgrp 3.3.3.3

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

ipset=/a.com/specialgrp
ipset=/b.net/specialgrp

With such configuration, dnsmasq will add all the resolved IPs belonging to a.com and 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 done with iptables. 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 224.0.0.0/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 SMARTROUTE chain, 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.

Final touch

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

Conclusion

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!

References

 
comments powered by Disqus