routing Linux processes through WireGuard
Common knowledge goes that it's straightforward to achieve split routing / split tunneling with WireGuard and Linux. The details aren't trivial.
The Linux network stack can readily differentiate between processes running as
different users (by uid
).
I decided there's no choice but to add a "service" user that will run the code
which needs to be routed through the WireGuard tunnel. I created a user called
tunneler
, which ended up with uid=1002
.
The WireGuard Tunnel
In its most basic setup - route all traffic through the tunnel - WireGuard will
use a fwmark
, two ip-rule
s and an ip-route
table.
- My LAN is on
192.168.1.0/24
. -
The WireGuard tunnel will be
192.168.2.0/24
(alsofd00:1337::/64
).- This machine, the local peer,
192.168.2.2
(alsofd00:1337::2
). - The remote peer
192.168.2.1
(alsofd00:1337::1
). Calling this a WireGuard server (in WireGuard terminology it's just a peer that happens to perform the routing, calling it "server" is a stretch).
- This machine, the local peer,
All of the local peer tunnel's control packets will get fwmark = 51820
from
within the WireGuard driver itself. The default rule for dealing with
0.0.0.0/0
will be "muted", priority 32764
in the listing:
ip rule
0: from all lookup local
32764: from all lookup main suppress_prefixlength 0
32765: not from all fwmark 0xca6c lookup 51820
32766: from all lookup main
32767: from all lookup default
Then routes for all packets that aren't WireGuard's control messages
(fwmark=51820
) will be looked up in table 51820
.
The WireGuard tunnel is maintained with UDP packets sent between the local and the remote peer. Those packets need to be sent over the "real" network interface and not over the tunnel. The tunnel is a virtual construct on top of the "real" network interfaces.
Table 51820
just uses the WireGuard interface wg0
as the default:
ip route show table 51820
default dev wg0 scope link
iptables
Way
The This is the "manual" method and it's less elegant than the one I show in the next section.
I'll mark myself with iptables
all network packets generated by user
tunneler
, then route the marked packets through wg0
.
The WireGuard config for this:
[Interface]
Address = 192.168.2.2/24, fd00:1337::2/64
PrivateKey = <key goes here>
Table = 31337
PostUp = iptables -t mangle -A OUTPUT -d 192.168.1.0/24 -j ACCEPT #1
PostUp = iptables -t mangle -A OUTPUT \! -d `wg show wg0 endpoints | cut -f2 | cut -d':' -f1` -m owner --uid-owner tunneler -j MARK --set-mark 31337 #2
PostUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE #3
PostUp = ip rule add fwmark 31337 table 31337 prio 31337 #4
PostUp = ip6tables -t mangle -A OUTPUT -d fc00::/7 -j ACCEPT #1 for ipv6
PostUp = ip6tables -t mangle -A OUTPUT -m owner --uid-owner tunneler -j MARK --set-mark 31337 #2 for ipv6
PostUp = ip6tables -t nat -A POSTROUTING -o wg0 -j MASQUERADE #3 for ipv6
PostUp = ip -6 rule add fwmark 31337 table 31337 prio 31337 #4 for ivp6
PreDown = iptables -t mangle -D OUTPUT -d 192.168.1.1 -j ACCEPT #undo 1
PreDown = iptables -t mangle -D OUTPUT \! -d `wg show wg0 endpoints | cut -f2 | cut -d':' -f1` -m owner --uid-owner tunneler -j MARK --set-mark 31337 #undo 2
PreDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE #undo 3
PreDown = ip rule del fwmark 31337 table 31337 prio 31337 #undo 4
PreDown = ip6tables -t mangle -D OUTPUT -d fc00::/7 -j ACCEPT #undo 1 for ipv6
PreDown = ip6tables -t mangle -D OUTPUT -m owner --uid-owner tunneler -j MARK --set-mark 31337 #undo 2 for ipv6
PreDown = ip6tables -t nat -D POSTROUTING -o wg0 -j MASQUERADE #undo 3 for ipv6
PreDown = ip -6 rule del fwmark 31337 table 31337 prio 31337 #undo 4 for ipv6
[Peer]
PublicKey = <peer key goes here>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <my.wireguard.server:port>
PersistentKeepalive = 25
-
Accept everything
tunneler
might try on the local peer's LAN:iptables -t mangle -A OUTPUT -d 192.168.1.0/24 -j ACCEPT
This is here so that the next rule gets short-circuited for LAN comms.
-
Mark packets from user
tunneler
withfwmark=31337
. It's important to exclude WireGuard's UDP control messages. I added a predicate that skips packets directly addressed to the WireGuard server, I only used the destination IP (DNS names don't work at theiptables
level) and I only showcase this for ipv4 - it should be similar for ipv6:iptables -t mangle -A OUTPUT \ \! -d `wg show wg0 endpoints | cut -f2 | cut -d':' -f1` \ -m owner --uid-owner tunneler -j MARK --set-mark 31337
- Do a regular NAT for what exits through the WireGuard interface
wg0
. -
Send traffic with with
fwmark=31337
to routing table31337
. That table is created by WireGuard itself due to theTable = 31337
config directive:ip route show table 31337 default dev wg0 scope link
ip-rule
Way
The This is the more slipstreamed version.
[Interface]
Address = 192.168.2.2/24, fd00:1337::2/64
PrivateKey = <key goes here>
Table = 31337
PostUp = ip rule add uidrange `id -u tunneler`-`id -u tunneler` table 31337 prio 31337 #1
PostUp = ip -6 rule add uidrange `id -u tunneler`-`id -u tunneler` table 31337 prio 31337 #1 for ipv6
PostUp = ip rule add to 192.168.1.0/24 table main prio 31336 #2
PostUp = ip -6 rule add to fc00::/7 table main prio 31336 #2 for ipv6
PostDown = ip rule del uidrange `id -u tunneler`-`id -u tunneler` table 31337 prio 31337 #undo 1
PostDown = ip -6 rule del uidrange `id -u tunneler`-`id -u tunneler` table 31337 prio 31337 #undo 1 for ipv6
PostDown = ip rule del to 192.168.1.0/24 table main prio 31336 #undo 2
PostDown = ip -6 rule del to fc00::/7 table main prio 31336 #undo 2 for ipv6
[Peer]
PublicKey = <peer key goes here>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <my.wireguard.server:port>
PersistentKeepalive = 25
ip-rule
itself can deal with user ids, soiptables
is not needed anymore. Everything originating from usertunneler
(uid=1002
in my case) will be routed according to table31337
.- With higher priority (
prio 31336
), everything targeted to the local peer's networks will be routed through tablemain
as if there was no tunnel.
The rules on local peer would now be:
ip rule
0: from all lookup local
31336: from all to 192.168.1.0/24 lookup main
31337: from all uidrange 1002-1002 lookup 31337
32766: from all lookup main
32767: from all lookup default
ip -6 rule
0: from all lookup local
31336: from all to fc00::/7 lookup main
31337: from all uidrange 1002-1002 lookup 31337
32766: from all lookup main
Reverse Routing
Computers in the WireGuard server's LAN won't be able to get replies to their packets routed to the local peer at this end of the tunnel unless something is done about it.
Assuming there's a machine called Pinger (192.168.100.2
) in
LAN (192.168.100.0/24
) with the WireGuard server (192.168.100.1
) running
and ICMP echo through the tunnel:
ping 192.168.2.1
- Pinger at
192.168.100.2
sends ICMP echo request to192.168.2.1
. - ICMP echo request reaches the WireGuard server
192.168.100.1
on the Lan. - Wireguard server forwards ICMP echo request through the tunnel to
192.168.2.2
. - local peer
192.168.2.2
receives ICMP echo request with source address192.168.100.2
and sends back ICMP echo reply to192.168.100.2
. - The default
ip rule
listing from above indicates that the ipv4 rule with prio32766: from all lookup main
applies at local peer, and the packet is routed within local peer's LAN, never going back through the tunnel.
To fix this case the source address of those packets needs to be replaced with
the WireGuard server's address, 192.168.2.1
/ fd00:1337::1
. Here's the
MikroTik RouterOS setup:
/ip/firewall/nat chain=srcnat action=src-nat to-address=192.168.2.1 dst-address=192.168.2.2
/ipv6/firewall/nat chain=srcnat action=src-nat to-address=fd00:1337::1/128 dst-address=fd00:1337::2/128
and an untested attept at a Linux iptables
equivalent:
iptables -t nat -A POSTROUTING -d 192.168.2.2 -j SNAT --to 192.168.2.1
ip6tables -t nat -A POSTROUTING -d fd00:1337::2 -j SNAT -- to fd00:1337::1