第37章:iptables 底层配置

第三十七章:iptables 底层配置

iptables 是 Linux 内核包过滤框架(netfilter)的命令行工具,也是 UFW、firewalld 这些"自动挡"背后真正干活的东西。理解它,你才能看懂 ufw status 生成的规则、明白 Docker 为什么能绕过 UFW。

iptables 的语法看着吓人,但它其实就是一个固定套路:在"哪张表"的"哪条链"上,匹配"什么条件",然后执行"什么动作"。把"四表五链"这张地图记住,剩下的都是查手册。

⚠️ 动手前先记住两件事:

  1. 改错了会把自己锁在服务器外面(尤其远程改 SSH 相关规则时);
  2. iptables 规则默认重启就丢,必须做持久化(见 37.7)。

37.1 iptables 与 nftables

iptablesnftables
出现时间Linux 2.4(2001 年)Linux 3.13(2014 年)
规则结构固定的表 + 链一个统一的规则集,可自定义表链
处理多个规则内核里逐条线性匹配,规则多时变慢支持集合和映射,一次匹配一批
现状维护中,仍是绝大多数文档的写法RHEL 9、Debian 12 等新系统的默认

而在现代发行版上,你敲的 iptables 很可能已经是 iptables-nft——它能理解 iptables 语法,但实际把规则翻译成 nftables 存起来:

1
2
3
4
5
6
7
8
# 看自己用的是哪个实现
iptables --version
# iptables v1.8.7 (nf_tables)     ← 翻译成 nftables
# iptables v1.8.7 (legacy)        ← 老式实现

# 同一批规则,用两种方式看
sudo iptables -S
sudo nft list ruleset | head -40

结论:iptables 语法值得学(存量系统和文档太多),但新项目不要用它手写复杂规则——用 firewalld/nftables,或者 ufw。

37.2 数据包是怎么走过来的

netfilter 在内核协议栈里埋了几个"检查点",数据包每经过一个检查点,就会按顺序去查对应的规则。

graph LR
    NIC["网卡收到数据包"] --> PRE["PREROUTING<br>路由前"]
    PRE --> ROUTE{"路由决策<br>目标是谁?"}
    ROUTE -->|"目标是本机"| IN["INPUT"]
    ROUTE -->|"目标是别的机器"| FWD["FORWARD"]
    ROUTE -->|"本机进程发出的包"| OUT["OUTPUT"]
    IN --> APP["交给本机进程"]
    FWD --> POST["POSTROUTING<br>路由后"]
    OUT --> POST
    POST --> NICOUT["从网卡发出"]

三条路径记牢:

  • 发给本机:PREROUTING → INPUT → 本机进程;
  • 经本机转发:PREROUTING → FORWARD → POSTROUTING;
  • 本机发出:OUTPUT → POSTROUTING。

37.2.1 四张表(其实是五张)

表干什么常用链
filter过滤数据包(放行/丢弃),默认表INPUT、FORWARD、OUTPUT
nat地址转换(SNAT/DNAT/端口映射)PREROUTING、POSTROUTING、OUTPUT
mangle修改包头(TTL、TOS、打标记)全部五条链
raw让某些包跳过连接跟踪PREROUTING、OUTPUT
security配合 SELinux 打安全标记INPUT、FORWARD、OUTPUT

日常 95% 的操作都在 filter 表,其次的 5% 在 nat 表。mangle、raw、security 除非有明确需求,否则不用碰。

1
2
3
# 不指定 -t 就是 filter 表,这两条等价
sudo iptables -L -n
sudo iptables -t filter -L -n

37.2.2 五条链

链处理什么
INPUT目标是本机的包
OUTPUT本机进程发出的包
FORWARD只是路过、要转给别人的包
PREROUTING包刚进网卡、还没做路由判断时
POSTROUTING路由判断完、即将离开网卡时

为什么要分 PREROUTING 和 POSTROUTING? 因为 DNAT(改目标地址)必须在路由判断之前做,否则内核不知道往哪转;SNAT(改源地址)必须在路由判断之后做,否则改完会影响路由选择。位置是固定的,也解释了为什么端口映射写在 PREROUTING、共享上网写在 POSTROUTING。

FORWARD 链什么时候会用上?

  • 本机当路由器/网关,连接内外网;
  • Docker/K8s 宿主机转发容器的流量;
  • VPN 服务器转发客户端的流量;
  • 端口映射(DNAT)之后,包也要经过 FORWARD 才转得出去——这是新手最容易漏的一步:DNAT 写对了却不通,十有八九是 FORWARD 默认策略把包丢了。

37.3 命令语法

1
iptables -t 表名 操作 链名 匹配条件 -j 动作

-t 不写就是 filter 表。最常用的"操作":

参数含义
-A 链追加到链末尾
-I 链 [编号]插入到链首(或指定编号位置)
-D 链 [编号]按内容或编号删除
-R 链 编号替换某条规则
-F [链]清空规则(不改默认策略)
-P 链 动作设置链的默认策略
-L -n -v --line-numbers查看规则(-n 不做 DNS 反解,快得多)
-S用"可执行命令"的形式打印规则,适合复制备份
1
2
# 查看带编号的规则(编号用于 -D / -R)
sudo iptables -L INPUT -n --line-numbers -v
1
2
3
4
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source        destination
1       12   720 ACCEPT     tcp  --  *      *       0.0.0.0/0     0.0.0.0/0     tcp dpt:22
2        0     0 DROP       tcp  --  *      *       0.0.0.0/0     0.0.0.0/0     tcp dpt:80

pkts 和 bytes 两列非常有用:规则明明写了却没生效,先看计数器是不是 0——是 0 就说明包根本没走到这条规则(多半被前面的规则拦了)。

37.4 匹配条件

37.4.1 基础匹配

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 协议
sudo iptables -A INPUT -p tcp -j ACCEPT
sudo iptables -A INPUT -p udp -j ACCEPT
sudo iptables -A INPUT -p icmp -j ACCEPT

# 端口(必须配合 -p tcp 或 -p udp)
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
sudo iptables -A INPUT -p tcp --sport 1024:65535 -j ACCEPT    # 源端口范围
sudo iptables -A INPUT -p tcp --dport 8000:9000 -j ACCEPT     # 目标端口范围

# 地址
sudo iptables -A INPUT -s 192.168.1.0/24 -j ACCEPT            # 来源网段
sudo iptables -A INPUT -d 10.0.0.1 -j ACCEPT                  # 目标地址
sudo iptables -A INPUT -s 10.0.0.50 -j DROP                   # 封禁单个 IP

# 网卡
sudo iptables -A INPUT -i eth0 -j ACCEPT                      # 入站网卡
sudo iptables -A OUTPUT -o eth0 -j ACCEPT                     # 出站网卡

37.4.2 扩展模块(-m):真正干活的在这里

单靠上面几个条件写不出可用的防火墙,必须用 -m 加载扩展模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1) conntrack:连接状态(最关键,务必理解)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT

# 2) multiport:一条规则写多个不连续的端口
sudo iptables -A INPUT -p tcp -m multiport --dports 80,443,8080 -j ACCEPT

# 3) iprange:匹配任意 IP 区间(网段写法表达不了的)
sudo iptables -A INPUT -m iprange --src-range 192.168.1.10-192.168.1.50 -j ACCEPT

# 4) limit:限速,防刷
sudo iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/second -j ACCEPT

# 5) recent:记录"最近来过的 IP",用来做简易封禁
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set --name SSH
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP

# 6) comment:给规则写备注,半年后还能看懂
sudo iptables -A INPUT -p tcp --dport 3306 -m comment --comment "MySQL 仅内网" -s 10.0.0.0/8 -j ACCEPT

# 7) mac:按来源 MAC 过滤(只能用于同一二层网络)
sudo iptables -A INPUT -m mac --mac-source 00:11:22:33:44:55 -j ACCEPT

--ctstate 的常用值:

状态含义
NEW发起一个新连接(第一个包)
ESTABLISHED属于已建立连接的后续包
RELATED与已有连接相关的新连接(如 FTP 数据连接、ICMP 错误)
INVALID状态异常,通常直接丢

-m state --state ... 是老写法,现在官方推荐 -m conntrack --ctstate ...,功能一致。但无论哪种,那条 ESTABLISHED,RELATED 的放行规则都是整套配置的基石——忘了它,你会遇到"新连接进得来、回包出不去"的诡异现象。

37.5 动作

动作含义
ACCEPT放行,不再往下匹配
DROP直接丢弃,不回复任何信息(对方等到超时)
REJECT拒绝并回复 ICMP/TCP 拒绝包(对方立刻知道被拒)
LOG记录到内核日志,然后继续往下匹配(不终止)
RETURN从自定义链返回上一级链
-j 自定义链名跳到自定义链继续匹配
1
2
3
4
5
6
7
8
9
# DROP:像石沉大海,扫描器更难判断你是否存在
sudo iptables -A INPUT -s 10.0.0.50 -j DROP

# REJECT:立刻回"端口不可达",调试方便,但会暴露主机存在
sudo iptables -A INPUT -p tcp --dport 80 -j REJECT --reject-with icmp-port-unreachable

# LOG:只记录,不拦截
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
     -j LOG --log-prefix "[SSH-ATTEMPT] " --log-level warning

⚠️ LOG 动作不会终止匹配。想"既记录又拦截"必须写成两条规则,而且顺序不能反:

1
2
sudo iptables -A INPUT -p tcp --dport 23 -j LOG --log-prefix "[TELNET] "
sudo iptables -A INPUT -p tcp --dport 23 -j DROP

一旦 DROP 写在前面,包已经被丢掉,后面的 LOG 永远不会被执行。另外日志写得太频繁会把自己的磁盘写满,给 LOG 规则加上 -m limit --limit 5/minute 是个好习惯。

DROP 与 REJECT 怎么选:

  • 对外网用 DROP:不给扫描器任何回应,减少信息泄露;
  • 对内网/调试期用 REJECT:立刻报错,方便排查(不用傻等超时)。

37.6 一套可以真正使用的配置

把前面所有零件拼起来。注意顺序:先放行必要流量,最后再改默认策略,中途任何一步错了都还有回旋余地。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/bash
# 基础防火墙脚本(简化版,生产请按需增删)
set -e

# ---- 1. 先确认 SSH 端口会放行(改之前一定要确认!)----
SSH_PORT=22

# ---- 2. 清空旧规则(-F 只清规则,不动默认策略)----
iptables -F
iptables -X            # 删除自定义链

# ---- 3. 默认策略:先设成"宽进",避免把自己关在外面 ----
iptables -P INPUT ACCEPT
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# ---- 4. 本机回环必须放行(很多服务靠它通信)----
iptables -A INPUT -i lo -j ACCEPT

# ---- 5. 已建立的连接和回包要放行(整套规则的地基)----
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# ---- 6. 丢弃状态异常的包 ----
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

# ---- 7. 放行需要的服务 ----
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/second -j ACCEPT
iptables -A INPUT -p tcp --dport "$SSH_PORT" -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dports 80,443 -m conntrack --ctstate NEW -j ACCEPT

# ---- 8. 最后才把 INPUT 默认策略改成 DROP ----
iptables -P INPUT DROP

# ---- 9. 看看结果 ----
iptables -L -n -v --line-numbers

三条必须理解的要点:

  1. ESTABLISHED,RELATED 那条规则是地基。默认 DROP 之后,如果没有它,你发出的请求的回包也会被丢弃,表现为"能发包、收不到响应"。
  2. 顺序就是一切。iptables 从上往下匹配,命中 ACCEPT/DROP/REJECT 就结束。所以"放行特定端口"必须写在"默认拒绝"生效之前。
  3. 默认策略最后改。先放行 SSH 再 -P INPUT DROP,即使写漏了别的规则,你至少还能连上去修。

37.6.1 远程改规则时的"自锁保险"

这是远程运维 iptables 最重要的一条经验:给自己留一条退路。方法是在后台挂一个定时任务,几分钟后自动恢复成一份"已知可用的规则":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. 先把当前可用的规则存下来
sudo iptables-save > /root/iptables.working

# 2. 起一个 5 分钟后自动恢复的后台任务
sudo nohup bash -c 'sleep 300; iptables-restore < /root/iptables.working' >/dev/null 2>&1 &

# 3. 现在可以放心大胆地改规则(5 分钟内)
#    改完自己新开一个终端验证 SSH 还能不能连

# 4. 确认没问题后,干掉那个定时恢复的进程,别让它把你的新规则覆盖回去
sudo pkill -f 'iptables-restore'

如果 5 分钟后把自己锁了,那个后台任务会把规则恢复回来,你重新连上即可。云服务器还可以借助控制台的 VNC/串口,但别把希望都寄托在它身上。

37.7 NAT:地址转换

NAT 在 nat 表里,用于"改地址"。两种主要场景:共享上网(SNAT/MASQUERADE)和端口映射(DNAT)。

37.7.1 前提:打开 IP 转发

任何涉及转发的配置都要求内核允许转发,否则包会在路由那步被丢掉:

1
2
3
4
5
6
# 临时生效(重启失效)
sudo sysctl -w net.ipv4.ip_forward=1

# 永久生效(推荐写在 /etc/sysctl.d/ 下,别直接改 /etc/sysctl.conf)
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-ipforward.conf
sudo sysctl --system

37.7.2 SNAT:共享上网(出口有固定公网 IP)

1
2
3
4
5
6
# 内网 192.168.1.0/24 的机器,经本机上网时把源地址改成 1.2.3.4
sudo iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j SNAT --to-source 1.2.3.4

# 对应的 FORWARD 放行(新连接出去 + 回包进来)
sudo iptables -A FORWARD -i eth1 -o eth0 -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o eth1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

注意 FORWARD 的两条规则方向不同:出去的那条要放行 NEW,回来的那条只需要 ESTABLISHED,RELATED。把两边都写成 NEW 是常见错误(虽然不太危险),把回来那条也要求 NEW 则会导致连接完全不通。

37.7.3 MASQUERADE:共享上网(出口 IP 会变)

出口是拨号/DHCP 拿到的动态 IP 时,用 MASQUERADE,它会自动取当前出口网卡的 IP:

1
sudo iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE

差别只有一个:SNAT 要写死 IP,MASQUERADE 自动查。代价是它每次都要去问网卡地址,性能略低——有固定 IP 就用 SNAT。

37.7.4 DNAT:端口映射(把外网端口转到内网机器)

1
2
3
4
5
6
7
# 把本机 80 端口收到的请求,转发到内网 192.168.1.100 的 8080 端口
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 \
     -j DNAT --to-destination 192.168.1.100:8080

# ⚠️ 别忘了 FORWARD:DNAT 只改了目标地址,包还得被"允许转出去"
sudo iptables -A FORWARD -p tcp -d 192.168.1.100 --dport 8080 \
     -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT

DNAT 写了却不通,90% 是漏了 FORWARD 那条规则(或者 FORWARD 默认策略是 DROP)。

如果内网机器需要"从内网访问公网 IP 也能转回来"(NAT 回流/hairpin),还要对本机出去的流量也做一次 DNAT:

1
2
3
# 内网访问公网 IP 时的回环处理(场景:内网用域名访问自己的服务)
sudo iptables -t nat -A OUTPUT -p tcp -d 1.2.3.4 --dport 80 \
     -j DNAT --to-destination 192.168.1.100:8080

⚠️ 纯 iptables 的 NAT 规则重启就丢,而且新手很容易在这里把自己写进死胡同。如果只是想做端口映射,用 firewalld --add-forward-port 或 nftables 会更省心。

37.8 保存规则:不做这一步,重启就白干

内存里的 iptables 规则在重启后全部消失,必须显式保存。

1
2
3
4
5
6
# 手工备份/恢复
sudo iptables-save > /root/iptables.rules
sudo iptables-restore < /root/iptables.rules

# 确认文件内容(这是一份可以直接复用的文本)
cat /root/iptables.rules
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
COMMIT

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
COMMIT

按发行版选择持久化方案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Debian / Ubuntu:用 netfilter-persistent
sudo apt install iptables-persistent        # 安装时会问你要不要保存当前规则
sudo netfilter-persistent save              # 存到 /etc/iptables/rules.v4
sudo netfilter-persistent reload            # 重新加载
sudo systemctl enable netfilter-persistent  # 开机自动加载

# Ubuntu 的 ufw 本身也是"持久化"的:
sudo ufw enable                             # 开机自动启用,规则存在 /etc/ufw/

# RHEL 7 / CentOS 7:用 iptables-services
sudo yum install iptables-services
sudo service iptables save                  # 存到 /etc/sysconfig/iptables
sudo systemctl enable iptables

# RHEL 8+ / CentOS Stream / Rocky 9:官方已不推荐 iptables-services,
# 请改用 firewalld(第 36 章)或 nftables,把规则写进 /etc/nftables.conf

⚠️ 不要用 /etc/network/interfaces 里写 pre-up iptables-restore 这种老办法——ifupdown 在现代 Ubuntu 上已经被 netplan 接管,写了不生效。统一用上面对应的持久化服务。

37.9 排错清单

  1. 规则没生效? 先看 iptables -L -n -v 里那条规则的 pkts 计数:计数为 0 说明包没走到这条规则(顺序问题),计数在涨说明规则生效但动作不对。
  2. 忘了 ESTABLISHED,RELATED? 症状是"新连接能出去,但收不到回包"或"SSH 能连上,一会儿就卡住"。
  3. LOG 写在 DROP 后面? 永远不会有日志。LOG 必须在前面。
  4. 顺序反了? 例如 -A INPUT -j DROP 写在放行规则之前,后面写再多也没用。要插到前面就用 -I。
  5. DNAT 不通? 检查 FORWARD 链和 net.ipv4.ip_forward,两个都要对。
  6. 重启后规则没了? 没做持久化,见 37.8。
  7. 装了 Docker 就"漏"了? Docker 会自己往 nat/filter 里插 DOCKER 链,且位置在 iptables 自定义规则之前。明文写 iptables -A INPUT -j DROP 通常挡不住已发布到 0.0.0.0 的容器端口。改用 -p 127.0.0.1:8080:80 或 ufw-docker,并在别的机器上实测。
  8. 被自己锁在外面了? 用云控制台/机房 KVM 进去,iptables -F,然后按 37.6.1 的方法给自己留保险再改。

本章小结

知识点要点
定位iptables 是 netfilter 的命令行配置工具;现代系统上通常由 iptables-nft 翻译给 nftables
五张表filter(过滤,默认)、nat(地址转换)、mangle(改包头)、raw(跳过连接跟踪)、security(SELinux)
五条链INPUT、OUTPUT、FORWARD、PREROUTING、POSTROUTING
包的路径本机→INPUT;转发→FORWARD;发出→OUTPUT;进出网卡分别经过 PREROUTING / POSTROUTING
语法iptables -t 表 -A 链 匹配 -j 动作;顺序至上,命中终止动作即结束
常用匹配-p、-s/-d、--dport、-i/-o、-m conntrack、-m multiport、-m limit、-m recent
动作ACCEPT、DROP、REJECT、LOG(不终止)、RETURN、跳自定义链
地基规则-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
NATSNAT(固定出口 IP)、MASQUERADE(动态 IP)、DNAT(端口映射,别忘了 FORWARD + ip_forward)
持久化iptables-save/iptables-restore + netfilter-persistent(Debian)或 iptables-services(RHEL 7)
安全操作先放行、后改默认策略;远程改规则前留"自动恢复"的后路

iptables 是这套体系里的"内功":理解了表、链、状态和顺序,你就能看懂 UFW 生成的规则、明白 firewalld 在背后做了什么,也知道为什么 Docker 能让你的防火墙"形同虚设"。

下一章预告:第三十八章进入系统安全加固——账号与登录安全、服务最小化、文件权限审计、SELinux/AppArmor、安全基线检查。

最后修改 September 19, 2026: 更新 (3489033b1)