I started this from the outside, the way a user would: typed the name into a phone browser on mobile data and watched the connection hang until it timed out. The name resolved, so DNS was fine. The home router's port-forward page was a dead end too, because its WAN address read 100.64.31.42, which is not an address an ISP will route to me from the internet. That number explains the whole failure: the line sits behind carrier-grade NAT, so the public address on my side of the tunnel belongs to the ISP. What follows is the path I walked from that hung tab to a service that answers.

Start at the client and work inward

Before changing any rule, prove where the chain stops. On the laptop I ran dig +short files.example.net and got the VPS address back, which ruled out stale DNS on my machine. Then curl -v --connect-timeout 5 files.example.net:5022 timed out instead of being refused. That difference matters: a timeout usually means packets are being dropped on the way, while a refusal means something answered and said no.

Next hop in was the VPS itself. ss -ltnp showed nothing listening on 5022, which was expected before the DNAT rule existed. On the same box wg show (or ip -br a show tun0 for OpenVPN) confirmed the home peer was up with a recent handshake. If the peer is down, no NAT rule will help, and the green light on the home router's VPN page means nothing.

Confirm the home line is really behind CGNAT

Compare the address the router reports on its WAN interface with what an outside service sees. Anything in 100.64.0.0/10, meaning 100.64.x.x through 100.127.x.x, is the shared address range from RFC 6598 and cannot be reached from the internet. You get the same conclusion when the router claims an ordinary private address and a public lookup returns something unrelated. A CGNAT line can also come with a public address that only lasts until the next lease, so a port forward appears to work for an hour and then quietly dies.

Some ISPs do hand out a real public address but block inbound 80 and 443 on residential accounts. If your service is web-only and the WAN address is genuinely public, ask support before building a relay. If it is not, the design below applies to every port equally, and it is worth stopping the router-side experiment: the translator in the ISP network terminates the connection before it ever reaches your WAN interface.

Point the name at the VPS address

DNS does not relay traffic, so the A record for the service must hold the VPS's public address. A record pointing at 192.168.1.20 resolves usefully only for clients already inside the VPN; from the internet it resolves to an address nobody can route. I keep a separate name for the VPS's own admin access so I never confuse which host a record refers to.

If both the VPS and the home box serve HTTP, give each its own name and let the VPS decide which one answers. Folding them into one name and hoping the proxy picks the right backend is how you end up debugging the wrong host for an afternoon.

DNAT one port, leave SSH on 22

The direct option is destination NAT on the VPS. Enable forwarding for the running kernel and keep the home server's tunnel address in the rule, not its LAN address:

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A PREROUTING -p tcp -d 198.51.100.10 --dport 5022 -j DNAT --to-destination 10.8.0.2

Keep SSH to the VPS itself on 22 and forward a different port, such as 5022, to the home machine. Forward 22 to a box that is down and you lose the only path that could have fixed it. The same logic applies to the proxy: leave the VPS's management port outside the relay rules.

Persist the rules once they work. On Debian or Ubuntu, apt install iptables-persistent then netfilter-persistent save writes them to /etc/iptables/rules.v4. On a firewalld host the equivalent is a permanent forward-port on the zone, which needs firewall-cmd --reload before it applies. Rules that live only in memory disappear at the first provider maintenance reboot, usually at a bad hour.

Fix the return path, or avoid needing it

DNAT rewrites the destination only. The home server sees a connection from the original client address, and if its default route points at the home router, the reply leaves through the house WAN where the ISP translator drops it. That asymmetry is the second failure I hit, after the DNAT rule looked completely correct.

Two fixes work. Give the home box a policy route so traffic from the tunnel goes back through the tunnel:

ip rule add from 10.8.0.2 table 100
ip route add default dev wg0 table 100

Or let the VPS rewrite the source so the reply has an address it can reach, and let conntrack reverse it on the way out:

iptables -t nat -A POSTROUTING -d 10.8.0.2 -p tcp --dport 5022 -j SNAT --to-source 10.8.0.1

The SNAT version hides the real client address from the home server, so application logs show 10.8.0.1 as the peer. Pass the original address at the application layer, or accept losing per-client logging, but decide deliberately rather than discovering it during an incident.

Check the whole path from outside

Verify from a network that is neither the VPS nor the house: a phone on mobile data, or a cheap shell somewhere else. nc -vz files.example.net 5022 should report a successful connection, and openssl s_client -connect files.example.net:443 -servername files.example.net should show the certificate for the public name. Then check the provider's security group or cloud firewall. A DNAT rule that never sees a packet is usually blocked one layer above the host.

Last, decide what happens when the tunnel drops. A DNAT rule keeps accepting connections and silently feeds them into a dead tunnel, which looks exactly like a broken service. A health check on the VPS that rejects the port while the peer is down costs a few lines and saves an hour of guessing. After that, stop editing rules and watch it for a day before you trust it.

Winndannde jowitiiɗe heen