@@ -38,6 +38,8 @@ POSTROUTING / `NF_INET_POST_ROUTING`
3838
3939: 数据包在离开主机由网卡发出之前,进入 POSTROUTING 阶段。这个阶段通常用于更改源地址(SNAT)。
4040
41+ 这些阶段对应 iptables 的内置链(chains)或 nftables 的 hook 点。
42+
4143从主机的视角来看,数据包经过 Netfilter 的各个阶段的路径如下图所示:
4244
4345![ Netfilter 阶段] ( ../../images/netfilter-host-view.svg )
@@ -59,10 +61,42 @@ POSTROUTING / `NF_INET_POST_ROUTING`
5961 {#wikipedia-netfilter-packet-flow}
6062
6163 它与本文的图示有一处微妙的区别:路由决策位于 OUTPUT 阶段之前,而 OUTPUT 阶段后另有一个 Reroute check。
62- 事实上此图是更加准确的,但在大多数情况下,将路由决策视作位于 OUTPUT 之后更容易理解,尤其是这样能保持 OUTPUT 阶段与 PREROUTING 阶段的相似性(均在路由决策之前)以及路由结果的准确性。
64+ 事实上此图是更加准确的,但在大多数情况下,将路由决策视作位于 OUTPUT 之后更容易理解,可以从以下两点看出:
65+
66+ 1. 保持 OUTPUT 阶段与 PREROUTING 阶段的相似性:两个阶段均发生在路由决策之前,且 NAT 模式为仅可更改目的地址(DNAT)。
67+ 2. 路由结果的准确性:数据包最终的路由结果基于经过 OUTPUT 或 PREROUTING 阶段修改后的元信息,如目的地址和防火墙标记等。
68+
6369 本文在介绍 iptables 的表时绘制了 [Netfilter 视角的阶段图](#netfilter-kernel-view-tables),能够更直观地反映出此「相似性」。
6470
65- ??? info "Reroute check 的细节(待补充)"
71+ ??? info "Reroute check 的细节"
72+
73+ 首先需要重复的一点是:数据包最终的路由结果是基于经过 OUTPUT 阶段后、进入 POSTROUTING(或 INPUT)阶段前的状态决定的。
74+ 那么既然数据包在 OUTPUT 阶段可能发生 MARK 或 DNAT 等修改,为什么不像外部传入的数据包一样,直接在 OUTPUT 阶段后进行路由决策呢?
75+ 作者认为有以下两种可能的原因:
76+
77+ 1. 只是一个历史遗留问题:早期的内核可能并没有考虑到 OUTPUT 阶段修改信息会导致路由变化,因此在数据包由本机进程发出后,直接进行路由决策。
78+ 2. 「由本机往网卡发出」的数据包只会经过 OUTPUT 和 POSTROUTING 两个阶段,而仅有 OUTPUT 阶段[具有](#iptables-tables-validity) filter 表。在这种情况下,若要限制本机进程允许发出数据包的网络接口,则 OUTPUT 阶段必须支持 `-o` 参数,即需要在 OUTPUT 阶段前进行一次(初步的)路由决策。
79+
80+ 为了兼顾「在 OUTPUT 链中可以使用 `-o`」和最终路由决策的正确性,内核采用了 Reroute 机制[^ip_route_me_harder],即:
81+
82+ - 数据包由本机进程 `send()` 到 Netfilter 之后,首先进行一次路由决策,确定初步的输出接口。
83+ - 在 mangle 表中,若数据包的源地址、目的地址、防火墙标记或 TOS 字段这 4 个元信息发生了变化[^ip6t_mangle_out],则重新进行一次路由决策[^ip_route_me_harder.mangle]。
84+ - 在 nat 表中,若数据包的目的地址发生了变化,则还会重新进行一次路由决策[^ip_route_me_harder.nat]。
85+
86+ 需要注意的是,尽管数据包可能在 mangle 表和 nat 表中已经经过了至多两次额外路由决策,但其在 filter 表中时,`-o` 参数所匹配的输出接口始终是最初的路由决策结果。
87+ 这是因为最终生效的路由决策存储在数据包的 `skb->_skb_refdst` 字段中[^skb._skb_refdst],而 Netfilter 在进行匹配时使用的是 `nf_hook_state->out` 字段[^ipt_do_table],该字段在数据包进入 OUTPUT 阶段之前就已经确定,并不会随着后续的 reroute check 而改变。
88+
89+ 在搞清楚这些细节后,我们就能理解为什么以下两种理解方式都是正确的:
90+
91+ 1. 路由决策位于 OUTPUT 之后:因为数据包最终的路由结果是基于经过 OUTPUT 阶段修改后的状态决定的。
92+ 2. 路由决策位于 OUTPUT 之前,且 OUTPUT 后另有重新路由:因为 OUTPUT 阶段需要支持 `-o` 匹配方式,该信息依赖于初步的路由决策结果。
93+
94+ [ ^ ip_route_me_harder ] : [ ` ip_route_me_harder ` ] ( https://elixir.bootlin.com/linux/v6.17.8/source/net/ipv4/netfilter.c#L21 ) 或 [ ` ip6_route_me_harder ` ] ( https://elixir.bootlin.com/linux/v6.17.8/source/net/ipv6/netfilter.c#L23 ) 函数
95+ [ ^ ip6t_mangle_out ] : IPv6 采用的判断条件有所不同,具体请阅读 [ ` ip6t_mangle_out ` ] ( https://elixir.bootlin.com/linux/v6.17.8/source/net/ipv6/netfilter/ip6table_mangle.c#L52 ) 函数。
96+ [ ^ ip_route_me_harder.mangle ] : [ ` ipt_mangle_out ` ] ( https://elixir.bootlin.com/linux/v6.17.8/source/net/ipv4/netfilter/iptable_mangle.c#L63 ) 函数
97+ [ ^ ip_route_me_harder.nat ] : [ ` nf_nat_ipv4_local_fn ` ] ( https://elixir.bootlin.com/linux/v6.17.8/source/net/netfilter/nf_nat_proto.c#L767 ) 函数
98+ [ ^ skb._skb_refdst ] : [ ` skb_dst_set ` ] ( https://elixir.bootlin.com/linux/v6.17.8/source/include/linux/skbuff.h#L1173 ) 函数
99+ [ ^ ipt_do_table ] : [ ` ipt_do_table ` ] ( https://elixir.bootlin.com/linux/v6.17.8/source/net/ipv4/netfilter/ip_tables.c#L245 ) 函数
66100
67101### Hook 的优先级 {#netfilter-hook-priorities}
68102
@@ -123,7 +157,7 @@ Conntrack 除了记录连接的五元组(四层协议、源地址、目的地
123157 - 此时 conntrack 记录该连接** 双向** 的五元组(共 9 个字段,其中协议号只需记录一次),包括正向(original)的四元组和反向(reply)的四元组
124158- 后续的数据包(不论方向)经过该路由器时,在 conntrack 阶段(` NF_IP_PRI_CONNTRACK ` )匹配到某一方向的四元组,由 conntrack 改写为另一方向的四元组的反向地址[ ^ nf_nat_manip_pkt ] ,使对端主机能够正确接收数据包。
125159
126- [ ^ nf_nat_manip_pkt ] : 参见 [ ` nf_nat_manip_pkt ` ] ( https://elixir.bootlin.com/linux/v6.17.8/source/net/netfilter/nf_nat_proto.c#L383 ) 函数。
160+ [ ^ nf_nat_manip_pkt ] : [ ` nf_nat_manip_pkt ` ] ( https://elixir.bootlin.com/linux/v6.17.8/source/net/netfilter/nf_nat_proto.c#L383 )
127161
128162#### conntrack 命令 {#conntrack-command}
129163
@@ -135,7 +169,7 @@ Conntrack 除了记录连接的五元组(四层协议、源地址、目的地
135169conntrack -L
136170```
137171
138- !!! example "conntrack 输出示例"
172+ ??? example "conntrack 输出示例"
139173
140174 ```text
141175 udp 17 91 src=192.0.2.2 dst=8.8.8.8 sport=39043 dport=53 src=8.8.8.8 dst=198.51.100.1 sport=53 dport=39043 [ASSURED] mark=1 use=1
336370iptables 的每个表都会注册到对应的 Netfilter hook 优先级上,因此同一个阶段(例如 PREROUTING)中,不同表的处理顺序与 [ hook 的优先级] ( #netfilter-hook-priorities ) 相同。
337371
338372各个表在各个阶段的可用性如下表所示:
373+ {: #iptables-tables-validity }
339374
340375| 阶段 | filter / security | nat | mangle | raw |
341376| :---------: | :--------------------------------------: | :-------: | :--------------------------------------: | :--------------------------------------: |
0 commit comments