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.
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 ACCEPTnftables:
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.10instead of-i ens192, and addiptables -t nat -A POSTROUTING -s 10.20.0.0/24 -d 10.20.0.50 -p tcp --dport 443 -j MASQUERADEso 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.
They will ask next
The DNAT counter increases but the connection still times out. What next?
Capture on the inside interface. If SYNs leave toward 10.20.0.50 and no SYN-ACK returns, the server's firewall or default gateway is the problem. If nothing leaves, the FORWARD chain or rp_filter is dropping.
How do you see the translation for a live connection?
conntrack -L -p tcp --dport 8443. The original tuple shows the public destination and port, the reply tuple shows 10.20.0.50 port 443 as the source.
Traps
- Allowing
-d 198.51.100.10 --dport 8443in 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.