Iptables lets you control the flow of IPv4 packets on a Linux device. I wrote this post because it took me 2 weeks to learn the tool, but I could have done it in a day with the right guide. By the end of this post, you’ll have a solid mental framework for working with iptables, and you should be able to use it in practice. This knowledge could come in handy when you’re working with virtual machines and containers, or trying to understand how popular software like Docker works under the hood.

A primer on Linux networking

Before we dive into iptables itself, let’s talk about Linux networking in general. Every network adapter – like a WiFi card or an Ethernet port – is represented on Linux as an interface. Network communication always happens between interfaces, potentially on different computers. When you opened this webpage, a bunch of messages called IP packets were sent from the interface corresponding to your WiFi card to an interface on the server that hosts my blog. The server then sent a bunch of IP packets containing this article back through the same interfaces.

How iptables works

With networking basics out of the way, let’s talk about iptables. It allows you to control the flow of IP packets between interfaces by writing rules and adding them to chains, which together form tables. For example, the Filter table consists of the Input, Output, and Forward chains. A chain is a series of rules. Every IP packet handled by your machine goes through a predefined list of chains, and every rule in these chains is evaluated in order. When iptables finds a matching rule, it either accepts, rejects, or executes a different action on the packet. If no matching rule is found in a chain, a configurable default policy is executed. That policy can be ACCEPT or DROP. There are other, less common policies too, but you don’t need to know them to start writing your own rules.

When a packet is accepted, it’s forwarded to the next chain, or if no chains remain, to its destination. When it’s dropped, it’s dropped right there and then.

These are the tables and chains that iptables uses by default. Table names are in the header, chains below.

FilterNATMangleRaw
INPUTPREROUTINGPREROUTINGPREROUTING
OUTPUTOUTPUTPOSTROUTINGOUTPUT
FORWARDPOSTROUTINGOUTPUT
INPUT
FORWARD

Iptables classifies packets into three types:

  • packets from source local host (our own computer);
  • packets to destination local host (our own computer);
  • and forwarded packets.

There’s a predefined order to how these types of packets go through the different tables and chains, and you need to be able to look it up when you write your own rules. Here’s the order:

#Source local hostDestination local hostForwarded packets
1Raw, OUTPUTRaw, PREROUTINGRaw, PREROUTING
2Mangle, OUTPUTMangle, PREROUTINGMangle, PREROUTING
3Nat, OUTPUTNat, PREROUTINGNat, PREROUTING
4Filter, OUTPUTMangle, INPUTMangle, FORWARD
5Mangle, POSTROUTINGFilter, INPUTFilter, FORWARD
6Nat, POSTROUTINGMangle, POSTROUTING
7Nat, POSTROUTING

This table is based on this excellent guide.

Whenever you add a new rule, you have to consider whether it will apply to source local host, destination local host, or forwarded packets. Then you look up which tables and chains the packet will hit, and you pick one to add your rule to.

Here’s a short breakdown of what kinds of rules should1 go into which tables:

  • Filter: rules that filter packets out;
  • NAT: rules that modify packets for NAT purposes;
  • Mangle: rules that modify some packet fields;
  • Raw: rules that opt packets out of the connection tracking system, which is an advanced part of iptables we will not get into.

Usage in practice

Now you know the basics of iptables. But how do you actually use it? Let’s say you want to add a rule to accept TCP traffic on port 22 on the eth0 interface. It applies to destination local host packets, and it filters them, so you’d add it to the Filter, INPUT chain. Here’s the shell command you’d use:

$ iptables -A INPUT -i eth0 -p tcp --dport 22 -j ACCEPT

Let’s break it down:

  • if no table is specified, the filter table is used by default.
  • -A INPUT is the action to append a rule to the end of the INPUT chain.
  • -i eth0 is the interface to match on.
  • -p tcp is the protocol to match on.
  • --dport 22 is the destination port to match on.
  • -j ACCEPT is the action to take if the packet matches the rule.

This rule only makes a difference if the default policy on the INPUT chain is set to DROP. If the default policy is set to ACCEPT, the rule is redundant.

Now let’s go through the journey of a tcp packet destined for port 22.

  1. The packet is on the wire, e.g. the Internet.
  2. The packet reaches an interface on the local host, e.g. eth0.
  3. Since its destination is the local host, it goes through the following tables and chains in order:
    • Raw, PREROUTING
    • Mangle, PREROUTING
    • Nat, PREROUTING
    • Mangle, INPUT
    • Filter, INPUT
  4. Once it reaches the INPUT chain in the Filter table, it is checked against the rule we wrote. Since it is a tcp packet destined for port 22 that arrived on the eth0 interface, it is accepted. Because the Filter, INPUT chain is the last chain for destination local host packets, the packet is passed to port 22.

You can also use iptables to filter outgoing traffic from your computer. If you wanted to prevent users from reading Hacker News2, you could write the following rule:

$ iptables -A OUTPUT -j REJECT -d 209.216.230.240/32

Let’s break it down:

  • as before, if no table is specified, the filter table is used by default.
  • -A OUTPUT is the action to append a rule to the end of the OUTPUT chain. We use the Filter, OUTPUT chain because the packet’s source is local host.
  • -j REJECT is the action to take if the packet matches the rule.
  • -d 209.216.230.240/32 is the destination IP address and mask to match on. 32 means that the mask is 32 bits long, which means that the destination IP address is the only IP address that matches.

Now let’s check out the journey of a packet that wants to reach the Hacker News server:

  1. The packet originates from a local process.
  2. Since its source is the local host, it goes through the following tables and chains in order:
    • Raw, OUTPUT
    • Mangle, OUTPUT
    • Nat, OUTPUT
    • Filter, OUTPUT
  3. Once it reaches the OUTPUT chain in the Filter table, it is checked against the rule we wrote. Since it matches the IP address, it is rejected. Note that once it is rejected, it is not checked against any other rules.

Conclusion and Further Reading

You should now be dangerous enough to write your own, basic iptables rules. The tool is also really good at doing more fancy things like modifying incoming and outgoing packets for NAT purposes. In fact, this is how Docker provides internet access to containers.3

Iptables is a beast, with more features than I ever had the time to explore. If you want to learn more, here’s an excellent, comprehensive guide that this post is based on.

However, iptables is also showing its age. It’s only compatible with IPv4. For IPv6, there’s ip6tables. The more modern nftables aims to succeed both, but iptables is still widely used and worth learning.

Footnotes

  1. You can generally stick your rules into whichever table you want regardless of their purpose. Certain tables will disallow some rules though. E.g. “You are strongly advised not to use [the Mangle] table for any filtering; nor will any DNAT, SNAT or Masquerading work in this table.”

  2. Assuming the Hacker News people haven’t changed their public IP address since I wrote this.

  3. As of Docker version 24.0.7 on Linux using the network driver “bridge.”