Notes on NAT and Forwarding using iptables

In my post about TUN interfaces, there was one question left unanswered - how to configure the interface to send/receive packets to hosts on the Internet.

Similar to the above post, I already had a set of commands for this, but didn't understand how they worked:

1
2
3
4sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -A FORWARD -i tun0 -s 192.0.2.2 -j ACCEPT
sudo iptables -t nat -A POSTROUTING -o eth0 -s 192.0.2.2 -j MASQUERADE
sudo iptables -A FORWARD -o tun0 -d 192.0.2.2 -j ACCEPT

I knew it vaguely had something to do with NAT or Network Address Translation, where the source address of a packet is changed before sending and vice versa.

However, understanding the concept alone wasn't enough, as the terms in the commands still didn't make much sense and I hadn't used iptables before this.

I decided to figure out what each command does, and how the commands relate to each other, i.e., the order in which the commands make sense.

Approach #

I have a C program that connects to the TUN interface and initiates a connection to example.com. Since I haven't implemented sending and receiving of data yet, the connection ends with a RST packet.

To see where and how the packets travel, I decided to capture packets with all the rules applied first, using tcpdump.

$ sudo tcpdump -Stni any host 93.184.216.34
tun0  In  IP 192.0.2.2.19843 > 93.184.216.34.80: Flags [S], seq 1542704629, win 65535, length 0
eth0  Out IP 192.168.100.141.19843 > 93.184.216.34.80: Flags [S], seq 1542704629, win 65535, length 0
eth0  In  IP 93.184.216.34.80 > 192.168.100.141.19843: Flags [S.], seq 467981851, ack 1542704630, win 65535, options [mss 1460], length 0
tun0  Out IP 93.184.216.34.80 > 192.0.2.2.19843: Flags [S.], seq 467981851, ack 1542704630, win 65535, options [mss 1460], length 0
tun0  In  IP 192.0.2.2.19843 > 93.184.216.34.80: Flags [.], ack 467981852, win 65535, length 0
eth0  Out IP 192.168.100.141.19843 > 93.184.216.34.80: Flags [.], ack 467981852, win 65535, length 0
tun0  In  IP 192.0.2.2.19843 > 93.184.216.34.80: Flags [R], seq 1542704630, win 65535, length 0
eth0  Out IP 192.168.100.141.19843 > 93.184.216.34.80: Flags [R], seq 1542704630, win 65535, length 0

The outgoing packets (Lines 1-2, 5-6, 7-8) travel from tun0 to eth0 with the source address changed to 192.168.100.141. Incoming packets (Lines 3-4) travel from eth0 to tun0 with the source address changed back to 192.0.2.2.

Each packet is displayed twice as tcpdump is displaying packets from both interfaces, eth0 and tun0.

This helps in understanding the bigger picture, however I can't tell which command is responsible for which action. To figure that out, I started another capture with none of the commands applied, and added each one till the output matched the one above.

No commands applied #

$ sudo tcpdump -Stni any host 93.184.216.34
tun0  In  IP 192.0.2.2.28921 > 93.184.216.34.80: Flags [S], seq 734780588, win 65535, length 0

The SYN packet arrives at the tun0 interface 1 and...that's it.

For the packet to reach eth0, my machine needs to become a router and forward packets from tun0 to eth0. This is possible with IP forwarding, a feature that's usually disabled by default.

Enable IP forwarding #

sudo sysctl -w net.ipv4.ip_forward=1

With this set, the packets should reach eth0...right?

tun0  In  IP 192.0.2.2.7432 > 93.184.216.34.80: Flags [S], seq 84458994, win 65535, length 0

Oh, that didn't work. The output is the same as before.

I then looked a bit more into iptables, and found out that packets are forwarded based on rules mentioned by it. So I checked what the existing rules looked like:

$ sudo iptables -L FORWARD -v
Chain FORWARD (policy DROP 0 packets, 0 bytes)
 pkts bytes target       prot opt in       out     source               destination
    0     0 DOCKER-USER  all  --  any      any     anywhere             anywhere
    0     0 DOCKER-ISOLATION-STAGE-1  all  --  any    any     anywhere             anywhere
    0     0 ACCEPT       all  --  any      docker0  anywhere             anywhere             ctstate RELATED,ESTABLISHED
    0     0 DOCKER       all  --  any    docker0  anywhere             anywhere
    0     0 ACCEPT       all  --  docker0 !docker0  anywhere             anywhere
    0     0 ACCEPT       all  --  docker0 docker0  anywhere             anywhere

There are two main things to notice here:

  1. The default policy is set to DROP. This means that all packets are dropped unless there is a rule that states otherwise.
  2. Among the current rules to accept packets (target = ACCEPT), none of them apply to the tun0 interface, which is why the SYN packet doesn't get forwarded.

So the next step would be to add a rule to allow packets arriving at the TUN interface to go through.

Forward packets from the TUN interface #

sudo iptables -A FORWARD -i tun0 -s 192.0.2.2 -j ACCEPT

This command appends a rule to the FORWARD chain, to accept all packets from tun0 with the source address 192.0.2.2.

tun0  In  IP 192.0.2.2.7076 > 93.184.216.34.80: Flags [S], seq 878715282, win 65535, length 0
eth0  Out IP 192.0.2.2.7076 > 93.184.216.34.80: Flags [S], seq 878715282, win 65535, length 0

Now the SYN packet has reached the eth0 interface and was sent out to example.com! However, if the packet was sent, why didn't the host send a response?

Let's pay close attention to the source address, which is currently 192.0.2.2. When the host sends a response, it sends it to 192.0.2.2...which is not my machine's actual IP address.

Here we need to enable NAT, which can be also be configured using iptables.

Network Address Translation #

sudo iptables -t nat -A POSTROUTING -o eth0 -s 192.0.2.2 -j MASQUERADE

This rule gets added to the POSTROUTING chain, which is part of the nat table. Packets arrive at this chain just before they're about to leave the network, which in this case is from the eth0 interface.

The MASQUERADE target confused me for a good while, until I came across another similar target, SNAT.

SNAT or Source Network Address Translation does exactly what it says, i.e., it changes the source address of a packet to the address you specify. MASQUERADE works exactly like SNAT, however it is intended for dynamic IP addresses, which can be assigned differently each time.

MASQUERADE figures out the IP address for you, which is why there's no second address mentioned in the command. If you're running this on a machine with a static IP address (a server on the cloud for example), you could replace -j MASQUERADE with -j SNAT --to-source <ip> and it would work the same way.

tun0  In  IP 192.0.2.2.26921 > 93.184.216.34.80: Flags [S], seq 269235594, win 65535, length 0
eth0  Out IP 192.168.100.141.26921 > 93.184.216.34.80: Flags [S], seq 269235594, win 65535, length 0
eth0  In  IP 93.184.216.34.80 > 192.168.100.141.26921: Flags [S.], seq 1505993560, ack 269235595, win 65535, options [mss 1460], length 0
eth0  In  IP 93.184.216.34.80 > 192.168.100.141.26921: Flags [S.], seq 1505993560, ack 269235595, win 65535, options [mss 1460], length 0
eth0  In  IP 93.184.216.34.80 > 192.168.100.141.26921: Flags [S.], seq 1505993560, ack 269235595, win 65535, options [mss 1460], length 0
eth0  In  IP 93.184.216.34.80 > 192.168.100.141.26921: Flags [S.], seq 1505993560, ack 269235595, win 65535, options [mss 1460], length 0

Looks like progress is being made as the host responds with a SYNACK packet! Or maybe too many of them.

The host is re-sending the same packet because it isn't getting an ACK in response, and is under the impression that the packet didn't reach my machine. But my program has code to send an ACK, so what's happening here?

If you recall the forwarding rule, I only allowed packets with a source address of 192.0.2.2 to be forwarded. I didn't account for the destination address, which is why the SYNACK didn't get forwarded, and the program didn't receive it.

Forward packets to the TUN interface #

sudo iptables -A FORWARD -o tun0 -d 192.0.2.2 -j ACCEPT

This last rule accepts packets coming to tun0 with the destination address of 192.0.2.2, allowing packets to reach the C program.

tun0  In  IP 192.0.2.2.19843 > 93.184.216.34.80: Flags [S], seq 1542704629, win 65535, length 0
eth0  Out IP 192.168.100.141.19843 > 93.184.216.34.80: Flags [S], seq 1542704629, win 65535, length 0
eth0  In  IP 93.184.216.34.80 > 192.168.100.141.19843: Flags [S.], seq 467981851, ack 1542704630, win 65535, options [mss 1460], length 0
tun0  Out IP 93.184.216.34.80 > 192.0.2.2.19843: Flags [S.], seq 467981851, ack 1542704630, win 65535, options [mss 1460], length 0
tun0  In  IP 192.0.2.2.19843 > 93.184.216.34.80: Flags [.], ack 467981852, win 65535, length 0
eth0  Out IP 192.168.100.141.19843 > 93.184.216.34.80: Flags [.], ack 467981852, win 65535, length 0
tun0  In  IP 192.0.2.2.19843 > 93.184.216.34.80: Flags [R], seq 1542704630, win 65535, length 0
eth0  Out IP 192.168.100.141.19843 > 93.184.216.34.80: Flags [R], seq 1542704630, win 65535, length 0

And now the output matches the packet capture at the very start!

Conclusion #

I think I have a better understanding of iptables, forwarding and NAT thanks to this, hope it it's the same case for you too!

Here are some links that helped me while writing this post:

Footnotes

  1. If you're wondering why the output mentions In instead of Out, a TUN interface has two addresses - the interface itself and a program that connects to the interface. So packets sent by the program are considered incoming. This section from the TUN interfaces post goes over this in detail.