This question is actually to solve a problem about UDP (WireGuard's tunnel envelope).
When not statingforwarding done with a source address, the routing stack has to figure out which one should be used for the given route.
Here, WireGuard uses INADDR_ANY. In particular it doesn't bind to 198.51.100.3. That means it presents as source 0.0.0.0dnat rule and leaves to the kernel's routing stack the resolution of the outgoing source IP address.
If server's WireGuard had been initiating the very first packet (ratherrather than peer doing it), it would have used 203.0.113.134 instead of 198.51.100.3: the routing stack has no specific ip rule for 0.0.0.0: the ip rule 32765: from 198.51.100.0/24 lookup subnets doesn't match and no special policy routing is applied. In the end, the UDP packet leaves as 203.0.113.134 using ens18.
It appears the kernel implementation at least then continues to use the same address it was queried onjust local traffic. That's not to be relied upon, multi-homingThere's also a hidden problem with UDP services requires special support from applications because of this. This is not an issue for TCP, as the duplicated established socket created after accept(2) is not bound to 0.0.0.0 anymore but to the correct address: it will then present this address to the routing stack.
Sought outcome for WireGuard:
# ip route get from 198.51.100.105 to 192.0.2.233
192.0.2.233 from 198.51.100.105 via 198.51.100.1 dev ens19 table subnets uid 0
cache
Actual outcome tunnel envelope also fixed at least if it's the first to initiate traffic:end.
# ip route get from 0.0.0.0 to 192.0.2.233
192.0.2.233 via 203.0.113.1 dev ens18 src 203.0.113.134 uid 0
cache
- not having enabled Strict Reverse Path Forwarding (RFC 3704)
- having the peer contact the server first (see the additional issue at the end)
- having (at least) the kernel implementation figure out it should reply with the same source it was initially contacted to
Ping testsThe behavior of the routing stack with the ping test (including the actual dnat that happened in prerouting and the un-masquerade that will happen for the reply) can be summarized with these two commands that query the kernel about what route will be used:
(this This also fixes the case wherewith rp_filter=1rp_filter=1 where the first route test above would just fail with RTNETLINK answers: Invalid cross-device link), even if normally one should add the wg0 route in this table too.
# ip route get from 10.8.0.105 iif wg0 to 192.0.2.2
192.0.2.2 from 10.8.0.105 via 198.51.100.1 dev ens19 table subnets
cache iif wg0
The ping test will now work correctly.
There's an additional somewhat hidden WireGuard envelope routing problem to.
When not stating a source address for a local (non-routed) flow, the routing stack has to figure out which one should be used for the given route. This is especially important for UDP where a socket is often kept unbound (ie: having source 0.0.0.0 aka INADDR_ANY). This is not an issue for a TCP server, as the duplicated established socket created after accept(2) is not bound to 0.0.0.0 anymore but to the correct address: it will then present this address to the routing stack. Here, WireGuard uses UDP with INADDR_ANY. In particular it doesn't bind to 198.51.100.3. That means it presents as source 0.0.0.0 and leaves to the kernel's routing stack the resolution of the outgoing source IP address.
If server's WireGuard had been initiating the very first packet (rather than peer doing it), it would have used 203.0.113.134 instead of 198.51.100.3: the routing stack has no specific ip rule for 0.0.0.0: the ip rule 32765: from 198.51.100.0/24 lookup subnets doesn't match and no special policy routing is applied. In the end, the UDP packet leaves as 203.0.113.134 using ens18.
It appears the kernel implementation at least then continues to use the same address it was queried on. That's not to be relied upon, multi-homing with UDP services requires special support (eg: using IP_PKTINFO) from applications because of this.
Sought outcome for WireGuard:
# ip route get from 198.51.100.105 to 192.0.2.233
192.0.2.233 from 198.51.100.105 via 198.51.100.1 dev ens19 table subnets uid 0
cache
Actual outcome at least if it's the first to initiate traffic:
# ip route get from 0.0.0.0 to 192.0.2.233
192.0.2.233 via 203.0.113.1 dev ens18 src 203.0.113.134 uid 0
cache