☰
iptables表与链彻底搞懂:数据包走向与自定义链实战
2026/10/10 14:18:23 网站建设 项目流程

只要你的服务器跑过 Docker、K8s、OpenStack 里的任何一个,就一定绕不开 iptables。但很多人背得出“五链四表”,真到写规则时却常常翻车:明明在 nat 表里加了 DNAT 不生效,filter 表全清了转发还是不通。问题基本出在一个点上——没搞清 iptables 里的链表关系。这篇就来把这件事彻底掰开揉碎,讲清楚表和链到底怎么组织、数据包怎么走、自定义链是怎么回事,以及最常见的那句“can't initialize iptables table `nat'”到底在说什么。

写这篇文章主要是因为我发现很多同事和读者都卡在同一个地方:他们会用iptables -A INPUT -p tcp --dport 22 -j ACCEPT这种简单命令,但一涉及到 NAT 网关、端口映射、透明代理这些场景就抓瞎。原因不是命令不熟,而是脑子里对“表”和“链”的层次关系没有建立起来。所以下面不讲零散命令,而是把整个模型讲透,适合所有想深入理解 Linux 网络数据路径的运维、开发和云原生从业者。

1. 先别急着背“五链四表”:理清链和表到底是个什么关系

1.1 表是规则分类,链是检查关卡

很多人第一次接触 iptables 时,会看到一张“五链四表”的图:五条链(PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING),四张表(raw、mangle、nat、filter)。然后下意识觉得这是九个独立的东西,互相之间有交叉。这个直觉不能说完全错,但它忽略了最关键的一点:表和链根本不在同一个维度上。

表是规则的分类维度。filter 表管放行和丢弃,nat 表管地址转换,mangle 表管修改包字段,raw 表管是否跟踪连接。你写规则时,首先要回答“这条规则属于哪一类”,这就是-t参数在干的事:-t filter、-t nat。

链是数据包流经的检查关卡。一个数据包从网卡进来,到被进程接收,或者从一个网卡转发到另一个网卡,中间会经过若干固定的检查点。每个检查点就是一条链。数据包要往哪走,决定了它经过哪几个关卡,而每个关卡上又挂了哪些“检查项目清单”——也就是哪些表。

打个比方:你去机场,要经过入口防爆检查、值机柜台、安检口、登机口等不同位置。这些位置就是“链”。而每个位置上要做的检查内容(证件核对、行李扫描、人身安检、登机牌查验)就是“表”。你在哪一站、检查什么项目,是由你的行程安排决定的,不是每个位置都把所有项目做一遍。

所以千万不要把“四张表”和“五条链”当成九个并列的盒子去背,而要把它们理解成一个矩阵:行是表,列是链,交叉点才有实际意义。

1.2 同名链在不同表里是各自独立的规则集合

这里有个特别容易踩的坑:像 OUTPUT 这样的链名,在 raw、mangle、nat、filter 四张表里都存在。它看起来是同一个名字,但在每张表里都是独立的一条链。也就是说,iptables -t filter -L OUTPUT和iptables -t nat -L OUTPUT查看的是两条完全不同的规则集合,只不过碰巧叫同一个名字。

这意味着什么?意味着如果你默认执行iptables -L,你只看到了 filter 表里的内容。nat 表里那套 OUTPUT 链规则,你根本没看到。很多排错翻车就翻在这里:用户说“我明明在 OUTPUT 链上加了规则,怎么不生效?”结果一问,他用-t nat加到了 nat 表的 OUTPUT,但检查时却用不加-t的命令去看 filter 表的 OUTPUT,当然看不到。

我在处理生产问题时,第一件事永远是iptables-save,把所有表一次性输出出来看。这个命令输出的格式非常清楚地展示了表、链、规则三层的层级关系,比iptables -L分段查看直观得多,也更容易发现“同名链在不同表里各有一套规则”这种问题。

2. 数据包过检的本质:每个检查点都背着固定顺序的检查项

2.1 表在内核里的优先级:为什么顺序永远是 raw → mangle → nat → filter

如果你在一条链上同时配置了多张表的规则,数据包经过这条链时,并不是随机或者按你配置顺序去执行,而是按内核预设的优先级固定执行的。这个优先级是 netfilter 框架层写死的:

表优先级值说明
raw-300最早执行,决定是否做连接跟踪
mangle-150第二执行,修改包字段
nat-100第三执行,地址转换
filter0第四执行,过滤放行
security50最后执行,配合 SELinux 等安全模块

所以只要某条链上同时有 raw、mangle、nat、filter,顺序就永远是 raw 最先、filter 最后,谁改了这个顺序都不行。这不是 iptables 的命令风格问题,而是内核在注册每个表对应的钩子函数时,把优先级作为链表排序依据,遍历时严格按照优先级从小到大执行。

为什么 raw 必须在最前面?因为 raw 的核心工作是 NOTRACK——决定一个数据包要不要被连接跟踪。连接跟踪是 nat 表做地址转换的基础,如果先做了 NAT 再去决定“要不要跟踪”,逻辑就乱套了。mangle 放在 nat 前面也有道理:mangle 可以修改包的 TOS、TTL、MARK 等属性,这些改动可能影响后续 NAT 和过滤的匹配结果。

还有一个很多人忽略的点:数据包经过某条链时,会把这条链上所有表的检查全部做完,才会去下一个检查点。不是“先查完所有链上的 raw 表,再查所有链上的 mangle 表”。表和链之间的关系是嵌套的:一条链内部按表顺序逐项过检,过检完才进入下一条链。这个顺序搞反了,整个数据路径就想不明白。

2.2 一张表对应多个钩子:从 iptables-save 的结构看懂链表组织

iptables-save的输出是一个人理解表链关系最好的教材。下面是我从一台真实服务器上截出来的简化版:

*filter :INPUT DROP [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [0:0] -A INPUT -p tcp --dport 22 -j ACCEPT COMMIT *nat :PREROUTING ACCEPT [0:0] :INPUT ACCEPT [0:0] :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.10:8080 COMMIT

这里的关键结构是:*filter标记表开始,COMMIT标记表提交。中间以冒号开头的那几行,定义了这张表下面的内置链以及默认策略。以-A开头的行,是向这张表的某条链追加规则。所以整体是表 → 链 → 规则三层结构,而不是“五条链平铺在四张表外面”。

内核视角其实更直接:每个表在它出现的那几个钩子(hook)上注册一个钩子函数,这个函数负责对数据包执行该表内对应链的规则。数据包到某个钩子时,内核沿着优先级遍历所有已注册的钩子函数,形成一条检查链表。换句话说,用户空间看到的“链上按表顺序过检”,在内核里就是一张按优先级排好序的钩子函数链表。这就是“链表关系”这个词最本质的含义。

2.3 为什么有的链上只有两张表,有的链却有五张

既然每张表都可以在多个钩子上注册,那为什么 PREROUTING 上只有 raw、mangle、nat 三张表,而 OUTPUT 上却有 raw、mangle、nat、filter 四张?这就要回归到每个表的职责在不同检查点是否“有意义”。

raw 表的职责是决定是否做连接跟踪。连接跟踪发生在路由决策之前对本机发出包的决策点,所以只在 PREROUTING 和 OUTPUT 上有 raw。

nat 表的职责是修改源地址或目标地址。DNAT 必须在路由决策之前做,否则路由表已经根据原目标地址决定好去向了,改地址也没用,所以 DNAT 放在 PREROUTING(外部流量进入)和 OUTPUT(本机发出且需要重定向)上。SNAT 必须在路由决策之后做,因为出接口选定了才知道要把源地址改成哪个 IP,所以 SNAT 放在 POSTROUTING 上。

filter 表的职责是根据五元组做放行或丢弃。放行判断需要在路由决策之后,区分“这个包是进本机还是转发出去”,所以 filter 出现在 INPUT、FORWARD、OUTPUT 上,而不会出现在 PREROUTING 和 POSTROUTING 上。

mangle 表比较特殊,它什么都能改,所以在五条链上都有。但这种“全都有”恰恰让很多人误以为所有表也是五条链全覆盖,实际并非如此。搞清楚每张表出现的链,比记住“五链四表”这个口号有用得多。

3. 内置链上的表矩阵:一张表看懂所有关系

3.1 一张表看懂所有关系

下面的矩阵是最核心的一张图,建议直接存下来:

表PREROUTINGINPUTFORWARDOUTPUTPOSTROUTING
raw有无无有无
mangle有有有有有
nat有有无有有
filter无有有有无

表格里“有”的意思是:这张表对应的内置链存在,并且数据包走到对应检查点时会按规则处理。同一列里多张表时,按上一节说的优先级顺序执行。比如 PREROUTING 列里 raw、mangle、nat 都有,实际过检顺序就是 raw → mangle → nat。

这里有两个容易困惑的点。一是 nat 表的 INPUT 链。以主流通用内核版本来说,nat 表内置链是 PREROUTING、INPUT、OUTPUT、POSTROUTING,但大多数场景你根本不会在nat/INPUT里写规则。它更多是内核连接跟踪在本地接收数据包时做反向地址恢复用的一个钩子位置。手工写规则时,记住“DNAT 去 PREROUTING,SNAT 去 POSTROUTING,本机发出的重定向去 OUTPUT”就够了。

二是 security 表。SELinux 等安全模块会用到它,一般出现在 INPUT、FORWARD、OUTPUT 上,优先级在 filter 之后。对绝大多数普通用户来说可以暂时忽略,知道有这么回事就行。

3.2 为什么 FORWARD 上没有 nat 表:这是理解路由和 NAT 先后关系的钥匙

FORWARD 上没有任何 nat 表,这是有深刻原因的。NAT 做地址转换,必须在路由决策生效之前或者之后,唯独不能在路由决策进行到一半的时候。

先看 DNAT。外部一个包进来,目标地址是公网 IP1.2.3.4,你希望把它转到内网机器192.168.1.10。这个改写必须在 PREROUTING 完成,因为内核紧接着要做路由决策,而路由决策的依据是数据包的目标地址。如果在 PREROUTING 里改了目标 IP,路由决策就会把包导向去往内网的路由,从而进入 FORWARD 链。如果等包已经进入 FORWARD 链,路由决策已经结束,这时候再改目标 IP,包的去向已经定死了,改地址没有任何意义。

再看 SNAT。内网机器访问外网,源地址是192.168.1.10,网关要把源地址改成公网出口 IP。这个改写必须发生在路由决策之后,因为路由决策确定了包要从哪个接口出去,你才知道该把源地址改成哪个接口的 IP。POSTROUTING 就是这个位置。

所以 nat 表在 PREROUTING 和 POSTROUTING 各司其职,缺一不可。FORWARD 上之所以没有 nat,正是因为这里不是做地址转换的正确时机。我之前真见过有人在 FORWARD 链里加 DNAT 规则,结果怎么都不生效,查了半天才发现整个 FORWARD 上根本没有 nat 表这个检查项。理解了这个原理,下次就不会犯同样的错。

4. 跟着一个包走完全程:三种真实场景下的链表追踪

4.1 访问本机服务:PREROUTING → INPUT

先看最简单的场景:外部主机访问你服务器上的 Web 服务。假设公网 IP 是1.2.3.4,服务器上跑了8080端口的服务,你想让外部直接访问80端口,于是加了一条 DNAT 规则。

数据包到达服务器网卡后,第一个检查点是 PREROUTING。这条链上按顺序过 raw、mangle、nat 三张表。在 nat 表里命中了 DNAT 规则,目标地址从1.2.3.4:80被改写成了192.168.1.10:8080。紧接着内核做路由决策,发现新目标地址是本机,于是包进入 INPUT 链。

INPUT 链上先过 mangle,再过 filter。这里有个非常关键的细节:filter 表匹配到的包,目标地址已经是 DNAT 之后的结果了。也就是说,如果你想在 filter 里写“只允许访问 8080 端口的包进来”,应该匹配--dport 8080,而不是原始的80。很多人在这个点上搞混,导致规则永远不对。

这正好解释了为什么 filter 在 nat 后面:NAT 先把地址改好,过滤规则再根据改完之后的五元组做判断。如果顺序反过来,filter 基于原始地址做判断,但包最终到达进程时地址已经变了,语义就非常混乱。

4.2 网关转发流量:PREROUTING → FORWARD → POSTROUTING

再看向外转发的场景。一台 Linux 网关,内网口eth1接192.168.1.0/24,外网口eth0接公网。内网一台机器访问8.8.8.8,数据包到达网关的eth1。

进 PREROUTING,顺序过 raw、mangle、nat。这里一般没有 DNAT 规则,包原封不动。然后路由决策发现目标不是本机,于是进入 FORWARD 链。FORWARD 上顺序过 mangle、filter,filter 表在这里决定是否允许这个包被转发。比如你写了-A FORWARD -i eth1 -o eth0 -j ACCEPT,包就继续往前走。

接着进入 POSTROUTING,顺序过 mangle、nat。在 nat 表的 POSTROUTING 链上命中了 MASQUERADE 规则,源地址从192.168.1.10改写成了网关的出口 IP。包带着新的源地址从eth0发出去。

回包方向则完全相反。公网回复到达eth0,进 PREROUTING,此时连接跟踪机制已经在为之前那条 NAT 记录自动做反向转换,所以不需要用户额外写规则。包经过 FORWARD 时,filter 看到的已经是反转换之后的地址,也就是192.168.1.10。最后从eth1出去,回到内网机器。

这里有一个实操建议:做网关 NAT 时,出口 IP 是动态获取(PPPoE、DHCP)的情况下,用 MASQUERADE 而不是 SNAT。SNAT 要把源地址写死成一个具体 IP,IP 变了就得改规则;MASQUERADE 会自动取出口地址,代价是每次新连接都要额外查询一次出口 IP,性能略差一点点,但对大多数场景完全够用。

4.3 本机主动外发:OUTPUT → POSTROUTING

最后看本机进程主动外发的情况,比如服务器上跑了个程序去请求外网 API。这个包不是从网卡进来的,而是由本机进程生成,首先做路由决策确认出口和下一跳,然后进入 OUTPUT 链。

OUTPUT 链是检查项最全的一条链:raw → mangle → nat → filter。raw 表在 OUTPUT 上存在的原因,是可以对本机发出的包做 NOTRACK,跳过连接跟踪。这在某些高并发本机代理场景下能省下不少连接跟踪表资源,但一定要注意:跳过连接跟踪之后,nat 表的转换也没法正常工作,因为 NAT 依赖连接跟踪。我的建议是,不到万不得已不要对需要 NAT 的流量用 NOTRACK。

nat 表在 OUTPUT 上的用途,是处理“本机发出的包需要被重定向”的场景,比如透明代理把本机程序的 HTTP 请求转到本地代理端口。命令类似-t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-ports 3128。因为 nat 在 filter 前面,所以如果 OUTPUT 的 filter 规则要根据目标端口放行,记得用重定向之后的端口,而不是原始 80 端口。

最后包进入 POSTROUTING,过 mangle 和 nat。如果这条连接来自内网机器被本机转发(虽然到了这步说明包是本机的,但如果你开了 IP 转发,某些由本机生成的包也可能需要 SNAT),或者本机出口需要伪装,就在这里完成源地址改写,最后从路由决策选定的网卡发出去。

5. 用户自定义链:在表内部实现“函数调用”的关键

5.1 自定义链属于具体表,不能跨表跳转

内置链满足不了所有需求时,可以用-N创建自定义链。比如:

iptables -t filter -N DROP_BAD iptables -t filter -A DROP_BAD -s 192.168.1.100 -j DROP iptables -t filter -A INPUT -j DROP_BAD

注意看第二个参数是-t filter。自定义链创建时就必须指定它属于哪张表,创建完成之后,它就只能在这张表内部被跳转。filter 表建的自定义链只能被 filter 表的内置链或者其他 filter 表自定义链跳转;nat 表建的自定义链也只能在 nat 表内部跳转。这是新手最容易犯的错之一:在默认的 filter 表里建了个链,然后想在 nat 表里跳过去,结果根本找不到。

为什么会这样设计?因为“表”本质上是内核里一个独立的规则集容器,表和表之间互不可见。自定义链只是在这个容器内部划出的一段可复用的规则序列,并没有跨容器跳转的概念。

5.2 跳转与返回:父链和自定义链之间是怎么结束的

自定义链的内部行为和函数调用很像。数据包进入自定义链后,从上到下逐条匹配:

  • 某条规则匹配成功,并且目标是ACCEPT或DROP,那整个包的处理就到此结束。
  • 所有规则都没匹配,或者某条规则的目标是RETURN,数据包就回到父链,继续执行父链里刚才跳走之后的下一条规则。

所以自定义链没有默认策略。内置链可以设置:INPUT DROP这样的默认策略,但自定义链不行。它必须把处理结果通过 RETURN 交还给父链,或者直接 ACCEPT/DROP 终结整个流程。

这个设计用一段小例子演示:

iptables -t filter -N BAD_POLICY iptables -t filter -A BAD_POLICY -s 10.0.0.0/8 -j DROP iptables -t filter -A BAD_POLICY -p tcp --dport 23 -j DROP iptables -t filter -A INPUT -j BAD_POLICY iptables -t filter -A INPUT -j ACCEPT

当包到达 INPUT 链,匹配到-j BAD_POLICY后进入自定义链。如果源 IP 是10.0.0.0/8,直接 DROP,整个流程结束,不会回到 INPUT 继续执行后面的ACCEPT。如果源 IP 不是内网段、目标端口也不是 23,自定义链里没有任何规则命中,包从链尾自然返回 INPUT,继续执行 INPUT 的-j ACCEPT。

这里有个实际中常见的坑:有些人在自定义链最后加了一条无条件ACCEPT,比如-A BAD_POLICY -j ACCEPT,结果父链里后面的规则全都失效了。原因就是 ACCEPT 直接终结了整个包的处理,根本不会回到父链。真要放行并且继续走父链后续规则,应该写RETURN或者干脆不写,让规则从链尾自然返回。

5.3 什么场景适合抽自定义链

自定义链不是用来炫技的,我一般在两种情况下使用。

第一种是规则复用。一组黑名单、一组日志审计规则、一组针对特定来源的限速规则,可能同时要在 INPUT 和 FORWARD 上生效。把它们抽到同一个自定义链里,两条父链各加一行-j跳转,改规则时只改一处。这比复制粘贴规则靠谱得多。

第二种是统计观察。自定义链天然有计数器,通过iptables -L BAD_POLICY -n -v就能看到这个链总共被命中多少次、每条规则命中多少包。排查“包到底有没有走到这层”时,这个计数器比dmesg日志更快。我处理过不少“规则看着没问题就是不生效”的工单,最后都是靠给关键位置插一个自定义链看计数器,十分钟内就定位到了是哪一层断了。

还有一点建议:自定义链的嵌套层数不要太深。虽然技术上限不止一层,但超过两层之后,一个包到底怎么走的,靠人脑几乎推不出来了。我个人的习惯是只嵌套一层,父链跳自定义链,自定义链内部不要再去跳另一个自定义链。规则再复杂,宁可在命名上多花点心思,也不要用深度嵌套换所谓的整洁。

6. 报错“can't initialize nat table”时,先从链表关系的缺失查起

6.1 这句报错到底在说什么

在服务器、容器、虚拟环境里,经常能看到这么一句:

iptables v1.8.9 (legacy): can't initialize iptables table `nat': table does not exist (do you need to insmod?) Perhaps iptables or your kernel needs to be upgraded.

结合前面讲的链表关系,这句报错的意思就非常直白了:用户态想操作 nat 表,但内核的钩子链表上根本没有 nat 表这个节点。你可以把它理解成你想在一个不存在的检查站执行检查,那所有规则都无从谈起。

注意报错里明确写了(legacy),说明当前执行的是iptables-legacy后端。这本身就是一条重要线索:同一个系统里可能同时装了iptables-legacy和iptables-nft两套工具,都是叫iptables,但后端完全不一样。默认命令通常指向其中之一,脚本里如果用绝对路径调了另一个,就会出现这种和“内核没有注册对应表”高度相关的报错。

6.2 常见原因与解决顺序

遇到这个报错,我建议按以下顺序排查,不要上来就重装 iptables:

第一步,确认当前用的是哪套后端。执行iptables -V,如果显示nf_tables或legacy,先知道自己站在哪边。大部分现代发行版默认是nf_tables,有些旧脚本硬编码了/usr/sbin/iptables-legacy,就会两头不对付。

第二步,看内核有没有加载对应模块。

lsmod | grep nat modprobe iptable_nat modprobe nf_nat

iptable_nat是老模块名,新内核里是nf_nat相关的几个模块。如果加载失败,再看内核配置文件里有没有开启CONFIG_IP_NF_NAT。多数发行版默认是开启的,如果没开,那就是内核本身缺功能,靠用户态怎么折腾都没用。

第三步,确认权限。iptables 操作需要 root 权限或者CAP_NET_ADMIN能力。在普通用户、非特权容器里执行,即使表存在也会初始化失败。容器环境特别容易踩这个坑:宿主机上 nat 表好好的,容器里一执行就报table does not exist,其实只是容器没有对应的权限和模块视图。

第四步,检查容器内的模块视图。容器如果没挂载宿主机的/lib/modules,modprobe会直接失败,这时候即使宿主机模块齐全,容器里也没法加载。需要以特权模式运行容器,或者把宿主机的/lib/modules挂进去。

下面是诊断时常用的三连命令:

iptables -V lsmod | grep -E 'nat|iptable' iptables -t nat -L -n

前两条确认用户态和内核态各自的状态,第三条确认 nat 表恢复到可用状态后的实际输出。

6.3 一个真实案例:容器环境里起不来 nat 表

我之前处理过一个实际案例:一套基于 Docker 的 CI 环境,某天某个流水线脚本突然报can't initialize iptables table 'nat'。脚本本身不放火,不复杂,就是要在容器里临时执行几条iptables -t nat命令做测试。

排错过程很快。宿主机上执行iptables -t nat -L,一切正常,说明宿主机内核没问题。进入容器执行同样的命令,复现报错。再执行modprobe iptable_nat,提示找不到模块目录——这就暴露了问题所在:容器没有挂载宿主机的/lib/modules,容器内无法访问内核模块,同时容器本身也不是特权模式,没有完整的CAP_NET_ADMIN。

最终方案是重建容器并加上--privileged,同时挂载/lib/modules进去,问题立刻消失。这个案例给我最大的教训是:遇到“表和链相关”的报错,先分清是用户态问题还是内核态问题。用户态问题包括后端选错、版本太老、权限不够;内核态问题包括模块没加载、内核没编译对应功能。一上来就翻规则文件往往是浪费时间。

另外多说一句,如果是在 Kubernetes 环境里,不要试图在每个 Node 里手动折腾宿主机 iptables,节点网络交由 CNI 插件(Calico、Cilium、Flannel)统一管理,手动规则很容易和 CNI 的规则互相覆盖,排查起来更痛苦。


最后分享一点个人体会:iptables 的“链表关系”这个概念,表面上是四张表五条链的交叉矩阵,本质上其实是数据包生命周期在不同检查点上按固定顺序执行检查的过程。你把每次检查想象成一把尺子上的刻度,表就是尺子上的不同量程,链就是量尺的位置。别把规则堆成一锅粥,也别在自定义链上叠太多层。碰到问题先看iptables-save理清结构,看计数器定位命中断点,比盲试命令效率高得多。这套思路,放到 nftables 时代依然通用——只是它把“表和链”的自由度又放开了一层,但那又是另一个话题了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询