Firewalls and NAT on Linux
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.
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.