iptables Deep Dive¶
What is iptables?¶
iptables is the traditional userspace tool for configuring netfilter. Despite nftables being the modern replacement, iptables remains widely used and is essential to understand because:
- Docker uses iptables
- libvirt uses iptables
- UFW generates iptables rules
- Most documentation references iptables
Command Structure¶
Tables (-t)¶
| Table | Default | Purpose |
|---|---|---|
| filter | Yes | Packet filtering |
| nat | No | Address translation |
| mangle | No | Packet modification |
| raw | No | Connection tracking bypass |
Commands¶
| Command | Action |
|---|---|
| -A | Append rule to chain |
| -I | Insert rule at position |
| -D | Delete rule |
| -R | Replace rule |
| -L | List rules |
| -F | Flush (delete all rules) |
| -Z | Zero counters |
| -N | Create new chain |
| -X | Delete chain |
| -P | Set chain policy |
Basic Examples¶
# List all rules with line numbers
iptables -L -n -v --line-numbers
# List specific table
iptables -t nat -L -n -v
# Append rule
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Insert at position 1
iptables -I INPUT 1 -p tcp --dport 80 -j ACCEPT
# Delete by specification
iptables -D INPUT -p tcp --dport 22 -j ACCEPT
# Delete by line number
iptables -D INPUT 3
# Set default policy
iptables -P INPUT DROP
Match Extensions¶
Protocol Matches¶
# TCP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --sport 1024: -j ACCEPT
iptables -A INPUT -p tcp --tcp-flags SYN,ACK SYN -j DROP
# UDP
iptables -A INPUT -p udp --dport 53 -j ACCEPT
# ICMP
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
State Matching (conntrack)¶
# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Only new connections to SSH
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
# Drop invalid packets
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
Source/Destination¶
# By IP
iptables -A INPUT -s 192.168.1.100 -j ACCEPT
iptables -A INPUT -d 10.0.0.0/8 -j DROP
# By interface
iptables -A INPUT -i eth0 -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Negation
iptables -A INPUT ! -s 192.168.1.0/24 -j DROP
Multiport¶
# Multiple ports
iptables -A INPUT -p tcp -m multiport --dports 80,443,8080 -j ACCEPT
# Port ranges
iptables -A INPUT -p tcp -m multiport --dports 6000:6100 -j ACCEPT
IP Ranges¶
MAC Address¶
Time-Based¶
# Only during business hours
iptables -A INPUT -p tcp --dport 22 -m time \
--timestart 09:00 --timestop 17:00 \
--weekdays Mon,Tue,Wed,Thu,Fri -j ACCEPT
Rate Limiting¶
# Limit new SSH connections
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m limit --limit 3/minute --limit-burst 3 -j ACCEPT
# Using hashlimit per source IP
iptables -A INPUT -p tcp --dport 80 -m hashlimit \
--hashlimit-name http \
--hashlimit-mode srcip \
--hashlimit-above 100/sec \
--hashlimit-burst 500 -j DROP
Recent Module¶
# Track recent connections
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --set --name SSH
# Drop if too many recent attempts
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP
String Matching¶
# Block requests containing pattern
iptables -A INPUT -p tcp --dport 80 \
-m string --string "malicious" --algo bm -j DROP
Comment¶
NAT Operations¶
Source NAT (SNAT)¶
# Static SNAT
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 \
-j SNAT --to-source 203.0.113.1
# MASQUERADE (dynamic SNAT)
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
Destination NAT (DNAT)¶
# Port forwarding
iptables -t nat -A PREROUTING -p tcp --dport 8080 \
-j DNAT --to-destination 192.168.1.10:80
# With interface restriction
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 2222 \
-j DNAT --to-destination 192.168.1.10:22
Redirect¶
# Redirect to local port
iptables -t nat -A PREROUTING -p tcp --dport 80 \
-j REDIRECT --to-ports 8080
Hairpin NAT¶
When internal clients access internal services via external IP:
# Enable hairpin NAT
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -d 192.168.1.10 \
-p tcp --dport 80 -j MASQUERADE
Practical Configurations¶
Basic Firewall¶
#!/bin/bash
# Flush existing rules
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
# Set default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Drop invalid packets
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# Allow SSH
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
# Allow ICMP (ping)
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
# Log dropped packets
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables-dropped: "
NAT Router¶
#!/bin/bash
# Enable forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
# Flush rules
iptables -F
iptables -t nat -F
# Default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# NAT for internal network
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
# Allow forwarding for established
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow internal to external
iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
# Allow specific ports from external
iptables -A FORWARD -i eth0 -o eth1 -p tcp --dport 80 -j ACCEPT
Docker-Aware Configuration¶
#!/bin/bash
# IMPORTANT: Run after Docker starts
# Allow Docker bridge
iptables -A INPUT -i docker0 -j ACCEPT
# Allow forwarding for Docker
iptables -A FORWARD -i docker0 -j ACCEPT
iptables -A FORWARD -o docker0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Block external access to unpublished ports
# Docker handles published ports via DOCKER chain
Viewing and Debugging¶
List Rules¶
# Basic list
iptables -L
# With packet counts
iptables -L -v
# With line numbers
iptables -L --line-numbers
# Numeric (no DNS lookups)
iptables -L -n
# All tables
iptables -L -n -v
iptables -t nat -L -n -v
iptables -t mangle -L -n -v
iptables -t raw -L -n -v
Save and Restore¶
# Save current rules
iptables-save > /etc/iptables/rules.v4
# Restore rules
iptables-restore < /etc/iptables/rules.v4
# View saved rules (good for debugging)
iptables-save
Packet Tracing¶
# Enable tracing for specific traffic
iptables -t raw -A PREROUTING -p tcp --dport 80 -j TRACE
iptables -t raw -A OUTPUT -p tcp --dport 80 -j TRACE
# View trace in kernel log
dmesg -w | grep TRACE
# Or with nfnetlink_log
modprobe nfnetlink_log
Logging¶
# Log before dropping
iptables -A INPUT -j LOG --log-prefix "INPUT-DROP: " --log-level 4
iptables -A INPUT -j DROP
# Rate-limited logging
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "DROPPED: "
# View logs
journalctl -k | grep INPUT-DROP
Rule Ordering¶
Order matters! Rules are processed top to bottom:
# WRONG: First rule matches everything
iptables -A INPUT -j DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT # Never reached
# CORRECT: Specific rules first
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -j DROP
Optimization¶
Put frequently matched rules early:
# High-traffic established connections first
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# Then specific services
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Default deny last
iptables -A INPUT -j DROP
iptables vs Docker/libvirt¶
Chain Priority¶
Both Docker and libvirt insert their chains early:
# Docker adds:
-A FORWARD -j DOCKER-USER # User customization
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -j DOCKER
# libvirt adds:
-A FORWARD -j LIBVIRT_FWI
-A FORWARD -j LIBVIRT_FWO
-A FORWARD -j LIBVIRT_FWX
DOCKER-USER Chain¶
The only safe place for custom Docker rules:
# Block external access to container
iptables -I DOCKER-USER -i eth0 -p tcp --dport 8080 -j DROP
# Allow only from specific network
iptables -I DOCKER-USER -i eth0 -s 192.168.1.0/24 -j RETURN
iptables -I DOCKER-USER -i eth0 -j DROP
Persistence¶
Using iptables-persistent¶
# Install
sudo apt install iptables-persistent
# Save current rules
sudo netfilter-persistent save
# Rules are stored in:
# /etc/iptables/rules.v4
# /etc/iptables/rules.v6
Manual systemd Service¶
# /etc/systemd/system/iptables-restore.service
[Unit]
Description=Restore iptables rules
Before=network-pre.target
Wants=network-pre.target
[Service]
Type=oneshot
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Interaction with Docker¶
Warning
Docker recreates its rules on restart, potentially overwriting your rules.
Ensure your rules go in DOCKER-USER or run your script after Docker starts.
IPv6 with ip6tables¶
Separate commands for IPv6:
# List IPv6 rules
ip6tables -L -n -v
# Add IPv6 rule
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
# Save IPv6 rules
ip6tables-save > /etc/iptables/rules.v6