Skip to content

Commit 6154cb9

Browse files
committed
firewall: Note quirk on reroute check
1 parent a7b9e30 commit 6154cb9

File tree

1 file changed

+39
-4
lines changed

1 file changed

+39
-4
lines changed

docs/ops/network/firewall.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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 除了记录连接的五元组(四层协议、源地址、目的地
135169
conntrack -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
@@ -336,6 +370,7 @@ raw
336370
iptables 的每个表都会注册到对应的 Netfilter hook 优先级上,因此同一个阶段(例如 PREROUTING)中,不同表的处理顺序与 [hook 的优先级](#netfilter-hook-priorities)相同。
337371

338372
各个表在各个阶段的可用性如下表所示:
373+
{: #iptables-tables-validity }
339374

340375
| 阶段 | filter / security | nat | mangle | raw |
341376
| :---------: | :--------------------------------------: | :-------: | :--------------------------------------: | :--------------------------------------: |

0 commit comments

Comments
 (0)