TT Lab
Get started
Learn Learning paths Courses

Network Fundamentals — Hands-on in a Linux VM

Push a Private Network Out with NAT

Continue in TT Lab

This lab runs on a VM

This time the VM itself is the router. The namespace lan is a host on a private network, and the VM's uplink (enp1s0, 10.0.2.2) faces the internet. The private address 10.30.0.2 has no way for replies to return from outside, so the VM has to rewrite it to its own address and send it out — that is masquerade. This VM can reach only public 80/443 and DNS.

Goal

Send a private-network host out to the internet with masquerade (SNAT), capture the packet's source actually being rewritten on both interfaces, and pass a connection that arrived from outside to an inner server with DNAT. This is exactly what a home router and Docker's -p do.

Why it matters

Private addresses are not routed on the internet. Yet homes, offices, cloud VPCs, and Kubernetes Pods all use private addresses. There is only one way for them to talk to the outside — rewrite the address at the boundary and restore it when the reply comes. What makes that restoring possible is connection tracking (conntrack), and that is why all connections drop when a NAT device restarts. The answer to the report "it works from inside but not from outside" is almost always in this chapter.

Steps

  1. Create the namespace lan and the veth lan0 (in lan, 10.30.0.2/24)↔lan-r (in root, 10.30.0.1/24), and set the default route of lan to 10.30.0.1 and net.ipv4.ip_forward in root to 1.
  2. Save the result showing that ping from lan to the uplink gateway 10.0.2.1 fails to /root/nat/before.txt.
  3. In the postrouting hook chain of the table ip labnat, add a rule that does counter masquerade when 10.30.0.0/24 goes out through the uplink, so that ping works.
  4. Capture ICMP on the uplink and on lan-r at the same time while sending a ping from lan, and save the source as seen from outside, now changed, to /root/nat/outside.txt and the original source on the inside to /root/nat/inside.txt.
  5. Write the upstream DNS server in /etc/netns/lan/resolv.conf so that getent hosts archive.ubuntu.com works inside lan, and save the result to /root/nat/dns.txt.
  6. Save the response headers of curl -sI http://archive.ubuntu.com/ from lan to /root/nat/web.txt.
  7. Save nft list table ip labnat to /root/nat/counter.txt. The counter packets of the masquerade rule must be greater than 0.
  8. Start a web server on 10.30.0.2:80 inside lan, create a namespace guest (veth guest0 10.31.0.2/24 ↔ guest-r 10.31.0.1/24), make http://10.31.0.1:8080/ work from guest with the rule iifname "guest-r" tcp dport 8080 dnat to 10.30.0.2:80 in a prerouting hook chain, and save the first line of that response to /root/nat/dnat.txt.

Notes

One private-network host

Create the namespace lan and the veth lan0 (in lan, 10.30.0.2/24)↔lan-r (in root, 10.30.0.1/24), and set the default route of lan to 10.30.0.1 and net.ipv4.ip_forward in root to 1.

This time you leave one end of the veth (lan-r) in root instead of moving it. ip addr add 10.30.0.1/24 dev lan-r; ip link set lan-r up. ip -n lan route add default via 10.30.0.1. sysctl -w net.ipv4.ip_forward=1.

Without NAT

Save the result showing that ping from lan to the uplink gateway 10.0.2.1 fails to /root/nat/before.txt.

ip netns exec lan ping -c 1 -W 2 10.0.2.1 > /root/nat/before.txt 2>&1. The request goes out through the VM, but there is no path for the reply to return to 10.30.0.2, so you get 100% packet loss. It is a different symptom from when there is no route (unreachable).

Masquerade

In the postrouting hook chain of the table ip labnat, add a rule that does counter masquerade when 10.30.0.0/24 goes out through the uplink, so that ping works.

nft add table ip labnat, nft 'add chain ip labnat post { type nat hook postrouting priority 100; }', nft add rule ip labnat post ip saddr 10.30.0.0/24 oifname "enp1s0" counter masquerade. Now ping from lan works.

The moment the address changes

Capture ICMP on the uplink and on lan-r at the same time while sending a ping from lan, and save the source as seen from outside, now changed, to /root/nat/outside.txt and the original source on the inside to /root/nat/inside.txt.

Start two captures: timeout 5 tcpdump -i enp1s0 -nn -l icmp > /root/nat/outside.txt 2>&1 & and timeout 5 tcpdump -i lan-r -nn -l icmp > /root/nat/inside.txt 2>&1 &. Then ip netns exec lan ping -c 1 10.0.2.1. The outside file must show 10.0.2.2 > 10.0.2.1 and the inside file must show 10.30.0.2 > 10.0.2.1.

The namespace's resolv.conf

Write the upstream DNS server in /etc/netns/lan/resolv.conf so that getent hosts archive.ubuntu.com works inside lan, and save the result to /root/nat/dns.txt.

mkdir -p /etc/netns/lan; echo 'nameserver <상류>' > /etc/netns/lan/resolv.conf (the placeholder is the upstream server address). The upstream address comes from resolvectl dns enp1s0. Inside a namespace there is no 127.0.0.53 stub, so you have to write the real server directly. The query goes out through the masquerade.

Reaching the internet

Save the response headers of curl -sI http://archive.ubuntu.com/ from lan to /root/nat/web.txt.

ip netns exec lan curl -sI -m 10 http://archive.ubuntu.com/ > /root/nat/web.txt. The first line must be HTTP/1.1 200 OK. A host with a private address has established TCP with an internet server.

Count it

Save nft list table ip labnat to /root/nat/counter.txt. The counter packets of the masquerade rule must be greater than 0.

You put in counter in step 3, so nft list table ip labnat shows counter packets N bytes M masquerade. N is the number of first packets of new connections — later packets are handled by conntrack and do not pass through the NAT chain again.

Port forwarding

Start a web server on 10.30.0.2:80 inside lan, create a namespace guest (veth guest0 10.31.0.2/24 ↔ guest-r 10.31.0.1/24), make http://10.31.0.1:8080/ work from guest with the rule iifname "guest-r" tcp dport 8080 dnat to 10.30.0.2:80 in a prerouting hook chain, and save the first line of that response to /root/nat/dnat.txt.

Server: ip netns exec lan setsid python3 -m http.server 80 --bind 10.30.0.2 --directory /root/nat > /root/nat/http.log 2>&1 < /dev/null &. Set up guest the same way as in step 1 (default route 10.31.0.1). Chain: nft 'add chain ip labnat pre { type nat hook prerouting priority -100; }'. To check: ip netns exec guest curl -sI http://10.31.0.1:8080/ | head -1.