Yesterday a friend ask me if it is possible to route the outgoing traffic of a Linux machine with a specific destination port via a wireguard VPN. He used a publicly accessible proxy host to forward SMTP (port TCP/25) via this VPN to a machine in his home network and now he wanted the mailserver to push all the outgoing mail also though this VPN connection to the proxy. However there should no mail relay be involved.

My idea was to mark all outgoing traffic with a IPtables rule and use a separate routing table to send it though the VPN instead of the default gateway of the home network (policy based routing). After some research I found out that it is actually possible to do this with NFtables based systems. Although the initial routing decision for locally generated packets is done in Linux before one is able to alter them via firewall rules, there is a possibility to change their route afterwards.

As the following flowchart shows, there is a “reroute check” after the OUTPUT chains of all tables and after that you can still use the POSTROUTING chains.

Netfilter Packet Flow image, published on Wikipedia, CC BY-SA 3.01

That means we first use the following firewall rules to mark all the outgoing SMTP traffic. The number in the firewall mark is arbitrarily chosen.

iptables -t mangle -I PREROUTING 1 -j CONNMARK --restore-mark
iptables -t mangle -I OUTPUT 1 -p tcp --dport 25 -j MARK --set-mark 0x25
iptables -t mangle -I OUTPUT 2 -j CONNMARK --save-mark
ip6tables -t mangle -I PREROUTING 1 -j CONNMARK --restore-mark
ip6tables -t mangle -I OUTPUT 1 -p tcp --dport 25 -j MARK --set-mark 0x25
ip6tables -t mangle -I OUTPUT 2 -j CONNMARK --save-mark

Then we add a new routing table and add a single default route via the VPN to it; this work because wireguard interfaces are layer 3 only so there is no need for a gateway. The table number is also arbitrarily chosen.

ip route add default dev wg0 table 25
ip -6 route add default dev wg0 table 25
# optional: naming the routing table
echo "25  smtp" > /etc/iproute2/rt_tables

Now we force the marked traffic to be routed with that newly created table with a IP rule. This all happens during the “routing recheck” after the traffic has been marked.

ip rule add from all fwmark 0x25 lookup 25
ip -6 rule add from all fwmark 0x25 lookup 25

The packet would now be sent via the wireguard interface but will have the source address that was selected in the first routing decision. There we need to change the packet again in the POSTROUTING tables and “masquerade” them with the correct source address. This isn’t a NAT event yet, since the packet has been created locally and is sent on this interface for the first time.

iptables -A POSTROUTING -m mark --mark 0x25 -j MASQUERADE
ip6tables -A POSTROUTING -m mark --mark 0x25 -j MASQUERADE

Now we can test if a TCP connection to port 25 is actually going via the VPN:

tcpdump -nni wg0 port 25 &
nc -vz4 gmail-smtp-in.l.google.com. 25
nc -vz6 gmail-smtp-in.l.google.com. 25

If you see at least a SYN packet on the VPN interface, then it works. Although the reverse path might not yet work. To allow the machine to receive answers for this connection you must first set the reverse path filtering in Linux to loose (2) or turn it off completely (0). See also the parameter doc on sysctl-explorer.net or kernel.org

sysctl -w net.ipv4.conf.wg0.rp_filter=2
# or
sysctl -w net.ipv4.conf.all.rp_filter=0

Also if the VPN interface does not have a public address a forwarding and NAT must be configured on the proxy host.