你有没有遇到过这种场面:辛辛苦苦写好了三条 iptables 规则,加完之后还特意iptables -L看了一眼,规则明明就在那儿,可客户端那边死活连不上。这种"规则写得没毛病但就是不生效"的案子,在我处理过的网络故障里能排进前三。很多人第一反应是删掉规则重写,可真正的问题往往不在规则内容本身,而在观察规则的方式。
这篇文章想讲的,就是一套我从实战里沉淀下来的 iptables 规则调试方法。核心思路很简单:先用计数器判断规则到底有没有被命中,再用 LOG 规则追踪包的决策路径,最后用 tcpdump 把包层面的证据和 Netfilter 的决策对齐。无论你是在云服务器上做端口转发,还是在公司内部网关维护整套防火墙策略,这套思路都能把"玄学排障"变成"按图索骥"。文章适合所有需要维护 Linux 防火墙的运维、后端开发和网络爱好者,我会尽量把每个判断依据和坑都讲透。
1. 先分清问题归属:规则没加载、被覆盖,还是流量根本没走到防火墙
很多人一上来就盯着规则内容看,这其实是最低效的起点。iptables 规则不生效,先要分清到底是三层问题里的哪一层:规则没写进去、规则被其他规则抢先处理了、还是流量压根就没有经过这条链。这三类问题的排查手段完全不同。
1.1 确认规则真的在正确的位置
先别急着改规则,用iptables -S看完整规则集。注意我特意不说iptables -L,因为-L默认只展示 filter 表的内容,而且默认会把 IP 解析成域名、端口解析成服务名,看起来"好像没问题"实际上很容易被误导。-S输出的是规则保存时的原始格式,比如:
iptables -S iptables -t nat -S iptables -t mangle -S iptables -t raw -S排查时务必把四个表都过一遍。我看到过不止一次,有人把 DNAT 规则加到了 filter 表,或者把转发放行规则写到了 INPUT 链,规则确实保存了,但语义完全不对。另外,如果你用的是支持iptables-nft的发行版(现在大部分新版系统默认就是),iptables -S显示的仍然是一套兼容的表示,但底层可能混着 nftables 的原生规则。如果服务器上有人直接用 nft 命令定义过规则,建议顺手nft list ruleset看一眼,否则你可能漏掉别人直接写在 nft 层的拦截项。
1.2 规则顺序与默认策略:存在不等于有机会执行
iptables 的表和链都是顺序匹配,这一点新手最容易忽略。假设 FORWARD 链里已经有一条-A FORWARD -j DROP兜底规则,你又用-A在链尾追加了一条-A FORWARD -p tcp --dport 8080 -j ACCEPT,那这条放行规则永远没有执行机会,因为包在前面就被 DROP 吃掉了。
用iptables -L FORWARD -n -v --line-numbers可以清楚看到每条规则的编号顺序。我自己的习惯是排查时先看链尾有没有大范围的 DROP 策略,再看业务放行规则是不是排在 DROP 后面。很多时候问题根本不是规则写错,而是规则插的位置不对。
1.3 流量路径判断:这条链本来就不该处理这个包
第三个维度最隐蔽。iptables 的 INPUT、FORWARD、OUTPUT 链分别对应不同的流量路径,选错链,规则写了也是白写。我见过最典型的几个场景:
| 流量场景 | 实际经过的链 | 容易踩的坑 |
|---|---|---|
| 外部访问本机端口 | PREROUTING → INPUT | 把入站规则写到 FORWARD |
| 本机转发到内网/其他主机 | PREROUTING → FORWARD → POSTROUTING | 只写了 NAT 规则,没放行 FORWARD |
| 本机进程访问外部 | OUTPUT → POSTROUTING | 在 INPUT 里限制外部访问却拦不住出站 |
| 本机访问自己(回环) | 走 lo 的 INPUT/OUTPUT | 规则限定在 eth0,curl 127.0.0.1 却通了 |
还有两个非常容易误导的场景。一个是网桥:如果流量经过 Linux bridge 转发,它走的是 FORWARD 链而不是 PREROUTING 后的本机入站,而且很多 bridge 相关的 iptables 规则挂在physdev模块下。另一个是 Docker 等容器环境:容器在独立的网络命名空间里,容器内的 iptables 规则和宿主机是两套系统,宿主机上写的规则对容器内的进程不生效,需要先确认你到底该在哪个 netns 里排查。这些判断不需要猜,tcpdump配合下面的计数器方法能很快确认流量实际路径。
2. 计数器是第一诊断工具:让每条规则自己汇报命中情况
iptables 每条规则都自带两个计数器,分别统计匹配的数据包数量和字节数。这不是鸡肋功能,它是定位问题的第一步。很多"规则不生效"的谜题,看一眼计数器基本就有答案了。
2.1 怎么看计数器
查看计数器要加-v参数,同时建议加-n避免 DNS 解析干扰:
iptables -L INPUT -v -n --line-numbers iptables -t nat -L PREROUTING -v -n --line-numbers iptables -L FORWARD -v -n --line-numbers输出里每一行前面的pkts和bytes就是该规则累积匹配的包数和字节数。刚开始排查时,先找出与你关注端口相关的规则,盯住它的计数器。
2.2 三种计数器状态分别说明什么
第一种情况:计数器始终是 0。这说明包根本没有到达这条规则。原因可能是前面的规则已经把它处理掉了,也可能是流量压根没有进入这张表或这条链,甚至包在更早的环节就被丢弃了。这时候不要继续盯着这条规则分析,要往上游找。
第二种情况:计数器在持续增长,但业务还是不通。这是最耐人寻味的。规则匹配上了,说明包确实走到了这里,但它执行的 action 被后续机制覆盖了。常见原因包括:规则在子链里执行了 ACCEPT,但子链结束后又被外层链的规则 DROP;或者规则只是 LOG/NOTRACK 之类的非终结动作,后面还有别的规则在等着;也可能包被 ACCEPT 之后,在 POSTROUTING 阶段又因为 NAT 或路由问题丢了。
第三种情况:计数器增长量级和你的预期对不上。比如你以为只有一台客户端在测试,计数器却像被压测了一样狂涨。这可能是因为你盯的规则太宽泛,比如没有限定端口或源地址,把所有流量都算进来了。此时该做的是收缩匹配条件,让计数器只统计你要观察的那一类流量。
2.3 构造一对一的测试流量,让计数器开口说话
为了不让其他流量干扰判断,我通常会构造一个特征非常明显的测试流量,然后单独盯住对应规则。比如在客户端上指定源端口发起连接:
# 客户端执行,指定源地址和源端口 nc -s 192.168.10.5 -p 33333 <服务器IP> 8080然后在服务器上刷新计数器,重点看有没有一条规则匹配了源 IP192.168.10.5、源端口33333的流量。如果日志或抓包工具能带上这个端口号,整个排查噪音会被压到最低。这条技巧在流量比较大的生产环境里尤其好用,普通 curl 测试产生的包混在几万条连接里,根本没办法用计数器精确定位。
2.4 清零计数器,给问题一个干净的起点
排查时我习惯先把关注链的计数器清零,然后重新跑一轮测试,这样数字变化一目了然。清零命令:
# 只清空某条链内所有计数器 iptables -Z INPUT # 只清空某条链的某条规则 iptables -Z INPUT 3注意iptables -Z不加参数会清空整个 filter 表的所有链计数器,生产环境用的时候要小心,最好带上链表名和规则号,缩小影响面。清零之后再做一次最小化测试,基本就能看到那条规则到底有没有被命中。
2.5 不要忽视 conntrack 状态表的配合
计数器回答的是"规则有没有匹配",conntrack 回答的是"连接处于什么状态"。尤其在FORWARD和NAT场景,很多问题其实是状态表异常导致的,比如连接跟踪表满了,新连接被丢弃;或者回程包的状态和预期不一致。排查时配合查看:
cat /proc/net/nf_conntrack # 或者用 conntrack 工具 conntrack -L如果看到大量 TCP 连接处于TIME_WAIT或异常状态,先把连接跟踪的问题解决,再回头看规则。顺序反过来容易把简单问题复杂化。
3. LOG 规则当探针:把每一步决策变成可读的内核日志
计数器能告诉你规则有没有被命中,但回答不了"一个包在整个链里到底经历了什么"。这时候就需要 LOG 规则。它的原理很简单:内核在匹配到 LOG 规则的瞬间,把包的关键信息通过 printk 写进内核日志,然后继续执行后面的规则。你可以把它理解成在防火墙决策路径上埋了一个个探针。
3.1 LOG 规则怎么写
先看一个基本的例子:
# 在 FORWARD 链最前面加一条 LOG,观察转发流量 iptables -I FORWARD 1 -p tcp --dport 8080 -j LOG --log-prefix "DBG-FWD-IN: " --log-level 4--log-prefix是日志前缀,最好写成一目了然的名字,方便 grep。--log-level对应内核日志级别,一般用 4(warning)就够了。关键点在于:LOG 是一个非终结 target,它记录完日志之后,包还会继续走下一条规则。所以你可以在一张链的头部和尾部各放一条 LOG,头部看的是一进链时的状态,尾部看的是前面所有规则都执行完、即将离开这条链时的状态。
3.2 日志去哪看
内核日志默认写到内核环形缓冲区,读取方式:
dmesg -T | grep DBG-FWD-IN # 或者实时跟踪 journalctl -k -f | grep DBG-FWD-IN如果系统里配置了 syslog 把 kern 级别的日志落盘,也可以直接tail -f /var/log/kern.log。注意 LOG 规则走的是内核日志通道,和 Nginx、Java 那类应用日志完全是两个体系,很多新手在/var/log/messages里找不到 LOG 输出就开始怀疑规则没生效,其实只是没找对地方。
3.3 生产环境一定要限流,否则日志风暴会教做人
不加限制的 LOG 规则在高流量链路上能瞬间刷爆内核日志缓冲区,极端情况下会拖慢整个系统的性能,甚至把磁盘写满。给 LOG 加上limit匹配是基本素养:
iptables -I FORWARD 1 -p tcp --dport 8080 -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "DBG-FWD-IN: " --log-level 4--limit 5/min表示每分钟最多记录 5 条,--limit-burst 10是允许的初始突发量。这样既能看到日志,又不会把系统打爆。调试完成后记得把 LOG 规则删掉,这东西留在生产环境就是一个定时炸弹。
3.4 更高级的追踪方式:nftables 的 trace 机制
如果你用的系统已经是 iptables-nft 后端(现在大多数新版发行版都是),Linux 还提供更细粒度的 trace 机制,能看到一个包在每个 hook 点的完整决策过程。用法是先给包打上追踪标记,再在另一个终端监听:
# 终端 A:开始监听追踪事件 nft monitor trace # 终端 B:给目标流量打标记 nft add rule inet filter INPUT tcp dport 8080 meta nftrace set 1执行之后你会看到类似这样的输出:
trace id 9f4b2c1a inet filter INPUT packet: iif "eth0" ... trace id 9f4b2c1a inet filter INPUT rule ... verdict continue trace id 9f4b2c1a inet filter INPUT rule ... verdict accept通过 trace id 可以把同一个包的前后决策关联起来,相当于把整个 Netfilter 的决策路径完整画出来了。这个功能排查复杂转发链时特别值钱,比逐个加 LOG 规则高效得多。
3.5 日志怎么解读:一条包多个探针
在链路首尾各加 LOG 规则之后,你会看到同一个连接产生了多条记录。如果"链首记录有、链尾记录没有",说明包在中间某条规则被终结了,要么被 ACCEPT 跳出了链,要么被 DROP 丢弃。如果两条记录都在,再看它们之间的其他 LOG 记录,就能精确锁定是哪一条规则做出了改变命运的决定。
4. tcpdump 交叉验证:把抓包点和 Netfilter 决策点对齐
日志和计数器反映的是 Netfilter 内部的决策,但网络问题经常是"包根本没到服务器"或"包被更上游的设备丢了"。要排除这些可能,必须用 tcpdump 从包层面拿到原始证据。但这里有个非常容易混淆的点:tcpdump 抓到的包,和 iptables 看到的包,在路径上并不是同一时刻的。
4.1 观测点差异:抓得到包不等于防火墙放行了
tcpdump 利用 AF_PACKET 套接字在协议栈很靠前的位置抓包,对入站流量来说,它在进入 Netfilter hook 之前就能看到包。所以当你看到"eth0 上明明抓到了 SYN,但 iptables 计数器纹丝不动"时,不要惊讶,这正是两个观测点不同步造成的。包被网卡驱动接收后,可能在到达 hook 之前就被丢掉了,也可能是你盯错了 hook。
比较实用的流程是先在物理网卡上确认包确实到了,再进协议栈内部确认包被怎么处理:
# 抓取发往本机 8080 端口的 SYN 包 tcpdump -nni eth0 'tcp port 8080 and (tcp[tcpflags] & tcp-syn != 0)' -c 10如果这个命令什么都抓不到,那问题基本不在 iptables,而在更上游:可能是路由不通、对端没发包、或者云平台上层网络策略把流量直接拦掉了。别急着怀疑自己的防火墙规则。
4.2 四条基本结论,帮你快速定位层次
根据 tcpdump 结果和 iptables 计数器的组合,可以做一个快速判断:
| tcpdump 表现 | iptables 计数器表现 | 问题层次 |
|---|---|---|
| 网卡抓不到 SYN | 任何计数器都不涨 | 上游网络/安全策略/路由问题 |
| 网卡有 SYN,INPUT 链计数不涨 | 相关规则为 0 | 包被 PREROUTING/raw 表处理掉,或走的是 FORWARD 路径 |
| INPUPT 链计数涨,但进程收不到 | 服务监听/应用层问题 | 检查 listen、backlog、SELinux |
| 回包没有出去 | OUTPUT/POSTROUTING 计数异常 | 路由、SNAT、反向路径过滤问题 |
这里面最容易被忽略的是 FORWARD 路径。很多人在网关做端口映射,只盯 INPUT 链,可转发流量根本不过 INPUT,它只走 FORWARD。这种情况在云服务器上做端口转发时尤其常见,后面第五章我会用一个完整案例复盘。
4.3 抓 NAT 包时,别被 DNAT 改写搞糊涂
还有一个高频迷惑点:你在网关的 eth0 上抓包,看到的目的 IP 是公网 IP,于是觉得 DNAT 规则没生效。其实未必,因为 tcpdump 在入方向上抓到的包大概率是进入 Netfilter 改写之前的原始包,DNAT 把目的 IP 从公网地址改成内网地址的动作发生在它后面。
所以判断 DNAT 是否生效,最可靠的手段还是看 nat 表的规则计数器:
iptables -t nat -L PREROUTING -v -n --line-numbers如果 DNAT 规则的pkts在增长,说明改写动作确实发生了。如果你还想从包层面看到改写后的结果,可以tcpdump -nni any或者在内网目标主机上抓包确认。始终记住一点:tcpdump 和 iptables 的观测点不同,结合起来看才能还原完整链路。
5. 一次 DNAT 端口映射不生效的完整复盘
理论讲再多不如走一遍真实排查链路。下面这个案例我做过很多次类似的处理,里面的陷阱非常有代表性。场景是一台双网卡网关,eth0 接外网,eth1 接内网,想把公网 IP 的 8080 端口转发给内网主机 192.168.8.10 的 80 端口。规则如下:
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 192.168.8.10:80 iptables -A FORWARD -i eth0 -o eth1 -p tcp --dport 80 -j ACCEPT然而外网客户端访问网关公网 IP 的 8080 端口一直超时。我们按秩序来排查。
5.1 第一步:先看规则是否在,再看计数器
我先执行了iptables -t nat -S PREROUTING,DNAT 规则在,没问题。然后看计数器:
iptables -t nat -L PREROUTING -v -n --line-numbersDNAT 那一条pkts在持续增长,说明外部流量确实到达了 PREROUTING 链,改写动作也执行了。于是问题缩小到转发路径。再看 FORWARD 链:
iptables -L FORWARD -v -n --line-numbers输出里有一条DROP all -- 0.0.0.0/0 0.0.0.0/0的兜底规则,它的pkts涨得飞快。而那条ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80的计数却是 0。到这里,嫌疑已经很重了。
5.2 第二步:加 LOG 规则,把决策过程钉死
为了确认包到底是在哪里被丢的,我在 FORWARD 链首尾各加了一条 LOG 规则:
iptables -I FORWARD 1 -p tcp --dport 80 -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "DBG-FWD-IN: " iptables -A FORWARD -p tcp --dport 80 -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "DBG-FWD-OUT: "然后在另一台终端跑dmesg -T | grep DBG-FWD。结果非常明显:DBG-FWD-IN有记录,DBG-FWD-OUT没有。这就证明包进入了 FORWARD 链,但还没走到链尾,就在中途被 DROP 规则终结了。
5.3 第三步:查规则顺序,根因水落石出
我用iptables -S FORWARD查看规则完整列表,发现兜底 DROP 规则是之前某个安全加固操作追加的,而新的 ACCEPT 规则因为用了-A追加在它后面。虽然内容都对,但包先撞上 DROP,根本轮不到 ACCEPT。这就是典型的顺序问题。那条 DROP 规则本来是为了兜底拒绝非法转发流量,却被当成"万能保险"用,最后把合法的端口转发也一并干掉了。
修复方式是把 ACCEPT 规则插到 DROP 之前,同时补上回程流量的放行:
iptables -I FORWARD 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -I FORWARD 2 -i eth0 -o eth1 -p tcp --dport 80 -j ACCEPT注意内网主机返回的流量也要经过 FORWARD 链,如果不放行回程包,客户端能发请求但收不到响应,表现依然是超时。加上状态放行规则后,再测试,端口通了,计数器也显示 ACCEPT 规则正常增长。
5.4 案例复盘:教训是什么
这个问题的根因并不是"iptables 规则没生效",而是"规则存在,但顺序不对"。《防火墙调试》里最核心的原则就是:不要只问"规则为什么没生效",要问"包在哪一步被谁处理了"。计数器告诉你包在哪条规则上被命中,LOG 告诉你决策过程,顺序梳理则告诉你为什么命中顺序和你设想的不同。三步走完,绝大多数问题都能定位。
6. 从设计上避开调试地狱:规则组织与变更管理的经验
排查技巧再熟练,也不如从一开始就把规则设计得不容易出错。这些年调试了太多防火墙,我总结出几个让规则集更可控的习惯,分享给大家。
6.1 明确兜底策略,但别用"一刀切 DROP"
默认策略-P FORWARD DROP是好事,但很多人在链尾再加一条-A FORWARD -j DROP作为"双保险",这就容易出问题。每当你新增业务放行规则时,如果忘了用-I把它插到 DROP 前面,这条新规则就形同虚设。更好的做法是:
- 用链默认策略承担兜底拒绝职责,链内不要放无条件的 DROP。
- 如果确实需要显式 DROP,把它放在链的最后,并且所有业务放行规则统一用
-I插入到链首。 - 在链首先放行
ESTABLISHED,RELATED回程流量,再放行具体业务端口。
6.2 conntrack 状态规则的位置
很多单向通问题的根源是回程包没有被妥善处理。对转发场景来说,链首放一条:
iptables -I FORWARD 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT能极大简化后续规则。有了它,你就不需要为每个内部服务单独配置回程放行规则,只需关心主动发起的新连接。注意-m conntrack --ctstate是较新的写法,旧系统上可能是-m state --state,语义基本一致,优先用前者。
6.3 双栈环境别忘了 IPv6
现在很多服务器默认开启 IPv6,但 iptables 命令管理的是 IPv4 规则,IPv6 的规则要单独用ip6tables维护。我碰到过好几次这样的案例:IPv4 端口一切正常,某个客户端却连不上,最后发现对方走的是 IPv6,而服务器上ip6tables -L显示所有入站流量都被 DROP。如果你的业务没有任何 IPv6 需求,建议直接禁用 IPv6 或者明确配置ip6tables策略,不要让"没配置"变成"意外拒绝"。
排查时也要养成习惯:
iptables -S ip6tables -S两个都看一眼,再下结论。
6.4 规则变更必须有三步预案
生产环境上改防火墙,最忌讳"改完再想怎么回滚"。我个人的固定流程是:先备份、再变更、最后验证。
# 第一步:全套备份,带时间戳 iptables-save > /root/fw-backup/iptables.$(date +%F_%H%M%S).rules ip6tables-save > /root/fw-backup/ip6tables.$(date +%F_%H%M%S).rules # 第二步:执行变更(举例) iptables -I FORWARD 1 -i eth0 -o eth1 -p tcp --dport 8080 -j ACCEPT # 第三步:验证 iptables -L FORWARD -v -n --line-numbers | grep 8080备份文件最好同步到另一台机器,因为防火墙配置出错之后,这台机器可能连 SSH 都进不来,本地备份等于白搭。我甚至见过把 iptables 规则文件放在/etc/下并配置了开机自动加载,结果试规则时不小心把当前连接也断了,重启之后规则照样是坏的。有了异地备份,至少能保证尽快恢复。
6.5 用注释和自定义链管理大型规则集
规则一多,看iptables -S的输出就变成一件痛苦的事。两个小技巧能明显改善:
给关键规则加注释,后续排查时一眼就能看出这条规则是干什么的:
iptables -I INPUT 5 -s 192.168.8.0/24 -p tcp --dport 22 -m comment --comment "allow admin ssh" -j ACCEPT按业务拆自定义链,把某一类流量放在单独链里管理,主链保持清爽:
iptables -N WEB iptables -A WEB -p tcp --dport 80 -j ACCEPT iptables -A WEB -p tcp --dport 443 -j ACCEPT iptables -I FORWARD 1 -i eth1 -m comment --comment "web forward" -j WEB这样做的好处是,某天 WEB 业务要整体下线,直接清空自定义链就行,不用在几百条规则里逐个找。
调试 iptables 这么多年,我最大的体会是:永远不要相信直觉,永远让计数器说话。规则写了没生效,先别急着删了重写,先确认规则确实在正确的位置、再确认包确实走到了这条规则、最后再确认是哪个动作把它终结了。只要把"包从哪来、到哪去、在哪一步被决策"这三件事弄清楚,大部分防火墙问题都能在十分钟内定位。
如果你现在手头正好有一个"不生效"的规则,建议按这个顺序来:iptables -S看全量规则和顺序,-v -n -L看计数器,加 LOG 看路径,再上 tcpdump 看包,最后才谈改规则。多数时候你会发现,在"规则不生效"之前,其实还横着一条被遗忘的默认策略、一次错误的链选择,或者一个你压根没注意到的 IPv6 地址。