Build a NAT router with a port forward in iptables, then in nftables
LinuxWorking
Tasks
- Enable IPv4 forwarding persistently.
- Set the FORWARD policy to DROP and allow LAN to internet plus return traffic.
- MASQUERADE the LAN out of ens192.
- DNAT TCP 8443 on the outside to 10.20.0.50:443 and allow it through FORWARD.
- Save the rules so they load at boot.
- Write the same policy as an nftables ruleset.
Solution
1ForwardingR1
R1 on Linux echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/90-router.conf sysctl --system2Stateful FORWARD policyR1
R1 on Linux iptables -P FORWARD DROP iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A FORWARD -m conntrack --ctstate INVALID -j DROP iptables -A FORWARD -i ens224 -o ens192 -s 10.20.0.0/24 -j ACCEPTThe established rule goes first because almost every packet belongs to an existing flow.
3NAT and the port forwardR1
R1 on Linux iptables -t nat -A POSTROUTING -s 10.20.0.0/24 -o ens192 -j MASQUERADE iptables -t nat -A PREROUTING -i ens192 -p tcp --dport 8443 -j DNAT --to-destination 10.20.0.50:443 iptables -A FORWARD -i ens192 -o ens224 -p tcp -d 10.20.0.50 --dport 443 -m conntrack --ctstate NEW -j ACCEPTThe FORWARD rule uses the inside address and port 443, because DNAT already happened in PREROUTING. With a static public address,
-j SNAT --to-source 198.51.100.10does the same job as MASQUERADE.4Save for bootR1
R1 on Linux apt install iptables-persistent netfilter-persistent saveRules are written to
/etc/iptables/rules.v4and restored at boot. Do not do this on a Docker host; see the mistakes.5The same policy in nftablesR1
R1 on Linux #!/usr/sbin/nft -f # /etc/nftables.conf flush ruleset table inet filter { chain forward { type filter hook forward priority filter; policy drop; ct state established,related accept ct state invalid drop iifname "ens224" oifname "ens192" ip saddr 10.20.0.0/24 accept iifname "ens192" oifname "ens224" ip daddr 10.20.0.50 tcp dport 443 ct state new accept } } 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 } chain postrouting { type nat hook postrouting priority srcnat; policy accept; oifname "ens192" ip saddr 10.20.0.0/24 masquerade } }Load with
nft -f /etc/nftables.confand enablenftables.service. Use either this file or the iptables rules, not both. The NAT table is familyipso thednat toaddress needs no family keyword.
Verify
R1# iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-A PREROUTING -i ens192 -p tcp -m tcp --dport 8443 -j DNAT --to-destination 10.20.0.50:443
-A POSTROUTING -s 10.20.0.0/24 -o ens192 -j MASQUERADEOne DNAT rule in PREROUTING and one MASQUERADE in POSTROUTING. iptables prints the implicit -m tcp match.
R1# iptables -S FORWARD
-P FORWARD DROP
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -m conntrack --ctstate INVALID -j DROP
-A FORWARD -s 10.20.0.0/24 -i ens224 -o ens192 -j ACCEPT
-A FORWARD -d 10.20.0.50/32 -i ens192 -o ens224 -p tcp -m tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPTPolicy DROP, the established rule first, and the port-forward rule matching the inside address and port.
R1# conntrack -L -p tcp
tcp 6 431996 ESTABLISHED src=10.20.0.15 dst=203.0.113.80 sport=40112 dport=443 src=203.0.113.80 dst=198.51.100.10 sport=443 dport=40112 [ASSURED] mark=0 use=1
tcp 6 431999 ESTABLISHED src=192.0.2.77 dst=198.51.100.10 sport=50612 dport=8443 src=10.20.0.50 dst=192.0.2.77 sport=443 dport=50612 [ASSURED] mark=0 use=1
conntrack v1.4.7 (conntrack-tools): 2 flow entries have been shown.First line: PC1's web session, source NATed (the reply goes to 198.51.100.10). Second line: the outside client hitting 8443, DNATed (the reply comes from 10.20.0.50 port 443).
R1# nft list chain ip nat postrouting
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "ens192" ip saddr 10.20.0.0/24 masquerade
}
}After loading the nftables file instead, the NAT rule is listed in its own table and chain.
Mistakes people make
- Writing the port-forward FORWARD rule with the public address or port 8443. FORWARD sees 10.20.0.50 and 443.
- Forgetting that SRV1's default gateway must be R1. Otherwise its replies bypass the NAT and the client drops them.
- Testing the port forward from PC1 on the LAN. Without hairpin NAT that fails even when the config is right.
- Setting
iptables -P FORWARD DROPbefore the allow rules on a router people are using. - Running
netfilter-persistent saveor an nftables file withflush ruleseton a host where Docker also manages rules.
Say it in the interview
"Three pieces: forwarding on, a stateful FORWARD chain with default drop, and NAT. MASQUERADE in POSTROUTING for the LAN, DNAT in PREROUTING for the published port, and a FORWARD rule written for the inside address, because DNAT happens before filtering. I check it with conntrack, where both translations are visible in the reply tuples, and I keep it in one nftables file so it loads atomically."