Firewall rules don’t just have to be a single, linear list; you can create custom chains to break down complexity and make your ruleset manageable.
Let’s see this in action. Imagine a basic web server setup. We want to allow incoming HTTP and HTTPS traffic, but only on specific ports and from specific sources, while also logging denied packets.
# Allow established and related connections to pass through
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Log and drop invalid packets
iptables -A INPUT -m conntrack --ctstate INVALID -j LOG --log-prefix "INVALID PACKET: "
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# Create a custom chain for web traffic
iptables -N WEB_TRAFFIC
# Jump to the WEB_TRAFFIC chain for incoming traffic on ports 80 and 443
iptables -A INPUT -p tcp --dport 80 -j WEB_TRAFFIC
iptables -A INPUT -p tcp --dport 443 -j WEB_TRAFFIC
# Define rules within the WEB_TRAFFIC chain
# Allow HTTP from anywhere
iptables -A WEB_TRAFFIC -p tcp --dport 80 -j ACCEPT
# Allow HTTPS from anywhere
iptables -A WEB_TRAFFIC -p tcp --dport 443 -j ACCEPT
# Log and drop any other traffic that tried to reach WEB_TRAFFIC
iptables -A WEB_TRAFFIC -j LOG --log-prefix "DENIED WEB TRAFFIC: "
iptables -A WEB_TRAFFIC -j DROP
# Set the default policy for the INPUT chain to DROP
iptables -P INPUT DROP
# List all rules, including custom chains
iptables -L -v -n --line-numbers
This example starts by accepting established connections, which is crucial for any stateful firewall. Then, it logs and drops invalid packets to prevent certain types of network attacks. The core of the organization comes with the iptables -N WEB_TRAFFIC command, which creates a new, empty chain named WEB_TRAFFIC.
We then use iptables -A INPUT -p tcp --dport 80 -j WEB_TRAFFIC and iptables -A INPUT -p tcp --dport 443 -j WEB_TRAFFIC to direct incoming traffic destined for ports 80 (HTTP) and 443 (HTTPS) to our new custom chain. The -j WEB_TRAFFIC instruction tells iptables to stop processing the current chain (INPUT) and jump to the WEB_TRAFFIC chain for further evaluation.
Inside the WEB_TRAFFIC chain, we define the specific rules for web traffic: allowing TCP traffic on ports 80 and 443. Crucially, after these allow rules, we have a logging and dropping rule: iptables -A WEB_TRAFFIC -j LOG --log-prefix "DENIED WEB TRAFFIC: " and iptables -A WEB_TRAFFIC -j DROP. This ensures that anything that reached the WEB_TRAFFIC chain but didn’t match an explicit ACCEPT rule is logged and then discarded. Finally, the iptables -P INPUT DROP sets the default policy for the main INPUT chain to DROP, meaning any traffic not explicitly allowed by rules in the INPUT chain (or its descendant custom chains) will be dropped.
The benefit here is clarity. Instead of a massive INPUT chain with dozens of rules for different services, we’ve compartmentalized web traffic. If we later wanted to add rules for SSH, we could create a SSH_TRAFFIC chain and jump to it from the INPUT chain, keeping the INPUT chain itself relatively clean. This modularity makes it significantly easier to audit, debug, and modify your firewall ruleset as your server’s role or security requirements change.
When you use iptables -j MARK --set-mark <value> within a custom chain, that mark is associated with the packet and can be acted upon by subsequent rules in any chain, including the main chains like INPUT, OUTPUT, or FORWARD. This allows you to create complex, multi-stage filtering and routing logic where decisions made deep within a custom chain can influence how the packet is handled much later in its journey.