Address translation
Port forward
One port on the router's public address passed to one device and port inside.
The router rewrites the destination on the way in (DNAT) and the source of the replies on the way out. It is separate from the firewall: a forward without a rule allowing it is dropped after the rewrite. The inside device still sees the real client's address.
How port forwarding works, step by step
A home router has the outside address 10.0.2.2 and the inside address 10.0.1.2. A NAS inside at
10.0.1.10 runs SSH on port 22. The router forwards its own port 2222 to it. Follow an outsider at 10.0.2.10:
- The outsider connects to the router. Its SYN is
10.0.2.10:43295 → 10.0.2.2:2222. It never learns the NAS's address. - DNAT before routing. In the nat table's PREROUTING chain the first packet of the connection matches the
forward, and its destination is rewritten to
10.0.1.10:22. Conntrack records the translation, so the rest of the connection is rewritten without consulting the rules again. - Routing, then the firewall. With the new destination, the routing table sends the packet out of the inside card, and the FORWARD chain decides whether it may pass. A forward is only an address rewrite; the permission is a separate rule.
- The NAS answers the real client. The source was never changed, so the NAS sees
10.0.2.10and replies to it, through its default gateway, the router. - The reply is translated back. Conntrack matches it and rewrites the source from
10.0.1.10:22to10.0.2.2:2222, so the outsider gets an answer from the address it called.
A forward can keep the port (80 to 80) or change it (2222 to 22), and covers TCP or UDP. Routers that expose it as "virtual servers" or UPnP do the same rewrite. With carrier-grade NAT upstream, the router's outside address is itself private, and a forward on the home router cannot be reached from the internet at all.
What it looks like on a Linux machine
In the port forwarding and NAT lab the home router's forward is one iptables
rule (kept in its own NETLAB-PREROUTING chain, called from PREROUTING), and a second rule lets it through the
firewall. --dst-type LOCAL limits the forward to packets addressed to the router itself, so traffic passing
through keeps its destination:
iptables -t nat -A PREROUTING -m addrtype --dst-type LOCAL -p tcp --dport 2222 -j DNAT --to-destination 10.0.1.10:22
iptables -A FORWARD -d 10.0.1.10 -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
The rule's counters and the connection table show it working:
$ iptables -t nat -L PREROUTING -nv
pkts bytes target prot opt in out source destination
1 60 DNAT 6 -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL tcp dpt:2222 to:10.0.1.10:22
$ conntrack -L -p tcp --dport 2222
tcp 6 119 TIME_WAIT src=10.0.2.10 dst=10.0.2.2 sport=43295 dport=2222 src=10.0.1.10 dst=10.0.2.10 sport=22 dport=43295 [ASSURED] mark=0 use=1
The second half of the entry is the reply as expected: from the NAS, to the outsider's real address. That is why the
NAS's SSH log reads Accepted password for admin from 10.0.2.10. In nftables the same forward is
ip daddr 10.0.2.2 tcp dport 2222 dnat to 10.0.1.10:22 in a type nat hook prerouting chain.
What goes wrong, and how you see it
Forwarded but not allowed. The lab forwards 8080 to the NAS's web page but has no FORWARD rule for port 80.
The destination is rewritten, the packet is dropped, and curl just times out. iptables -t nat -L -nv counts the
DNAT; the FORWARD chain's final DROP counts the loss.
Connection refused instead of a timeout. The DNAT rule is missing, so the connection is for the router itself,
which has nothing on 2222 and answers with a reset: ssh: connect to host 10.0.2.2 port 2222: Connection refused. iptables -t nat -S shows which forwards exist.
It works from outside but not from inside (hairpin NAT). The laptop at 10.0.1.11 connects to
10.0.2.2:2222. The router rewrites the destination to the NAS, but the NAS sees a source on its own network and
answers directly, bypassing the router. The laptop gets a SYN-ACK from an address it never called and resets it:
10.0.1.11.41907 > 10.0.2.2.2222: Flags [S], ...
10.0.1.10.22 > 10.0.1.11.41907: Flags [S.], ...
10.0.1.11.41907 > 10.0.1.10.22: Flags [R], ...
The fix is to also rewrite the source for inside-to-inside forwarded traffic, so replies return through the router:
iptables -t nat -A POSTROUTING -s 10.0.1.0/24 -d 10.0.1.10 -p tcp --dport 22 -j MASQUERADE. Split DNS, giving
inside clients the inside address, avoids the detour.
The inside server uses another gateway. Its replies leave untranslated by another router and the client drops them. Its default route must point back through the forwarding router.
Questions people ask
Is port forwarding the same as DNAT? Yes: a port forward is destination NAT for one port. Source NAT for outgoing traffic is the other half of a home router.
Is port forwarding safe? It exposes one service to the whole internet, so that service must be patched and authenticated, and its logs watched. A DMZ network or an SSH bastion limits what an attacker reaches if it is broken.
Port forwarding vs SSH port forwarding? A router's forward rewrites packets in the kernel. ssh -L and ssh -R
carry a port through an encrypted SSH connection between two programs.