Home › The Stash

NAT and firewall lab: build a home router, port-forward, then lock it down

LabOctober 27, 2026

IntermediateTestedTime ~35 minTool nftablesNeeds Lab 00Cost freeExam N+ 1, 2, 4 · Sec+ 3, 4

0 / 10 steps

The goal

Build a home router inside one Linux machine: a PC on a private network, a router, and a server standing in for the internet. You will watch private addresses fail to get replies, switch on NAT and see the source address change on the wire, forward a port to a web server on the PC, prove that NAT on its own is not a firewall, and then add the stateful firewall that fixes it.

Everything here lives inside your own Linux machine. The three boxes are network namespaces; the “internet” is the documentation range 198.51.100.0/24, reserved for examples like this one. The nftables rules live only inside the router namespace, so your own firewall is never touched. Step 10 removes all of it.

Why this lab. Every home router does NAT, and most people believe it is what keeps them safe. It does help, but mostly by accident. Seeing the translation table and then walking straight past NAT in step 7 makes the difference between “NAT” and “firewall” obvious, and it is a favourite exam and interview question.

NAT is a receptionist who forwards your mail. Lovely. Not a bouncer. — Nyx

Before you start. Use an Ubuntu machine you control: the VM from Lab 00 or Ubuntu in WSL2. Install the tools with sudo apt install nftables conntrack tcpdump curl, then open a root shell with sudo -i (prompt #). The routing lab explains the “missing return route” idea step 2 relies on.

Steps

01Build the LAN, the router and the “internet”

lan is your PC on 192.168.1.0/24 (a private range). rtr has one leg on the LAN and one on the “internet”. inet is a server out there.

# for n in lan rtr inet; do ip netns add $n; ip -n $n link set lo up; done
# ip link add pc-lan netns lan type veth peer name rtr-lan netns rtr
# ip link add rtr-wan netns rtr type veth peer name inet-wan netns inet
# ip -n lan link set pc-lan up; ip -n rtr link set rtr-lan up
# ip -n rtr link set rtr-wan up; ip -n inet link set inet-wan up
# ip -n lan addr add 192.168.1.10/24 dev pc-lan
# ip -n rtr addr add 192.168.1.1/24 dev rtr-lan
# ip -n rtr addr add 198.51.100.1/24 dev rtr-wan
# ip -n inet addr add 198.51.100.80/24 dev inet-wan
# ip -n lan route add default via 192.168.1.1
# ip netns exec rtr sysctl -w net.ipv4.ip_forward=1

The router’s “public” address is 198.51.100.1, like the single address your internet provider gives your home.

02Try without NAT

Capture on the server while the PC pings it.

# ip netns exec inet tcpdump -n -l -i inet-wan -c 2 icmp &
# ip netns exec lan ping -c 2 -W 1 198.51.100.80

Our capture showed the requests arriving from the private address, IP 192.168.1.10 > 198.51.100.80: ICMP echo request, and the ping reported 100% packet loss.

Why. The server has no idea how to reach 192.168.1.10, so its replies go nowhere. On the real internet the same thing happens on purpose: the private ranges (10/8, 172.16/12, 192.168/16, RFC 1918) are never routed across the public internet, so thousands of homes can all use 192.168.1.10 at once.

03Switch on NAT (masquerade)

One nftables rule on the router: anything leaving through the internet-side interface gets the router’s own address as its source.

# ip netns exec rtr nft add table ip nat
# ip netns exec rtr nft 'add chain ip nat postrouting { type nat hook postrouting priority srcnat; policy accept; }'
# ip netns exec rtr nft add rule ip nat postrouting oifname "rtr-wan" masquerade
# ip netns exec rtr nft list table ip nat

The listing should show a postrouting chain containing oifname "rtr-wan" masquerade.

Why “postrouting”? Source NAT happens as the packet leaves, after the router has already decided where it goes. Masquerade is source NAT that automatically uses whatever address the outgoing interface has, which suits home connections whose public address changes.

04Watch the source address change

# ip netns exec inet tcpdump -n -l -i inet-wan -c 2 icmp &
# ip netns exec lan ping -c 2 -W 1 198.51.100.80

Now the ping works (0% packet loss) and the server sees the router, not the PC:

IP 198.51.100.1 > 198.51.100.80: ICMP echo request
IP 198.51.100.80 > 198.51.100.1: ICMP echo reply

Why this matters. This is why websites see your home’s public address, never your laptop’s 192.168 address. It is also why an investigation that starts from a public IP in a log usually needs the router’s records to find the actual device behind it.

05Read the router’s translation table

Ping once more, then list the connection tracking table, which is the router’s memory of who is talking to whom.

# ip netns exec lan ping -c 1 198.51.100.80
# ip netns exec rtr conntrack -L

Ours showed one entry (trimmed):

icmp 1 29 src=192.168.1.10 dst=198.51.100.80 type=8 ... src=198.51.100.80 dst=198.51.100.1 type=0 ...

Reading it. The first src/dst pair is the packet as the PC sent it. The second is the reply the router expects back: from the server, to the router’s public address. When that reply arrives, the router looks up this entry and swaps the destination back to 192.168.1.10. The 29 is the seconds left before the entry expires.

06Forward a port to a web server on the PC

Start a small web server on the PC, then tell the router: anything arriving on its public address at port 80 goes to the PC’s port 8080. That is destination NAT, better known as port forwarding.

# cd /tmp && echo "hello from the LAN pc" > sg-index.html
# ip netns exec lan python3 -m http.server 8080 --bind 192.168.1.10 &
# ip netns exec rtr nft 'add chain ip nat prerouting { type nat hook prerouting priority dstnat; policy accept; }'
# ip netns exec rtr nft add rule ip nat prerouting iifname "rtr-wan" tcp dport 80 dnat to 192.168.1.10:8080
# ip netns exec inet curl -s http://198.51.100.1/sg-index.html

From the “internet”, asking the router’s public address returned hello from the LAN pc.

Why “prerouting”? Destination NAT has to happen as the packet arrives, before routing, so the router then routes it to the new destination inside the LAN.

07Prove NAT is not a firewall

Right now the server can’t reach 192.168.1.10 directly, but only because it has no route there. Give it one, as someone on your provider’s network could, and try again.

# ip netns exec inet ping -c 1 -W 1 192.168.1.10
# ip -n inet route add 192.168.1.0/24 via 198.51.100.1
# ip netns exec inet curl -s -m 3 http://192.168.1.10:8080/sg-index.html

The ping fails with Network is unreachable. After adding the route, the curl straight to the PC’s private address returned hello from the LAN pc, with no port forward involved.

The attacker’s view. NAT only hides addresses; it never says “no”. Anyone who can route packets to your router can reach every service on your LAN that the router will forward. Home routers are mostly safe because their default firewall drops this traffic, not because of NAT. IPv6, which usually has no NAT, depends entirely on that firewall.

08Add a stateful firewall

Block everything crossing the router by default, then allow three things: replies to conversations already under way, anything the LAN starts, and traffic you deliberately port-forwarded.

# ip netns exec rtr nft add table inet filter
# ip netns exec rtr nft 'add chain inet filter forward { type filter hook forward priority filter; policy drop; }'
# ip netns exec rtr nft add rule inet filter forward ct state established,related accept
# ip netns exec rtr nft add rule inet filter forward iifname "rtr-lan" oifname "rtr-wan" accept
# ip netns exec rtr nft add rule inet filter forward iifname "rtr-wan" ct status dnat accept
# ip netns exec rtr nft list table inet filter

Reading the rules. policy drop means anything not allowed below is thrown away. ct state established,related lets replies back in, using the same connection tracking table as step 5. ct status dnat allows only traffic that a port-forward rule rewrote, so opening a port takes a deliberate DNAT rule.

09Test all three paths again

# ip netns exec lan ping -c 1 -W 1 198.51.100.80
# ip netns exec inet curl -s -m 3 http://198.51.100.1/sg-index.html
# ip netns exec inet curl -s -m 3 http://192.168.1.10:8080/sg-index.html; echo "exit $?"

In our run: the LAN ping still worked, the port-forwarded request still returned hello from the LAN pc, and the direct request now hung and gave up with exit 28 (curl’s “timed out”). The firewall silently dropped it.

The defender’s move. Default-drop on anything crossing the router, and know every port forward you have. nft list ruleset shows them here; on a home router, check the port-forwarding page and turn off UPnP unless you need it, because UPnP lets any device on your LAN open a forward without asking you.

10Clean up

Stop the web server first. Deleting a namespace doesn’t stop programs started inside it.

# pkill -f "http.server 8080"
# for n in lan rtr inet; do ip netns del $n; done
# rm /tmp/sg-index.html
# exit

What you learned

  • private addresses — RFC 1918 ranges are never routed on the internet, so they need NAT to get replies
  • SNAT / masquerade — rewrites the source on the way out (postrouting)
  • DNAT / port forward — rewrites the destination on the way in (prerouting)
  • conntrack — the table that remembers each translation so replies find their way home
  • NAT is not a firewall — a default-drop, stateful forward chain is what actually says no

Tried it? Tell us

Nova built this lab from Steven’s ideas, and we ran every step once before publishing. If a step broke, behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.

NextTCP vs UDP: watch the handshake, refused vs filtered→

Go deeper

Original walkthrough. Every command was run end to end on Ubuntu 24.04 (WSL2, kernel 6.6, nftables 1.0.9) on 27 Oct 2026; the outputs shown are from that run. If something doesn’t match what your machine does, tell me and I will fix it.