4

The goal:

  1. Use the VPS's public IPv4 address for its reputation (my ISP allocated a static IPv4 that doesn't have a good reputation) and forward ALL traffic to a container (Debian 12) on my home network.
  2. Make the container route ALL outbound traffic via the VPS (also Debian 12).

Steps so far:

  • I have set net.ipv4.ip_forward = 1 in sysctl.
  • I have a WireGuard tunnel between the VPS and the container on subnet 10.10.10.0/24.
  • I have opened some ports for testing on my VPS provider's hardware firewall that sits in front of the VPS.
  • I have installed nft on both VPS and container, but am wondering if I can accomplish what I want without a software firewall.

Questions:

  1. Do I even need to use a software firewall/NAT?
  2. Can I just use a routing table at both ends?

I would like to keep this as simple as possible, so if I can avoid (S|D)NAT entirely, then I would prefer that.

Summary:

  • All traffic incoming to the VPS's public IP is forwarded to the container.
  • All traffic outgoing from the container reaches the internet via the VPS.

Can someone outline the easiest way of accomplishing the goal?


Part Two

Thank you grawity for that thorough answer; I can see the pitfalls of trying to route ALL, unless I have a spare IP.

So to elaborate the NAT solution, may may I check my workings with some examples?

On VPS

  • wg0 interface = 10.10.10.1/24
  • eth0 interface = e.g. 80.70.60.50
  • AllowedIPs = 10.10.10.10/32
chain dstnat {
    type nat hook prerouting priority dstnat;
ip daddr 80.70.60.50 jump {

    # Don't DNAT certain communications to the VPS itself
    meta l4proto icmp return
    tcp dport 22 return
    udp dport 51820 return

    # DNAT everything else (which is in practice just TCP and UDP)
    meta l4proto {tcp, udp} dnat to 10.10.10.10

    # Blanket 'dnat to' could be done, if you end up needing other protocols
    #dnat to ...
}

}

chain srcnat { type nat hook postrouting priority srcnat;

iifname "wg0" masquerade

}

On Container at Home

  • wg0 interface = 10.10.10.10/24
  • AllowedIPs = 0.0.0.0/0 (No other steps are needed as this tells the container to route everything through wg0?)

With this solution, does the container see the public IP address on inbound packets?

If I wanted to terminate the WireGuard tunnel on my router (OpenWrt) instead, how would I change the recipe?

  • OpenWrt wg0 interface 10.10.10.2/24
  • Container eth0 interface 192.168.22.25

Would I have to NAT again? How would I make sure the container gets the VPS' IP address in both cases?


Part Three

So it would be equally valid to have the postrouting chain like this:

chain srcnat {
    type nat hook postrouting priority srcnat;
iifname "wg0" snat 80.70.60.50

}

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?

Extending this to the OpenWrt intermediary results in the follwing:

On VPS

  • wg0 = 10.10.10.1/24
  • eth0 = e.g. 80.70.60.50
  • AllowedIPs = 10.10.10.2/32, 192.168.22.25/32
chain dstnat {
    type nat hook prerouting priority dstnat;
ip daddr 80.70.60.50 jump {

    # Don't DNAT certain communications to the VPS itself
    meta l4proto icmp return
    tcp dport 22 return
    udp dport 51820 return

    # DNAT everything else (which is in practice just TCP and UDP)
    meta l4proto {tcp, udp} dnat to 10.10.10.2

    # Blanket 'dnat to' could be done, if you end up needing other protocols
    #dnat to ...
}

}

chain srcnat { type nat hook postrouting priority srcnat;

iifname "wg0" snat 80.70.60.50

}

On OpenWrt

  • wg0 = 10.10.10.2/24
  • br-lan.22 = 192.168.22.0/24
  • AllowedIPs = 0.0.0.0/0

Create a routing rule that sends all traffic from 192.168.22.25 to the wg0 interface (what happens to DNS queries)

On Container

Do nothing?


(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.)

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

Giacomo1968
  • 58,727
Stephen
  • 41

1 Answers1

5

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:

  1. 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.

  2. 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.

  3. 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).

grawity
  • 501,077