Andrew Mercer
on this page

What it is

iptables is the classic userspace tool for configuring the kernel's Netfilter packet filtering framework — firewall rules, NAT, and packet mangling. Most modern distros have shifted the underlying engine to nftables, but iptables (now a compatibility shim — iptables-nft) remains the interface most people still write and read, and it's what firewalld and most existing automation generate under the hood.

For the framework this tool configures, see the Netfilter guide.

Tables and chains

Table Purpose Built-in chains
filter (default) Accept/drop/reject decisions INPUT, OUTPUT, FORWARD
nat Address/port translation PREROUTING, POSTROUTING, OUTPUT
mangle Packet header modification all five
raw Exemption from connection tracking PREROUTING, OUTPUT

For a router/firewall the two chains that matter most are FORWARD (traffic passing through the box) and nat's POSTROUTING (rewriting source addresses for outbound NAT).

Enabling IP forwarding

Required before the box will route traffic between interfaces at all — without this, NAT/FORWARD rules are configured but nothing actually passes:

echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -w net.ipv4.ip_forward=1

Verify:

cat /proc/sys/net/ipv4/ip_forward   # should print 1

NAT / masquerading (router use case)

The single most common firewall task on a home router or gateway box: let internal clients reach the internet through one external interface.

# NAT internal traffic out through the external interface
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

# Allow forwarding from internal to external
sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT

# Allow the return traffic back in
sudo iptables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT

MASQUERADE vs SNAT: use MASQUERADE when the external interface's IP can change (DHCP, dynamic IP); use SNAT --to-source <ip> when it's static — it's slightly cheaper since the kernel doesn't need to re-check the interface address per packet.

Port forwarding a specific service inward

# Forward external:8080 to an internal web server
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.10:80
sudo iptables -A FORWARD -p tcp -d 192.168.1.10 --dport 80 -j ACCEPT

Static routes

ip route add 10.2.0.0/16 dev eth2
ip route add 10.8.0.0/24 via 10.2.0.1 dev eth2

Persisting rules

Rules configured with iptables -A live only in the running kernel state — they're gone on reboot unless saved:

# Debian/Ubuntu
sudo apt -y install iptables-persistent
sudo netfilter-persistent save

# RHEL/CentOS (legacy iptables service)
sudo iptables-save > /etc/sysconfig/iptables

Inspecting current rules

sudo iptables -L -v -n              # list with packet/byte counters, no name resolution
sudo iptables -t nat -L -v -n       # same, for the nat table
sudo iptables -S                    # rules in iptables-restore-compatible syntax (easier to copy/paste)

firewalld direct rules

On RHEL-family systems running firewalld, raw iptables syntax is injected via --direct:

sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i eth1 -o eth0 -j ACCEPT
sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo firewall-cmd --complete-reload

Or the same rules as a persistent XML file at /etc/firewalld/direct.xml:

<?xml version="1.0" encoding="utf-8"?>
<direct>
    <rule ipv="ipv4" table="nat" chain="PREROUTING" priority="0">-i eth1 -p tcp --dport 80 -j REDIRECT --to-ports 3128</rule>
    <rule ipv="ipv4" table="nat" chain="POSTROUTING" priority="0">-i eth1 -p udp --dport 1194 -j MASQUERADE</rule>
</direct>

Zone-based config (the firewalld-native way)

Prefer firewall-cmd zones over direct rules where possible — they're easier to audit and survive firewall-cmd --reload cleanly:

# Assign interfaces to zones
sudo firewall-cmd --zone=external --add-interface=eth0 --permanent
sudo firewall-cmd --zone=internal --add-interface=eth1 --permanent

# Enable masquerading on the external zone
sudo firewall-cmd --zone=external --add-masquerade --permanent

# Allow specific inbound services/ports on the external zone
sudo firewall-cmd --permanent --zone=external --add-rich-rule='rule family="ipv4" source address="203.0.113.10" port protocol="tcp" port="22" accept'

sudo firewall-cmd --complete-reload
sudo firewall-cmd --zone=external --list-all

Common gotcha: an interface under NetworkManager's control that isn't correctly bound to a firewalld zone will silently fall back to the default zone's rules. If firewall-cmd --zone=<zone> --list-all shows an empty interfaces: line, fix the binding through NetworkManager rather than firewalld:

nmcli connection modify "External eth0" connection.zone external

Troubleshooting checklist

  • sysctl net.ipv4.ip_forward — is forwarding actually on?
  • iptables -L FORWARD -v -n — are packets hitting ACCEPT, or getting dropped/rejected by an earlier rule? (-v shows per-rule packet counters — a rule with 0 packets matched is never being hit)
  • iptables -t nat -L -v -n — is MASQUERADE/SNAT actually being applied?
  • Interface assigned to the correct firewalld zone (RHEL-family)?
  • Default policy: iptables -L | head -3 — an ACCEPT default policy on FORWARD will mask a missing rule; explicit rules are safer for anything internet-facing.

Cheat sheet

iptables -L -v -n                                              # list filter rules
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE            # NAT outbound
iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT                   # allow forwarding
iptables -t nat -A PREROUTING -p tcp --dport 8080 \
  -j DNAT --to-destination 192.168.1.10:80                      # port forward inbound
netfilter-persistent save                                       # persist rules (Debian/Ubuntu)