Firewalls and NAT on Linux

February 2024 · iptables · netfilter · NAT · networking

I have used NAT every day of my life without ever watching it work. So I built three small Linux VMs, put one in the middle as a router, and used iptables to do the address translation by hand, with a packet capture open so I could see the rewrite actually happen.

The topology

Three bare console VMs, each with minimal RAM, wired in a line. The middle VM is the only one with a foot in both subnets, so all traffic between the ends has to pass through it.

VM1 client VM2 router · NAT VM3 web server

Routing through the middle

By default the middle VM will not forward packets between its interfaces. Linux ships with forwarding off, so the first step is to turn it on:

echo 1 > /proc/sys/net/ipv4/ip_forward

With private addresses assigned so each pair of neighbours shares a subnet, the client can now reach the far VM only by going through the router. A traceroute from the client to the web server confirms the path: it shows the middle VM as the hop in between, which is exactly the proof that traffic is being routed rather than delivered directly.

Bidirectional NAT

This was the part I wanted to see. The goal is that when the client reaches the web server, the server sees the connection as coming from the router, and when the reply comes back, the client sees it as coming from the router too. Neither end ever sees the other's real address. That is source NAT on the way out and destination NAT on the way back, configured on the router's nat table:

# rewrite the source of outbound traffic to the router
iptables -t nat -A POSTROUTING -o <out-iface> -j MASQUERADE

# send inbound web traffic on to the real server
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination <vm3-ip>:80

Then I ran tcpdump on all three machines at once. On the web server, the requests appear to originate from the router's address. On the client, the responses appear to come from the router. The middle VM sees both the real and the translated addresses. Seeing the same packet wearing two different source addresses on two different machines is the moment NAT stops being abstract.

Turning the firewall into an ACL

A firewall is really just a network access-control list, so the last step was to make it behave like one. The filter table on the router was set to allow only the web server's ports, 80 and 443, and to drop everything else. With an SSH server running on VM3 as a test, port 22 became unreachable through the router while the web server stayed up, which is the firewall doing its one job: permitting the services you named and refusing the rest.

I then tightened it further so that only the client VM was allowed to reach ports 80 and 443, and traffic from the router itself was denied. Source-based rules like this are how you express "these hosts, to these ports, and nothing else" at the network layer, the same shape of policy I lean on at home to keep services boxed in.

Why bother doing it by hand

Every home router and cloud gateway does this for you, silently. Wiring it up on three throwaway VMs, and watching the source address change between captures, turned NAT and firewalling from things I trusted into things I had actually observed. That is the whole reason I did it.