Right, let’s get our hands dirty with the actual mechanics of packet filtering. Forget the abstract theory for a moment; this is where the rubber meets the road, or more accurately, where packets meet their glorious or ignominious end.

The core concept here is that of a chain. Think of a chain as a numbered checklist of rules that a packet must work through. When a packet enters your machine’s network stack, the kernel figures out which chain to send it to based on what the packet is doing (e.g., is it a new incoming connection? is it traffic being forwarded? is it traffic originating from this machine?).

Each rule on that checklist is a simple conditional statement: “IF the packet looks like this, THEN do that.” The packet is checked against each rule in the chain, in order, from rule 1 to rule N. As soon as it matches a rule, the action (ACCEPT, DROP, REJECT, etc.) specified by that rule is executed, and the packet’s fate is sealed. No further rules are checked. If it makes it all the way through the chain without matching any rule, it hits the chain’s default policy, which is usually DROP or ACCEPT. This is why the order of your rules is absolutely critical. It’s the first match that wins.

The Holy Trinity: Filter, NAT, and Mangle Tables

Now, you can’t just have one giant checklist for everything. That would be a nightmare. So, iptables and its modern successor nftables organize rules into different tables, each responsible for a specific type of packet manipulation.

  • The filter Table: This is the one you care about 99% of the time. It’s for basic packet filtering—allowing, dropping, or rejecting traffic. The INPUT, OUTPUT, and FORWARD chains live here. If you’re building a wall, this table is the big, imposing brick facade.

  • The nat Table: This is for Network Address Translation. Its job is to rewrite the source or destination addresses of packets. The PREROUTING chain (for altering packets as they come in, before routing) and POSTROUTING chain (for altering packets as they are about to go out, after routing) are the stars here. This is how your home router makes your entire family’s internet traffic appear to come from a single IP address. It’s the sleight of hand happening behind the curtain.

  • The mangle Table: This is for, well, mangling packets. It’s used for specialized alteration of packet headers, like adjusting the Type of Service (TOS) field or setting specific marks on packets that can be used by other parts of the kernel (like tc for traffic shaping) or for later rule matching. You’ll use this about as often as you use a surgical scalpel to make a peanut butter sandwich—powerful, but usually overkill.

Here’s a concrete example. Let’s say you want to block all incoming SSH connection attempts from a particularly pesky network, 192.168.100.0/24. You’d add a rule to the INPUT chain of the filter table. The -A flag means “append this rule to the chain.”

sudo iptables -t filter -A INPUT -s 192.168.100.0/24 -p tcp --dport 22 -j DROP

But since the filter table is the default, you can omit the -t filter and achieve the exact same thing. This is the syntax you’ll see most often in the wild.

sudo iptables -A INPUT -s 192.168.100.0/24 -p tcp --dport 22 -j DROP

State is Everything: The Magic of Connection Tracking

Here’s the part that trips up every newcomer, and it’s the single most important concept for writing effective, simple rulesets: connection tracking (conntrack).

The early, stateless firewalls treated every single packet in isolation. Allowing inbound SSH reply packets meant writing a rule like -p tcp --sport 22, which is a massive security hole—you’re allowing any packet from anyone’s port 22! Connection tracking fixes this madness.

The kernel can watch network connections from start to finish and assign a state to packets within that connection. The main states you’ll use are:

  • NEW: The packet is starting a new connection.
  • ESTABLISHED: The packet is part of an already-established, two-way conversation.
  • RELATED: The packet is starting a new connection that is related to an existing one (e.g., an FTP data transfer connection).

This is a superpower. It means your firewall can be intelligent. You can write one simple rule to allow all outgoing traffic (-A OUTPUT -j ACCEPT), and then, because of connection tracking, you can write a single rule to allow the replies to that traffic back in!

# Allow all outgoing traffic (and its replies will be ESTABLISHED)
iptables -A OUTPUT -j ACCEPT

# Allow incoming traffic that is part of an established or related connection
# This is the key line that makes everything work
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Now, you can explicitly open only the specific NEW incoming ports you need, like SSH
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# And finally, set the default policy to DROP everything else that snuck through
iptables -P INPUT DROP

This “allow ESTABLISHED/RELATED first” pattern is non-negotiable best practice. It creates a clean, secure, and maintainable foundation. The most common pitfall is getting the order wrong and accidentally blocking the ESTABLISHED traffic before you get to the rule that allows it, locking yourself out of your own machine. Always list your ACCEPT rules for ESTABLISHED traffic before your DROP policy. The designers gave us this brilliant tool; it’s borderline criminal not to use it.