ipchains arrives, and the firewall improves

The Linux 2.2 kernel — which is now stable and shipping in Slackware 4.0 and the other major distributions — comes with a new firewall framework called ipchains. It replaces ipfwadm, the tool I wrote about a year ago when I was trying to make sense of it.

I have spent the last week migrating my home firewall from ipfwadm to ipchains. The migration was straightforward; the conceptual differences are larger than they first appear. This post is what I have learned.

The shape of the change

The 2.0 kernel had three categories of firewall hook: input, output, and forward. Each was a single ordered list of rules. A packet passing through the kernel triggered exactly one of these lists, depending on its direction.

This was simple but limiting. There was no way to express "if a packet matches X, jump to a different rule list". Every rule had to be in one of the three flat lists. Complex rule sets became unmanageable quickly.

The 2.2 kernel, with ipchains, generalises the model. Instead of three fixed lists, you have chains — named ordered lists of rules. The three built-in chains (input, output, forward) still exist. But now you can create your own chains with your own names, and you can have a rule that says "jump to chain foo". Rules in a sub-chain can return to the calling chain or terminate the packet.

This is the same technique that programming languages use for procedure calls. It does not sound revolutionary written down. In practice it changes everything about how a non-trivial firewall is organised.

A small example

My old ipfwadm configuration had something like a hundred rules in the input chain. Many of them were related — there were ten rules to do with HTTP, fifteen to do with mail, seven to do with DNS — but they were all interleaved in a single list, ordered by priority but not by topic.

The ipchains version of the same configuration has a small input chain that does the basic policy work — accept loopback, accept established connections — and then dispatches to topic-specific sub-chains.

# Build the sub-chains
ipchains -N http-rules
ipchains -N mail-rules
ipchains -N dns-rules

# Dispatch from the main input chain
ipchains -A input -p tcp --dport 80 -j http-rules
ipchains -A input -p tcp --dport 25 -j mail-rules
ipchains -A input -p udp --dport 53 -j dns-rules

# Now define the rules in each sub-chain
ipchains -A http-rules -s 192.0.2.0/24 -j ACCEPT
ipchains -A http-rules -j DENY
# ... etc

The organisational benefit is enormous. Each topic is its own block. When I am editing the HTTP rules, I am working in a chain that contains only HTTP rules. I cannot accidentally break mail handling by editing HTTP, because mail handling lives somewhere else.

The semantics that surprised me

A few subtle differences from ipfwadm that took me a moment to absorb.

The default policy is per-chain, not global. Each chain has its own default. If a packet runs off the end of input without matching, the input policy applies. This is more fine-grained but also a place where sloppy configuration can hide.

A jump to a sub-chain that does not match returns to the calling chain. This is exactly like a function call: the called chain runs through, and if no terminal action is taken, control returns to the next rule in the caller. This means a sub-chain can be "check this category and act if relevant; otherwise let the parent handle it".

MASQ and REDIRECT targets are first-class. Network address translation, which under ipfwadm was a separate concept, is now expressed as a target inside the firewall ruleset. This unifies what was previously two separate moving parts.

The matching syntax is more uniform. ipfwadm had different command-line flags for different match types; ipchains uses a consistent --option value style. Hand-written rule scripts read better. Generated rule scripts have a saner shape.

What I changed about my home firewall

The old layout was an unstructured list of about a hundred rules. The new layout is:

input chain
├── accept-loopback
├── accept-established
├── reject-spoofs
├── log-and-drop reserved-source-addresses
├── jump to public-services
├── jump to private-services
├── jump to outbound-only
└── default: log and drop

With each named chain containing its own focused rules. The total rule count is similar; the legibility is far higher. I can walk a maintenance person through the firewall in five minutes; the previous one needed half an hour and a printout.

The rejection-of-spoofs sub-chain is a good example of the value of the new model. Spoofed-source-address handling used to be three or four rules scattered through the input chain; now it is its own named block. If I find that another reserved range needs to be added (which happens — IANA's allocations change occasionally), I add a rule to the spoof chain. Nothing else needs to change.

A migration tip for anyone moving across

The ipchains-restore and ipchains-save tools are the right way to manage rules. They produce a textual representation of the entire ruleset that can be saved to a file, version-controlled, edited, and reloaded.

The naive approach is to write a shell script that calls ipchains repeatedly. This works. It also tends to drift over time, with the file representation getting out of sync with the actual running rules.

The save/restore approach inverts this. The file is the source of truth. The running ruleset is reloaded from the file on every boot. Hand-edits to the running ruleset are temporary and require a deliberate save to make them permanent.

I have moved my whole setup to this discipline. The file lives in /etc/firewall.rules and is version-controlled in CVS. Any change to the firewall is, in effect, a small change request in the version-controlled file.

What is coming next

The ipchains framework is itself going to be replaced. The work that will become netfilter — the framework for the 2.4 kernel, currently in development — generalises further again, with a clean separation between packet-classification and packet-action, and with extension points that allow modules to add new match and action types without modifying the core.

I have read some of the early design documents. The shape is good. The transition from ipchains to netfilter will, I expect, be similar in scale to the ipfwadm to ipchains transition: not a one-for-one rewrite, but a re-think that produces a noticeably better tool.

For now, ipchains is what is shipping, and it is a meaningful improvement on what came before. The migration was a weekend. The benefit will compound for years.


Back to all writing