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. Related front ends: firewalld, ufw, nftables.
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 zones over direct rules where possible: they are easier to audit and survive reloads cleanly. Zone management, rich rules, forwarding, and masquerading are covered in the firewalld guide. One gotcha worth repeating here: an interface under NetworkManager control that is not bound to the intended zone silently falls back to the default zone. Fix it in NetworkManager (nmcli connection modify "<conn>" connection.zone external), not only in firewalld.
Common rule recipes¶
# Open web ports
sudo iptables -A INPUT -p tcp -m conntrack --ctstate NEW --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp -m conntrack --ctstate NEW --dport 443 -j ACCEPT
# Block one address (both directions)
sudo iptables -A INPUT -s 198.51.100.7 -j DROP
sudo iptables -A OUTPUT -d 198.51.100.7 -j DROP
# Delete a rule: repeat the add command with -D instead of -A
sudo iptables -D INPUT -s 198.51.100.7 -j DROP
# Allow one source to one port on one interface (protocol must precede the port match)
sudo iptables -A INPUT -p tcp -i eth1 --dport 10080 -s 192.168.31.13 -j ACCEPT
# Time-window restriction with logging (evening lockout for one client)
sudo iptables -A OUTPUT -p tcp -s 192.168.1.130 -m time --weekdays Su,Mo,Tu,We,Th \
--timestart 20:30 --timestop 23:59 -j LOG --log-prefix "Restricted Internet "
sudo iptables -A OUTPUT -p tcp -s 192.168.1.130 -m time --weekdays Su,Mo,Tu,We,Th \
--timestart 20:30 --timestop 23:59 -j DROP
-m state --state (used elsewhere on this page) is the older spelling of -m conntrack --ctstate; both work, conntrack is preferred.
Blocking many addresses this way creates one rule per address, and every packet walks the list. For more than a few dozen, use an ipset.
Default-deny host firewall¶
#!/bin/bash
iptables -F # start clean
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp -s 192.168.1.0/24 --dport 22 -j ACCEPT # ssh from the LAN only
Setting a DROP policy on a remote machine before the ESTABLISHED and SSH rules are in place cuts your own session. Order the script so the accepts come first, or run it under at now + 5 minutes with a rollback.
Custom chains: isolating a service's traffic¶
Keeping one concern in its own chain makes it easy to audit and to flush independently. Example: only two cluster nodes may exchange Corosync (UDP) traffic on a subnet.
# On node 1 (peer is 10.0.0.142); mirror the addresses on node 2
sudo iptables -N COROSYNC
sudo iptables -A INPUT -j COROSYNC
sudo iptables -A COROSYNC -p udp -s 10.0.0.142 -j ACCEPT
sudo iptables -A COROSYNC -p udp -s 10.0.0.0/24 -j DROP
sudo iptables -L COROSYNC -n
Logging¶
# Log then drop (LOG does not terminate, so the next rule still applies)
sudo iptables -A INPUT -s 198.51.100.7 -j LOG --log-prefix "BLOCKED: " --log-level 4
sudo iptables -A INPUT -s 198.51.100.7 -j DROP
# Log every new connection (noisy: for short debugging sessions only)
sudo iptables -I INPUT -m conntrack --ctstate NEW -j LOG --log-prefix "New Connection: "
Messages go to the kernel log. On systemd systems read them with journalctl -k | grep BLOCKED; to send them to a dedicated file, add an rsyslog rule such as :msg, contains, "BLOCKED: " /var/log/iptables.log. If nothing is logged at all, check that the kernel log daemon or journald is actually receiving kernel messages.
Applying rules from a list of addresses at boot¶
Keep addresses in a file (one per line, # for comments) and load them from a script that runs at boot (a systemd unit or netfilter-persistent hook):
BLOCKIPS=/etc/blocked-ips.conf
for ip in $(grep -Ev '^\s*(#|$)' "$BLOCKIPS"); do
iptables -A INPUT -s "$ip" -j DROP
iptables -A OUTPUT -d "$ip" -j DROP
done
Load and save by hand¶
sudo iptables-save > /root/firewall.rules
sudo iptables-restore < /root/firewall.rules
Further reading¶
- How to forward ports through a Linux gateway with iptables
- Connection tracking (LWN)
- Related: nftables, ipset, firewalld, ufw
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? (-vshows 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)