Source based routing with wireguard
What this article is about?
This article describes how to configure a linux router to send traffic from specific IPs to a non-default (wireguard) route. With such a setup, you will be able to use a VPN with ‘smart’ devices (A TV, Nintendo Switch, etc…) which do not have native wireguard support.
Configure wireguard interface
First, we configure a new wireguard interface which we will call sbr0
. Note that we are not using wg-quick
to bring the device up since we want to have full control over its configuration (and not use it as a default route anyway).
To bring the interface up, we can use the following script:
# The IPv4 to assign to the sbr0 device
IP=10.69.97.36/32
# The IPv6 to assign to the sbr0 device
IP6=fc00:bbbb:bbbb:bb01::8:fe93/128
# Pubkey of the remote peer
PUBKEY="+03cLSQzggzB01wyCyh4YPjIo3yBFX5TP6Fs47AJnSA="
# Endpoint of the remote peer
ENDPOINT=185.209.12.12:12345
ip link add dev sbr0 type wireguard
ip address add dev sbr0 $IP
ip address add dev sbr0 $IP6
## Note: this expects the private key to be stored in './privkey.key':
wg set sbr0 private-key ./privkey.key peer $PUBKEY allowed-ips 0.0.0.0/0,::0/0 endpoint $ENDPOINT
# Put returnpath filter into 'relaxed' mode for sbr0; without this, traffic will be dropped.
sysctl -w net.ipv4.conf.sbr0.rp_filter=2
# Finally, bring the device up:
ip link set up dev sbr0
Once you executed this script, you should be able to see the interface in wireguard by running wg show sbr0
and via ip a show sbr0
.
Adding fwmark
rules
The sbr0
interface should now be up but unusued, since we did not configure it to be our default route (which is not what we would want).
The next task is to add it as a default route for a newly created table and tell the kernel to have packets with a certain fwmark
use this table.
This can all be done using the ip
command.
In the following example, we will create a new table with the ID ‘815’ and tell the kernel to use it for packets with the fwmark set to ‘123’:
# Configure sbr0 route for IPv4 and IPv6
ip -4 route add 0.0.0.0/0 dev sbr0 table 815
ip -6 route add ::0/0 dev sbr0 table 815
# Create fwmark rules to point to table 815
ip -4 rule add from all fwmark 123 table 815
ip -6 rule add from all fwmark 123 table 815
Ok, almost there: We now have:
- An unused wireguard interface named
sbr0
- A routing table (
815
) which points tosbr0
… - …which is never used since normal traffic will not match the
fwmark 123
rule.
Classify packets
Ok, time to hop over to our nftables config and tell the kernel to set a fwmark on packets which come from certain IPs.
# Internal/LAN interface (which receives traffic from sbr_net-ranges)
define lan_if = "eth9"
# ALWAYS use iifname for sbr0! the interface may change or not be present.
define sbr_if = sbr0
define sbr_net4 = { 192.168.3.123}
define sbr_net6 = { 2a02:1:2:3::df32/128 }
table inet filter {
...
chain sbr_classify {
comment "mark all traffic for sbr routing table in range which should use source based routing"
type filter hook prerouting priority -150;
ip saddr $sbr_net4 meta mark set 123
ip6 saddr $sbr_net6 meta mark set 123
}
chain postrouting {
type nat hook postrouting priority 100;
...
iif $lan_if oifname $sbr_if ip saddr $sbr_net4 counter masquerade
iif $lan_if oifname $sbr_if ip6 saddr $sbr_net6 counter masquerade
...
}
}
So what did we do here?
This config snippet:
- Defines the source IP (ranges) which should use our wireguard tunnel
- Creates the new
sbr_classify
chain which applies thefwmark
to traffic coming from these ranges - Tells the kernel to masquerade in-scope traffic (
postrouting
chain)
Does it work?
Maybe? Depends on how paranoid your existing nftables rules are. My config has some pretty strict rules when it comes to forwarding, so we need to explicitly allow traffic to be forwarded from and to sbr0
:
table inet filter {
chain forward {
type filter hook forward priority 0; policy drop;
...
# make sure that FORWARDING is allowed:
iif $lan_if oifname $sbr_if ip saddr $sbr_net4 counter accept
iifname $sbr_if oif $lan_if ip daddr $sbr_net4 counter accept
iif $lan_if oifname $sbr_if ip6 saddr $sbr_net6 counter accept
iifname $sbr_if oif $lan_if ip6 daddr $sbr_net6 counter accept
}
}
After this, you should be good to go. A quick way to check whether or not things are working as expected is to use the ip4.me and ip6.me API:
$ wget -O - -q http://ip4.me/api
...
$ wget -O - -q http://ip6.me/api
...