Put a service on a laptop behind carrier-grade NAT, point that laptop at a VPS over a VPN, and nobody outside can reach it. You add a destination NAT rule on the VPS and get something worse than silence: connections open, then stall. The reply is not missing, it is leaving by the client's own default route, which means the server never sees it. Work these five checks in order and stop at the first one that does not hold.

Confirm the server will forward at all

Tick this box before writing a single translation. A DNAT rule for a packet the kernel declines to forward is accepted by the rule parser and then dropped by the forward path, which produces a timeout that looks like a firewall problem.

sysctl net.ipv4.ip_forward
sysctl -w net.ipv4.ip_forward=1

Setting it in a running kernel is not the same as setting it across a reboot. Write net.ipv4.ip_forward=1 into a file under /etc/sysctl.d/, run sysctl --system, then read the value back instead of trusting that the file was picked up. A container that manages its own network namespace needs the same value inside that namespace, not just on the host.

If the read-back is still zero, stop here. Nothing downstream will work.

Pin the tunnel address before any rule refers to it

The pool hands out addresses in connection order, so a rule written for today's lease can quietly point at a different client next week. Bind the address to the certificate identity instead of to arrival order.

client-config-dir /etc/openvpn/ccd
ifconfig-push 10.77.0.14 10.77.0.13

That pair is the client end and the peer end of a point-to-point tunnel, with the server keyed on the certificate's common name. Under a subnet topology the second value is read as a netmask instead, so write the form that matches the topology in use. Then reconnect the client and read the address on the client itself, because the server's view of the pool is not proof of what the client configured.

If the address changes after a reconnect, stop. A NAT rule that follows a moving target will fail intermittently and teach you nothing.

Translate in both directions

Destination translation sends the inbound packet to the client. Source translation is what makes the client answer the server rather than the original sender, and it is the rule people leave out.

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 29443 -j DNAT --to-destination 10.77.0.14:29443
iptables -t nat -A POSTROUTING -d 10.77.0.14 -p tcp --dport 29443 -j SNAT --to-source 10.77.0.13

Without the second rule the client answers through its ISP uplink, so the remote peer sees a source address it never spoke to while the server holds no tracking entry for the reply. The outside observer sees a handshake that half-completes, which is exactly the symptom that sends people hunting for a firewall bug that is not there.

Read both rules back with iptables -t nat -S and confirm the interface name is the one carrying the public address. On a host that already filters with nftables, write the translation in an nftables NAT chain of the matching address family instead of mixing tools.

Let the filter table through

The NAT tables rewrite headers; they do not grant permission. With a default DROP policy the forwarded flow needs its own accept, plus the return direction that most policies already cover with an established,related rule.

iptables -A FORWARD -i eth0 -o tun0 -p tcp --dport 29443 -m conntrack --ctstate NEW -j ACCEPT
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Check the policy of the FORWARD chain before adding anything, because the order of an early DROP decides whether your rule is ever consulted. On a host whose filtering lives in nftables, add the equivalent rule to the forward hook of the inet table and confirm that the base chain actually sees forwarded traffic rather than only local input. If a packet counter on the new rule stays at zero while the NAT counters rise, the packet is being dropped before your rule. That is the signal to read the chain order, not to add a second copy of the same rule.

Prove it from an outside network

Test from an unrelated network. A probe from another machine on the same LAN can pass through hairpin NAT support and never exercise the path a remote client uses, so a green result there proves the wrong thing.

tcpdump -ni any port 29443
conntrack -L | grep 29443

Run the capture on the server and on the client at the same time. The server capture shows whether the translated packet leaves toward the tunnel; the client capture shows whether the request arrives and where the reply is addressed. A translation entry in conntrack -L proves the rule matched, and its absence proves the packet never reached the NAT chain.

Port scanners are weak evidence. They report the port as closed until the application is bound and listening, so use a real connection to the real service. When it finally works, keep the exposure to that one port: forwarding SSH or an admin panel because the first rule felt awkward is a much larger decision than publishing a service.

Artitgels cun il medem tema