网络收发包应用之 iptables

iptables 最容易混乱的地方,是它同时在讲两件事:

  • 从内核视角看,Linux 在网络包经过协议栈的几个关键位置埋了 Netfilter hook。包流到这些位置时,内核会调用已经注册好的规则处理函数。
  • 从用户视角看,iptables 是配置这些规则的命令行工具。我们写的 iptables -A ... -j ... 最终会变成内核里某个 hook 上的一条规则。

所以这篇笔记可以用一句话串起来:iptables 把规则按功能放进表 table,按网络包经过的位置挂到链 chain,网络包经过 Netfilter hook 时按顺序匹配规则,命中后执行 target 动作。

几个关键词先对齐:

名词 作用 记忆方式
hook 内核协议栈里的固定检查点 包走到这里,内核问一声:有没有规则要处理
chain 某个 hook 上的一串规则 PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING
table 按功能组织规则 raw、mangle、nat、filter,现代系统还有 security
rule 匹配条件和动作 -s 1.2.3.4 -j DROP
target 命中后的动作 ACCEPT、DROP、DNAT、SNAT、MASQUERADE、MARK 等

说明:很多文章会说“四表五链”,这里的四表通常指 raw/mangle/nat/filter。现代 iptables 还可能有 security 表,用于 SELinux 等强制访问控制场景。学习网络收发包和 Docker NAT 时,先掌握四表五链就够了。

一、iptables 常见应用

1. 四表的职责

iptables 从应用视角来看分为 4 表:

这张图是在回答“规则按什么功能分类”:

  • filter 表:做包过滤,是防火墙最常见的表。例如允许、拒绝、丢弃某些包。
  • nat 表:做地址和端口转换。例如 Docker 容器访问外网时做 SNAT/MASQUERADE,外网访问容器端口时做 DNAT。
  • mangle 表:修改包的附加属性或标记。例如改 TTL、TOS,或者打 MARK 给策略路由、流量整形使用。
  • raw 表:主要用于连接跟踪之前的早期处理。例如配合 NOTRACK 跳过 conntrack,减少某些高流量场景下的状态跟踪开销。

理解这张图时不要先纠结命令,而是先想清楚:filter 决定包能不能过,nat 决定包看起来从哪里来、到哪里去,mangle 决定包带什么标记,raw 决定要不要尽早绕开连接跟踪。

2. filter:在 INPUT 链过滤本机流量

首先来看 filter 过滤:

这张图画的是一个“进入本机”的包:

  1. 包从网卡进入,到 IP 层入口 ip_rcv()
  2. 先经过路由前的 PREROUTING
  3. 内核查路由,发现目的 IP 是本机。
  4. 包进入 ip_local_deliver(),在真正交给 TCP/UDP 之前经过 INPUT
  5. INPUT 链上的 filter 规则判断是否接受这个包。

也就是说,想保护本机服务,通常在 filter/INPUT 上下规则。例如禁止某个 IP 访问本机,或者只允许白名单 IP 访问 SSH。

可以通过命令配置:

图中的两个场景:

# 场景 1:拒绝某个 IP 对本机的全部请求
iptables -I INPUT -s 1.2.3.4 -j DROP
iptables -D INPUT -s 1.2.3.4 -j DROP

# 场景 2:SSH 只允许白名单 IP 访问
iptables -t filter -I INPUT -s 1.2.3.4 -p tcp --dport 22 -j ACCEPT
iptables -t filter -I INPUT -p tcp --dport 22 -j DROP

这里有两个细节很重要:

  • -I INPUT 是插到链头,优先级比后面的规则高;-A INPUT 是追加到链尾。
  • 第二个场景必须先放行白名单,再丢弃其他 SSH 流量。如果顺序反了,白名单也会被前面的 DROP 拦住。

iptables 的规则匹配是“从上到下,命中就执行动作”。如果 target 是 ACCEPT/DROP/REJECT/DNAT/SNAT 这类终止动作,后续规则通常就不会继续匹配了。

3. NAT 场景一:容器或私网主机访问外网

再来看看 NAT。

场景一:请求发送。

这张图画的是 Docker 类似的网络结构:

  • net1 是一个网络命名空间,可以理解成容器的网络世界。
  • veth1 在容器命名空间中,IP 是 192.168.0.2
  • br0 在宿主机命名空间中,IP 是 192.168.0.1,连接容器 veth。
  • 宿主机还有外网网卡 eth0,IP 是 10.162.0.100
  • 外部服务器是 10.162.0.101

容器里的 192.168.0.2 是私有地址,外部网络不知道怎么把响应包路由回它。所以容器请求外部服务器时,宿主机必须把源地址从 192.168.0.2 改成宿主机外网地址 10.162.0.100。这就是 SNAT

其原理如下:

对应规则:

iptables -t nat -A POSTROUTING -s 192.168.0.0/24 ! -o br0 -j MASQUERADE

这条规则的意思是:

  • -t nat:使用 NAT 表。
  • -A POSTROUTING:在路由决策之后、真正发出网卡之前处理。
  • -s 192.168.0.0/24:只处理来自容器网段的包。
  • ! -o br0:如果包不是发回容器网桥,就说明它要出外部网络。
  • -j MASQUERADE:把源地址伪装成出口网卡的地址。

为什么 SNAT 放在 POSTROUTING?因为此时路由已经算出来了,内核已经知道包要从哪个出口网卡出去。MASQUERADE 才能使用出口网卡的地址作为新的源地址。

回包时不需要再写一条显式 DNAT 规则。第一次出包做 SNAT 时,conntrack 已经记录了连接映射:192.168.0.2 -> 10.162.0.101 被改写成了 10.162.0.100 -> 10.162.0.101。响应包回来后,内核根据 conntrack 自动做反向转换,把目的地址从 10.162.0.100 还原成 192.168.0.2

4. NAT 场景二:外网访问容器服务

场景二:请求接收。

这张图画的是外部机器访问宿主机的 10.162.0.100:8088,但真正提供服务的是容器里的 192.168.0.2:80。外部机器并不知道容器地址,只知道宿主机地址,所以宿主机需要把“目的地址和端口”改写到容器服务上。这就是 DNAT,也就是常说的端口映射。

原理如下:

对应规则:

iptables -t nat -A PREROUTING ! -i br0 -p tcp --dport 8088 \
  -j DNAT --to-destination 192.168.0.2:80

这条规则的意思是:

  • -A PREROUTING:包刚进入 IP 层、路由选择之前处理。
  • ! -i br0:不是从容器网桥进来的包,通常就是外部入口流量。
  • -p tcp --dport 8088:匹配访问宿主机 8088 端口的 TCP 包。
  • -j DNAT --to-destination 192.168.0.2:80:把目的地址改成容器的 IP 和端口。

为什么 DNAT 放在 PREROUTING?因为路由决策依赖目的地址。只有先把目的地址从 10.162.0.100:8088 改成 192.168.0.2:80,后面的路由才能判断这个包应该转发给 br0/veth1

回包时同样依赖 conntrack。容器回给外部机器的包,源地址原本是 192.168.0.2:80,内核会自动反向改写成 10.162.0.100:8088。这样外部机器一直以为自己在和宿主机的 8088 端口通信。

二、iptables 底层实现

上述应用都基于一个实现:五个链。它们看起来是 iptables 的概念,实际对应的是网络包接收、转发、发送过程中的 Netfilter hook。

这张图把“五链”和“四表”放到了一起:

  • 纵向是链,也就是包经过的位置:INPUT/PREROUTING/OUTPUT/FORWARD/POSTROUTING
  • 横向是表,也就是规则的功能:raw/mangle/nat/filter
  • 某个格子里有“规则”,说明这个表可以在这个链上挂规则。

注意:不是每张表都能挂到每条链上。比如 filter 只关心最终是否放行,所以主要在 INPUT/FORWARD/OUTPUTraw 要尽早决定是否跳过连接跟踪,所以只在 PREROUTING/OUTPUT

1. 接收本机流量:PREROUTING -> INPUT

结合网络收包过程进行理解:

这张图对应“外部请求本机服务”的路径:

  1. 网卡收到包,经过设备层处理后进入 IP 层。
  2. IPv4 接收入口是 ip_rcv()
  3. ip_rcv() 触发 NF_INET_PRE_ROUTING,也就是 PREROUTING 链。
  4. 规则处理完后,进入路由选择。
  5. 如果路由判断目的地址是本机,就进入 ip_local_deliver()
  6. ip_local_deliver() 触发 NF_INET_LOCAL_IN,也就是 INPUT 链。
  7. INPUT 放行后,才会继续交给 TCP/UDP,再进入本机 socket。

其源码如下:

图中红框是 ip_rcv() 里的关键调用:

NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, ..., ip_rcv_finish)

含义是:先执行 PREROUTING hook 上注册的规则,如果规则最终允许包继续走,再回调 ip_rcv_finish()。而 ip_rcv_finish() 里会做路由选择。

这张图的红框是 ip_local_deliver() 里的关键调用:

NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, ..., ip_local_deliver_finish)

含义是:路由已经确认这是本机包,真正交给传输层之前,执行 INPUT 链规则。防火墙在这里丢包,可以避免包继续进入 TCP/UDP 层和应用 socket。

小结:

  • Linux 在网络包接收的 IP 层入口函数是 ip_rcv()
  • 先执行 PREROUTING 规则。
  • 再进行路由选择。
  • 判断是本机包,则继续执行 INPUT 规则。

2. 本机发送流量:OUTPUT -> POSTROUTING

再结合发送过程:

这张图对应“本机进程主动发包”的路径:

  1. 用户进程调用 send/write,数据经过 TCP 层。
  2. TCP 调用 IP 层发送入口,常见路径会进入 ip_queue_xmit()
  3. 内核先查路由,确定目的地址、下一跳、出口网卡等信息。
  4. 进入 ip_local_out()__ip_local_out(),触发 NF_INET_LOCAL_OUT,也就是 OUTPUT 链。
  5. 继续进入 ip_output(),触发 NF_INET_POST_ROUTING,也就是 POSTROUTING 链。
  6. 再往下进入邻居子系统、设备队列、网卡驱动。

其源码如下:

图中红框表示 ip_local_out() 里触发 OUTPUT

NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT, ..., dst_output)

这里的包是本机进程产生的,所以不会经过 PREROUTINGPREROUTING 是“从外部进来的包”才会经过的链。

图中红框表示 ip_output() 里触发 POSTROUTING

NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, ..., ip_finish_output, ...)

POSTROUTING 的位置在路由之后、发出网卡之前。因此 SNAT/MASQUERADE 很适合放在这里:此时已经知道出口设备,可以安全地改源地址。

小结:

  • Linux 在网络包发送的 IP 层常见入口函数是 ip_queue_xmit()
  • 先进行路由选择。
  • ip_local_out()__ip_local_out() 中执行 OUTPUT 规则。
  • ip_output() 中执行路由后的 POSTROUTING 规则。

3. 转发流量:PREROUTING -> FORWARD -> POSTROUTING

再看网络包转发过程:

这张图对应“Linux 像路由器一样工作”的路径。典型场景就是宿主机帮容器转发流量,或者一台 Linux 做网关。

  1. 包从网卡进入,先到 ip_rcv()
  2. 执行 PREROUTING
  3. 内核查路由,发现目的地址不是本机,而是应该从另一块网卡转发出去。
  4. 进入 ip_forward(),执行 FORWARD
  5. 转发通过后进入 ip_output()
  6. 执行 POSTROUTING
  7. 包从出口网卡发出去。

源码如下:

这张图再次对应 ip_rcv() 触发 PREROUTING。不管最终是本机接收还是转发,只要是外部进来的 IPv4 包,都会先经过这里。

这张图对应 ip_forward() 触发 FORWARD

NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, ..., ip_forward_finish)

FORWARD 只处理“经过本机但不属于本机”的包。如果 Linux 没有开启 IP 转发,或者路由不允许转发,包不会顺利走到这一步。开启 IPv4 转发通常需要:

sysctl -w net.ipv4.ip_forward=1

这张图对应转发包进入 ip_output() 后触发 POSTROUTING。所以转发包路径和本机发包路径的后半段是重合的:最终都要通过 POSTROUTING 后再出网卡。

小结:

  • Linux 在网络包接收的 IP 层入口函数是 ip_rcv()
  • 先执行 PREROUTING 规则。
  • 再进行路由选择。
  • 判断不是本机包,则进入 ip_forward() 转发,在这里执行 FORWARD 规则。
  • 接着进入 ip_output() 发送,会执行 POSTROUTING 规则。

三、iptables 原理汇总

再来看其实现的五链四表:

这张图是“表和链的对应关系”:

典型会经过的表 适合做什么
PREROUTING raw、mangle、nat 入站最早期处理,DNAT,连接跟踪前处理
INPUT mangle、nat、filter 本机入站包过滤或改标记
FORWARD mangle、filter 转发包过滤或改标记
OUTPUT raw、mangle、nat、filter 本机产生的包过滤、DNAT、连接跟踪前处理
POSTROUTING mangle、nat 出站最后阶段处理,SNAT/MASQUERADE

实际系统会受内核版本、iptables 后端和模块影响。例如一些文档会把 nat/INPUT 列出来,一些较老文档不列。日常排查时,可以用命令看当前系统到底支持哪些链:

iptables -t raw -S
iptables -t mangle -S
iptables -t nat -S
iptables -t filter -S
iptables -t security -S

这张图再次总结了四张表的用途。把它和上一张“表链矩阵”结合起来看,就能回答两个问题:

  • 这个功能应该放在哪张表?例如过滤放 filter,地址转换放 nat
  • 这个规则应该挂在哪条链?例如外部访问容器端口要先改目的地址,所以放 PREROUTING;容器访问外网要最后改源地址,所以放 POSTROUTING

结合网络收发转发过程来看:

图中的 1 到 5 可以作为最终记忆版:

流量类型 经过的链 例子
外部访问本机 1 PREROUTING -> 2 INPUT 访问本机 Nginx、SSH
本机访问外部 4 OUTPUT -> 5 POSTROUTING 本机 curl 外部服务
外部经本机转发 1 PREROUTING -> 3 FORWARD -> 5 POSTROUTING Docker 容器出网、Linux 网关转发

一句话记忆:

  • 入站先 PREROUTING,再根据路由分流到 INPUT 或 FORWARD。
  • 本机发包从 OUTPUT 开始。
  • 所有真正要从网卡出去的包,最后都会经过 POSTROUTING。

规则优先级:同一条链上谁先执行

同一个 hook 上可能有多个表的规则。大体顺序可以这样记:

raw -> conntrack -> mangle -> nat(DNAT) -> filter -> security -> nat(SNAT)

更准确地说,Netfilter 内部按 priority 数值从小到大执行。raw 通常在连接跟踪之前,mangle 用于改包和打标,filter 用于过滤,DNAT 发生在路由前,SNAT 发生在路由后。

学习时先不用背完整优先级,抓住两个关键点就很够用:

  • raw 比 conntrack 早,所以能配合 NOTRACK
  • DNAT 要在路由前,SNAT 要在路由后。

NAT 和 conntrack:为什么只写一条规则,回包也能回来

NAT 不是孤立地把每个包都硬改一遍。对于一个新连接,nat 表会在连接开始时决定如何改写,并把映射写入 conntrack。后续同一连接的包,包括反方向的响应包,会根据 conntrack 自动做一致的转换。

这解释了两个常见现象:

  • 做 SNAT/MASQUERADE 时,只写出方向规则,响应包能自动回到容器。
  • 做 DNAT 端口映射时,只写入方向规则,容器响应包的源地址也会自动改回宿主机端口。

如果 conntrack 表满了、被关闭了,或者 raw 表里对某些包做了 NOTRACK,NAT 和有状态防火墙就可能出现异常。

四、和 Docker 网络的关系

Docker 默认 bridge 网络本质上就是把前面的两个 NAT 场景自动化:

  • 容器访问外网:容器私网地址通过宿主机做 POSTROUTING MASQUERADE
  • 外部访问容器端口:宿主机对外端口通过 PREROUTING DNAT 转到容器 IP 和端口。

容器网络里还会用到:

  • network namespace:每个容器有自己的协议栈、网卡、路由表。
  • veth pair:一端在容器里,一端接到宿主机网桥。
  • bridge:宿主机上的 docker0 或自定义 bridge 负责连接多个容器。
  • iptables:负责 NAT、端口映射和部分转发过滤。

所以排查 Docker 网络时,要同时看四样东西:

ip netns
ip addr
ip route
iptables -t nat -S
iptables -t filter -S
conntrack -L

如果容器能 ping 宿主机但不能访问外网,通常优先看:

  • net.ipv4.ip_forward 是否开启。
  • POSTROUTING MASQUERADE 是否存在。
  • FORWARD 链是否允许转发。
  • 宿主机默认路由是否正确。

如果外部访问不了容器暴露端口,通常优先看:

  • PREROUTING DNAT 或 Docker 生成的端口映射规则是否存在。
  • 宿主机防火墙是否在 INPUT/FORWARD 丢包。
  • 容器内服务是否真的监听在目标端口。
  • 回包路径是否能走回宿主机。

五、和 tcpdump 的关系

推荐文章里还有一个很实用的问题:iptables 丢掉的包,tcpdump 能不能抓到?

答案要分方向:

  • 收包方向:tcpdump 通常在网络设备层就能看到包,而 Netfilter 的 PREROUTING/INPUT 在 IP 层。因此外部来的包即使后面被 iptables DROP,tcpdump 也可能已经抓到了。
  • 发包方向:本机发出的包先经过 IP 层的 OUTPUT/POSTROUTING,再到设备层。如果在 Netfilter 阶段被 DROP,tcpdump 可能抓不到这个发出的包。

这能帮助排查一个经典疑惑:抓包能看到入站 SYN,不代表应用一定能收到;因为它可能在 INPUT 链被丢了。抓不到出站包,也可能是 OUTPUT 或 POSTROUTING 前就被规则处理掉了。

六、常用排查命令

查看规则,推荐优先用 -S,它展示的是可复用的命令形式:

iptables -S
iptables -t nat -S
iptables -t mangle -S
iptables -t raw -S

查看计数器,确认规则有没有命中:

iptables -nvL
iptables -t nat -nvL
iptables -t filter -nvL FORWARD

看连接跟踪:

conntrack -L
conntrack -S

看路由和转发:

ip route
sysctl net.ipv4.ip_forward

定位时按包路径问问题:

  1. 这个包是进本机、出本机,还是经本机转发?
  2. 它会经过哪几条链?
  3. 哪张表上的规则最可能影响它?
  4. 规则顺序是否正确?
  5. 计数器有没有增长?
  6. conntrack 里有没有对应连接?

七、扩展阅读