Octet

How do you forward a port on a Linux router to an internal server, and what usually goes wrong?

Show the answer

Say this first

Three pieces: a DNAT rule in nat PREROUTING that rewrites the destination, a FORWARD rule that allows the new inside address and port, and a server whose return path goes back through the router. What goes wrong: the FORWARD rule matching the public address, the server's default gateway pointing elsewhere, and inside users who try the public address and hit the hairpin problem.

Go deeper

Setup

R1: outside ens192 198.51.100.10, inside ens224 10.20.0.1/24. Forward TCP 8443 to 10.20.0.50:443.

Shell
iptables -t nat -A PREROUTING -i ens192 -p tcp --dport 8443 -j DNAT --to-destination 10.20.0.50:443
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -i ens192 -o ens224 -p tcp -d 10.20.0.50 --dport 443 -m conntrack --ctstate NEW -j ACCEPT

nftables:

Output
table ip nat {
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;
        iifname "ens192" tcp dport 8443 dnat to 10.20.0.50:443
    }
}

Hairpin NAT

An inside client 10.20.0.15 opens 198.51.100.10:8443. The rule above has -i ens192, so nothing happens. Even with the DNAT applied on the inside interface, the server replies straight to 10.20.0.15 on the same subnet, from 10.20.0.50, and the client drops it because it expected 198.51.100.10. Fixes:

  • Match -d 198.51.100.10 instead of -i ens192, and add iptables -t nat -A POSTROUTING -s 10.20.0.0/24 -d 10.20.0.50 -p tcp --dport 443 -j MASQUERADE so the server replies to the router.
  • Or better, split DNS: inside clients resolve the name to 10.20.0.50 directly.

Traffic from the router itself

Locally generated packets skip PREROUTING. To redirect them, add the DNAT in nat OUTPUT as well.

Traps

  • Allowing -d 198.51.100.10 --dport 8443 in FORWARD. After DNAT, FORWARD sees 10.20.0.50 and port 443.
  • Forgetting that the server must route its replies through the box doing the DNAT.
How did you do?