ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

iptables表与链彻底搞懂:数据包走向与自定义链实战

iptables表与链彻底搞懂:数据包走向与自定义链实战 只要你的服务器跑过 Docker、K8s、OpenStack 里的任何一个就一定绕不开 iptables。但很多人背得出“五链四表”真到写规则时却常常翻车明明在 nat 表里加了 DNAT 不生效filter 表全清了转发还是不通。问题基本出在一个点上——没搞清 iptables 里的链表关系。这篇就来把这件事彻底掰开揉碎讲清楚表和链到底怎么组织、数据包怎么走、自定义链是怎么回事以及最常见的那句“cant 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 一张表看懂所有关系下面的矩阵是最核心的一张图建议直接存下来表PREROUTINGINPUTFORWARDOUTPUTPOSTROUTINGraw有无无有无mangle有有有有有nat有有无有有filter无有有有无表格里“有”的意思是这张表对应的内置链存在并且数据包走到对应检查点时会按规则处理。同一列里多张表时按上一节说的优先级顺序执行。比如 PREROUTING 列里 raw、mangle、nat 都有实际过检顺序就是 raw → mangle → nat。这里有两个容易困惑的点。一是 nat 表的 INPUT 链。以主流通用内核版本来说nat 表内置链是 PREROUTING、INPUT、OUTPUT、POSTROUTING但大多数场景你根本不会在nat/INPUT里写规则。它更多是内核连接跟踪在本地接收数据包时做反向地址恢复用的一个钩子位置。手工写规则时记住“DNAT 去 PREROUTINGSNAT 去 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、filterfilter 表在这里决定是否允许这个包被转发。比如你写了-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 要把源地址写死成一个具体 IPIP 变了就得改规则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. 报错“cant initialize nat table”时先从链表关系的缺失查起6.1 这句报错到底在说什么在服务器、容器、虚拟环境里经常能看到这么一句iptables v1.8.9 (legacy): cant 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_natiptable_nat是老模块名新内核里是nf_nat相关的几个模块。如果加载失败再看内核配置文件里有没有开启CONFIG_IP_NF_NAT。多数发行版默认是开启的如果没开那就是内核本身缺功能靠用户态怎么折腾都没用。第三步确认权限。iptables 操作需要 root 权限或者CAP_NET_ADMIN能力。在普通用户、非特权容器里执行即使表存在也会初始化失败。容器环境特别容易踩这个坑宿主机上 nat 表好好的容器里一执行就报table does not exist其实只是容器没有对应的权限和模块视图。第四步检查容器内的模块视图。容器如果没挂载宿主机的/lib/modulesmodprobe会直接失败这时候即使宿主机模块齐全容器里也没法加载。需要以特权模式运行容器或者把宿主机的/lib/modules挂进去。下面是诊断时常用的三连命令iptables -V lsmod | grep -E nat|iptable iptables -t nat -L -n前两条确认用户态和内核态各自的状态第三条确认 nat 表恢复到可用状态后的实际输出。6.3 一个真实案例容器环境里起不来 nat 表我之前处理过一个实际案例一套基于 Docker 的 CI 环境某天某个流水线脚本突然报cant 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 时代依然通用——只是它把“表和链”的自由度又放开了一层但那又是另一个话题了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进