Routing is an option if you have a whole IP address to spare. Remember that the VPS still needs an address for itself, to act as the tunnel endpoint (the outer address) – and generally also for SSH'ing to the VPS when the tunnel isn't working yet.
So if the VPS has two addresses – either two IPv4, or perhaps it has IPv6 and you also have IPv6 at home – then yes, one address can be deassigned from the VPS and routed through the tunnel, allowing it to be directly assigned to the home server.
But if the VPS only has a single address in total, then no, that prevents you from forwarding "All" traffic because some of it (like the outer tunnel traffic!) necessarily has to go to the VPS – and therefore you have to use DNAT.
Routing the VPS's external address somewhat depends on the provider. Either way you start by deassigning the address from the VPS's eth0, but in many cases you'll have to turn on Proxy-ARP on the VPS instead, as the provider's network will expect that address to be "on link" i.e. within the local Ethernet.
tcpdump -e -n -i eth0 "arp and host 1.2.3.4" could be used to determine whether the datacenter treats a given address as on-link or not. If you see the gateway making ARP queries for an address, then it's on-link, and more straightforwardly, something has to answer those ARP queries, so if you unassign the address from eth0 then proxy-ARP is required.
If a VM has multiple IPv4 addresses, sometimes they're all on-link and all require ARP (proxy or regular), but sometimes only the "primary" address is on-link while the extra ones are routed. Depends on the provider.
Fine-grained Proxy-ARP is ip neigh add IPADDR dev eth0 proxy. There is also a sysctl to enable catch-all Proxy-ARP which will answer on behalf of any address that is routed through a different interface. Either of the two methods is enough. I'd prefer manual neighbor entries instead of coupling it to routing.
Then you add a route for that address through the tunnel, using some equivalent of ip route add XXX/32. The VPN/tunnel software may also need internal routes, e.g. WireGuard needs AllowedIPs to be updated to include that address, OpenVPN (tun mode) needs iroute. Ethernet-emulating VPNs like ZeroTier or point-to-point tunnels like GRE need nothing special as the initial ip route add provides all necessary information.
The basic route is ip route add IPADDR/32 dev wg0 [via GATEWAY].
For Ethernet-emulating VPNs such as ZeroTier or OpenVPN-tap, via GATEWAY has to be specified, with the home server's VPN IP as the "gateway". (For exactly the same kind of ARP reasons.)
For other VPN types (GRE, WireGuard, OpenVPN-tun), via is meaningless; either they only have one destination (like GRE) or they have internal routing (like WG's AllowedIPs).
WireGuard and OpenVPN-tun are multipoint VPNs but don't have MAC addresses, so they need to be told about the route as well. For WireGuard, that means adding the address/32 to the peer's AllowedIPs=.
Use tcpdump on the client (home server) to verify that it's receiving the packets for the address.
tcpdump -n -i wg0, optionally with a filter like "host 1.2.3.4".
If it is, then assign the address to the VPN interface (ip addr add XXX/32 dev wg0 or similar).
If the VPN software makes it difficult to assign several addresses to its interface, you can assign the address to a different interface instead. Sometimes lo is used for such /32's (and such "roaming" addresses are often called "loopback addresses"), or a dedicated dummy0 interface can be created. The address could even be assigned to the home server's eth0, though that makes little sense logically, but it'll work.
The "home" tunnel endpoint doesn't have to be the server directly. Once packets reach you, you can route the address even further.
As you can see, although it's simpler in the sense that there's no packet mangling involved – just pure forwarding – at the same time it's actually more complex in the setup needed.
For DNAT, the corresponding nftables chain could look like:
chain dstnat {
type nat hook prerouting priority dstnat;
ip daddr $vps_ipv4 jump {
# Don't DNAT certain communications to the VPS itself
meta l4proto icmp return
tcp dport 22 return
udp dport <tunnel_port> return
# DNAT everything else (which is in practice just TCP and UDP)
meta l4proto {tcp, udp} dnat to ...
# Blanket 'dnat to' could be done, if you end up needing other protocols
#dnat to ...
}
}
SNAT can be interface-based. SNAT rules are only for 'new' traffic.
chain srcnat {
type nat hook postrouting priority srcnat;
iifname "tun*" oifname "eth0" masquerade
}
Edit: I forgot an oifname check in the SNAT rule. Might not affect anything, but as a general rule, only packets that actually go out to the WAN should be SNATed. (Who knows, maybe you'll end up with two VPN interfaces or even a container bridge on the VPS, and you shouldn't SNAT that.)
The final step, [policy] routing to make sure the home server's outbound traffic goes through the VPN, is exactly the same whether you use plain routing or whether you use DNAT, so there are other threads explaining it.
In either case, a plain default route via the tunnel interface would suffice if the home server is to exclusively access the outside world through the tunnel, but policy routing would be needed if the home server may receive connections both through the tunnel and directly through your home IP address (as then its outbound traffic needs to go appropriately as well).
With this solution, does the container see the public IP address on inbound packets?
The source IP address of inbound packets stays exactly what it was. You don't have any inbound SNAT rules that would change it.
SNAT of inbound traffic is usually added only as a hack to deal with outbound routing – it's the remaining option when your home server can neither use a 0.0.0.0/0 route for reasons, nor supports policy routing.
If I wanted to terminate the WireGuard tunnel on my router (OpenWrt) instead, how would I change the recipe?
Preferably:
Set up routing so that your VPS is aware of 192.168.22.0/24 – again, add to AllowedIPs, route through wg0.
(wg-quick will indeed set up kernel routes to match AllowedIPs, but systemd-networkd won't, so if you're using networkd or netplan, then both AllowedIPs and a route have to be defined.)
Test by making sure the VPS can reach the container, e.g. by ping 192.168.22.25 or curl.
Change the VPS's DNAT rule to dnat to 192.168.22.25. Once nftables DNAT has rewritten the packet's destination in prerouting, the packet will just be routed all the way towards the container, even if it takes more than 1 hop.
I would make sure to disable SNAT in OpenWrt for the tunnel interface. There's no need for it anymore when the VPS has routes to the real addresses.
It's by virtue of this mechanism, that packets leaving 192.168.22.25 can appear as packets addressed from 80.70.60.50 to the outside world?
Yes, that's what snat or masquerade do.
(Both do the same, but 'masquerade' just automatically takes the new address from the output interface.)
Speaking of which: see yesterday's edit. This generally ought to additionally be restricted to oifname "eth0" or similar.
iifname wg0 oifname eth0 snat to 80.70.60.50
Create a routing rule that sends all traffic from 192.168.22.25 to the wg0 interface
Regular routes cannot handle the "from" condition. (Well, on Linux, IPv6 routes can – but not IPv4 ones.)
So you'll need to use policy routing: an additional layer above regular routing. Policy rules choose between whole routing tables.
WireGuard's wg-quick configuration has a Table = parameter. If you set it to a number (e.g. table 7), wg-quick will insert routes to that table instead of the main table; see e.g. ip route ls table 7 vs ip route ls table main.
Then find OpenWrt's "Policy routing" (the GUI equivalent of ip rule) and define the rules in this order:
- from: any, to: 10.0.0.0/8, lookup table: "main"
- from: any, to: 192.168.0.0/16, lookup table: "main"
- from: 192.168.22.25, to: any, lookup table: 7
Below them you have the built-in rule "lookup table: main" that handles all remaining traffic.
(what happens to DNS queries)
It depends on whether the server directly talks to some external DNS server(s) or whether it talks to OpenWrt's cache/proxy (which I think is Dnsmasq?). The former will follow regular routing and/or NAT, as they're just ordinary UDP packets. The latter depends on how you configure OpenWrt's dnsmasq.
Is this on the VPS? How should I define that route? The VPS appears to be using netplan and gets it's public IP via DHCP
How the external interface is configured is irrelevant. The point was about how the WireGuard tunnel is created (via wg-quick vs via netplan/systemd-networkd .netdev).