Netfilter masquerade 的 NAT 行为到底是什么

本文为关于 einat-ebpf 的系列文章第(二)章。

Netfilter NAT 工作方式

在 nftables 中,不管是如 snatdnatmasquerade 还是 redirectNAT 操作, 其本质都是改写当前的 conntrack(连接跟踪)记录为 SNAT 或 DNAT conntrack 并修改其中的映射信息,而不是直接改写网络包。 并且一旦 conntrack 的 SNAT 或 DNAT 映射信息被确认1就不能在后续的包处理流中再次更改2,以此确保 NAT 后连接在其活动寿命中的稳定连通性,直至该 conntrack 超时被移除。

而最终的网络包地址和端口改写由 Netfilter NAT 系统在 postroutinginput 钩子的 SNAT 优先级处和 preroutingoutput 的 DNAT 优先级处3根据当前匹配的 conntrack 记录进行。

Conntrack 的 NAT 行为

由于 Netfilter 的 NAT 映射规则完全依附在连接跟踪记录上,对于任意 conntrack 记录它只能匹配来源地址和端口与发出网络包目的地址和端口相同的接收包, 因此它即是有 Address and Port-Dependent Mapping 映射行为的 NAT,并且由此隐含了 Address and Port-Dependent Filtering 的过滤行为。 这也可归类为对称(Symmetric)NAT 类型。

实验

为了验证这一点,我们用 ip netns 配置如下网络拓扑并在路由器上配置 LAN 到 WAN 的 nftables masquerade。以下 shell 命令均默认在 root 权限下运行。

netns 配置示例
#!/usr/bin/env bash
set -eux

ip netns add server1
ip netns add router
ip netns add device1

## add interfaces
ip link add br-lan netns router type bridge
ip link add wan netns router type veth peer veth_s1_r netns server1
ip link add lan1 netns router type veth peer veth_d1_r netns device1

## setup server network
ip netns exec server1 ip addr add 10.0.1.1/24 dev veth_s1_r
ip netns exec server1 ip addr add 10.123.123.100/24 dev veth_s1_r
ip netns exec server1 ip addr add 10.123.123.200/24 dev veth_s1_r
ip netns exec server1 ip link set veth_s1_r up


## setup router external network
ip netns exec router ip addr add 10.0.1.100/24 dev wan
ip netns exec router ip link set wan up
ip netns exec router ip route add default via 10.0.1.1 dev wan
## setup router internal network
ip netns exec router ip link set lan1 master br-lan
ip netns exec router ip addr add 192.168.1.1/24 dev br-lan
ip netns exec router ip link set br-lan up
ip netns exec router ip link set lan1 up


## setup device network
ip netns exec device1 ip addr add 192.168.1.100/24 dev veth_d1_r
ip netns exec device1 ip addr add 192.168.1.200/24 dev veth_d1_r
ip netns exec device1 ip link set veth_d1_r up
ip netns exec device1 ip route add default via 192.168.1.1 dev veth_d1_r


## setup Netfilter masquerade on router
ip netns exec router sysctl net.ipv4.ip_forward=1
ip netns exec router nft --file - <<EOF
    table inet nat {
        chain postrouting {
            type nat hook postrouting priority srcnat; policy accept;
            iifname br-lan oifname wan masquerade random
        }
    }
EOF

## router: show networking info
ip netns exec router ip addr show
ip netns exec router ip route show

virtual network

在设备上向服务器任意发送一个 UDP 包,路由器会转发并如同上一节所描述的创建相应的 conntrack,并最终执行 NAT。

# 进入设备 netns 从本地 192.168.1.100:20000 端口发送 UDP 包到服务器 45678 端口
$ ip netns exec device1 \
  bash -c 'echo test | nc -q0 -u \
    -s 192.168.1.100 -p 20000 10.123.123.100 45678'
# 从另一地址 192.168.1.200 的相同源端口发送,以预先占用对目的端口 12345 的外部源端口 20000
$ ip netns exec device1 \
  bash -c 'echo test | nc -q0 -u \
      -s 192.168.1.200 -p 20000 10.123.123.100 12345'
# 从与第一条命令相同的本地源地址和端口发送到不同的服务器 12345 端口
$ ip netns exec device1 \
  bash -c 'echo test | nc -q0 -u \
      -s 192.168.1.100 -p 20000 10.123.123.100 12345'
# 进入路由器 netns 查看新创建的 conntrack
$ ip netns exec router \
    conntrack -L
# 这里根据 conntrack 创建顺序重新排序
# 对应第一个 UDP 包的 conntrack
(1) (协议)udp    (协议号)17 (超时时间)29
(原始连接信息, original)
  src=192.168.1.100 dst=10.123.123.100 sport=20000 dport=45678 [UNREPLIED]
(SNAT后反向连接信息, reply)
  src=10.123.123.100 dst=10.0.1.100 sport=45678 dport=20000
mark=0 use=1

# 对应第二个 UDP 包的 conntrack
(2) (协议)udp    (协议号)17 (超时时间)29
(原始连接信息, original)
  src=192.168.1.200 dst=10.123.123.100 sport=20000 dport=12345 [UNREPLIED]
(SNAT后反向连接信息, reply)
  src=10.123.123.100 dst=10.0.1.100 sport=12345 dport=20000
mark=0 use=1

# 对应第三个 UDP 包的 conntrack
(3) (协议)udp    (协议号)17 (超时时间)29
(原始连接信息, original)
  src=192.168.1.100 dst=10.123.123.100 sport=20000 dport=12345 [UNREPLIED]
(SNAT后反向连接信息, reply)
  src=10.123.123.100 dst=10.0.1.100 sport=12345 dport=53801
mark=0 use=1

观察 conntrack 记录的连接信息,其中的原始连接信息即反应从内部视角所看到的连接信息,而反向连接信息就是从外部视角所看到的连接信息。 对于未经过 masquerade 等 NAT 操作改写的 conntrack,其原始连接信息与反向连接信息完全对称, 即 original.{src, sport} = reply.{dst, dport}original.{dst, dport} = reply.{src, sport}。 而对于经过 masquerade 等 SNAT 操作改写的 conntrack 则会将对应原始源地址端口的 reply.{dst, dport} 改写为选定的外部地址和端口,以待后续实际改写匹配的网络包。 如第一条 conntrack 的外部地址和端口就被改写为了 10.0.1.100:20000,即路由器的外部出口地址。

我们这里暂且忽略第二条 conntrack 记录,注意到第一条和第三条 conntrack 的原始连接信息除了目的端口外的源地址、端口和目的地址都相同, 即 192.168.1.100:20000 --> 10.123.123.100:*。然而因为目的端口不同 masquerade 选定了不同的外部端口, 即根据目的端口的 4567812345 分别映射为了外部源端口的 2000053801,而这正是目的地址和端口关联的 NAT 映射行为, 即 Address and Port-Dependent Mapping

Netfilter masquerade 是 EIM?

然而如果有在 Netfilter masquerade 下使用 STUN 测试过 NAT 行为的同学可能有疑惑, 明明测出来的是 Endpoint-Independent Mapping 加 Address and Port-Dependent Filtering(对应 Port Restricted Cone NAT), 而且是可以正常在 P2P 应用中轻松打洞的。 这不像是 Symmetric NAT(即我们上文所得出的 APDM + APDF)的行为。

比如我们在服务器上设置 stunserver 并在设备上使用 stunclient 测试 NAT 行为:

$ ip netns exec server1 \
    stunserver --mode full --primaryinterface 10.123.123.100 \
      --altinterface 10.123.123.200
# 另起一个 shell
$ ip netns exec device1 \
    stunclient --mode full --localport 29999 10.123.123.100
# 测试结果为 EIM + APDF
Binding test: success
Local address: 192.168.1.100:29999
Mapped address: 10.0.1.100:29999
Behavior test: success
Nat behavior: Endpoint Independent Mapping
Filtering test: success
Nat filtering: Address and Port Dependent Filtering

测试所创建的 conntrack 记录为:

$ ip netns exec router \
    conntrack -L
# 这里不再标注项目,标注见上文。这里根据 conntrack 创建顺序重新排序
(1) udp      17 116
  src=192.168.1.100 dst=10.123.123.100 sport=29999 dport=3478
  src=10.123.123.100 dst=10.0.1.100 sport=3478 dport=29999 [ASSURED] mark=0 use=1
(2) udp      17 20
  src=10.123.123.200 dst=10.0.1.100 sport=3479 dport=29999 [UNREPLIED]
  src=10.0.1.100 dst=10.123.123.200 sport=29999 dport=3479 mark=0 use=1
(3) udp      17 26
  src=10.123.123.100 dst=10.0.1.100 sport=3479 dport=29999 [UNREPLIED]
  src=10.0.1.100 dst=10.123.123.100 sport=29999 dport=3479 mark=0 use=1
(4) udp      17 29
  src=192.168.1.100 dst=10.123.123.200 sport=29999 dport=3478
  src=10.123.123.200 dst=10.0.1.100 sport=3478 dport=29999 mark=0 use=1

STUN 客户端首先向 STUN 服务器主要地址3478 端口发送了请求并收到回复,对应 conntrack (1) 且外部端口映射为 29999。 然后 STUN 服务器分别从副地址和主要地址的不同端口 3479 向外部端口发送请求,由于 conntrack 有 APDF 的过滤行为,这里被视为向路由器本身发起连接,这符合我们的预期。 最后 STUN 客户端从相同的内部源地址和端口向 STUN 服务器的次要地址发送了请求,然而却映射了和 conntrack (1) 相同的外部端口 29999, 于是 STUN 客户端判断 NAT 行为是目的无关的 EIM。

第 (1) 条和第 (4) 条 conntrack 看似是有相同的外部源地址和端口所以符合 EIM 行为,但实际上这仍符合 APDM 的定义。 这是因为 Netfilter masquerade 在不产生冲突的前提下会优先将内部源地址和端口映射为相同外部源端口或和之前有相同内部源地址和端口 conntrack 记录的相同外部源端口。 但要符合 EIM 的关键是所有的连接所对应的映射规则都需要符合 EIM 的目的无关的定义,而这并不适用于 conntrack 的全局情况, 因为我们可以很简单地构建额外的 conntrack 来破坏这看似是 EIM 的映射行为。

比如事先以其他源地址占用以对于 STUN 服务器次要地址的 3478 端口所映射的 29999 源端口,这就会使上 conntrack (4) 的外部端口被映射为其他随机端口, 从而 STUN 客户端不能在检测出 EIM 而是降级为 ADM。这里检测出 ADM 是因为我们只是将对于目的次要地址的外部源端口映射为其他端口, 但是对于目的次要地址的目的次要端口 3479 还是会优先映射为同其他端口。这里的重点是 conntrack 只能保证 APDM 的下限情况,而更宽松的 NAT 映射行为并不绝对。 当然如果我们将 masquerade 配置为随机分配端口, 则可以稳定的检测出 APDM。

$ ip netns exec device1 \
    bash -c 'echo test | nc -q0 -u \
      -s 192.168.1.200 -p 29999 10.123.123.200 3478'
$ ip netns exec device1 \
    stunclient --mode full --localport 29999 10.123.123.100
...
Nat behavior: Address Dependent Mapping
...

此外可以看出,STUN 的 NAT 映射行为检测并不适用于检测 NAT 全局的映射行为。

Netfilter masquerade 端口映射

上一节我们提到 Netfilter masquerade 在不产生冲突的前提下会优先将内部源地址和端口映射为相同外部源端口4或和之前有相同内部源地址和端口 conntrack 记录的相同外部源端口5, 这在一定程度上给出了和 EIM 类似的映射行为。所以在“Netfilter NAT 工作方式”一节的实验中我们主动制造冲突的 conntrack 以破坏这行为从而印证 Netfilter masquerade 及其依赖的 conntrack 的 APDM 的本质。

尽管其 EIM 类似的映射行为并不是绝对的,但是对于一个内部源地址和端口,只要它连接的目的地址和端口与源自其他的内部地址、端口的连接不大相同,就可以保证它对应的连接中的大部分的端口映射规则是 EIM。 而正如上面 STUN 可以测出 EIM 的 NAT 行为一样,对于 P2P 应用来说只要对其能在一定程度上保证外部端口映射的稳定就可成功“打洞”,更何况即使产生了冲突也可以尝试其他端口, 而 65535 个端口对于用户数为个位或十位的家庭、办公室场景不算很少。

所以如果你的应用场景只是为了一对一 P2P 应用可以正常工作,Netfilter masquerade 的类似 EIM 行为完全可以满足需要,你无需特意去追求 Full Cone NAT。 但如果你希望 NAT 网络后的设备可以开放端口(特别是 TCP 端口)并允许任何远端访问,则你需要严格的 EIM 所允许的 EIF,这是 Netfilter masquerade 所无法满足的,而你或许希望部署 einat-ebpf

基于 Conntrack 的 Full Cone NAT

这里顺便提一下 netfilter-full-cone-nat 及其启发的 nft-fullcone 实现。

要实现 Full Cone,EIM 是 EIF 必要条件, 上述内核模块所提供的在 postrouting 钩子 SNAT 阶段工作的 fullcone 操作维护了 EIM 的映射规则表并保证了所修改的 conntrack 符合维护的映射规则以提供更严格的 EIM 行为,其为 masquerade 的提供严格 EIM 的上位替代。

而上述内核模块所提供的在 prerouting 钩子 DNAT 阶段工作的 fullcone 操作则将当前连接的目的地址和端口与映射规则表中的外部源地址和端口进行匹配,并将当前 conntrack DNAT 至匹配映射规则中的原始内部源地址和端口, 即达到 EIF 的效果。

这个模型的缺陷是它只能掌控通过该 fullcone SNAT 或 DNAT 操作的 conntrack,而对因其他网络路径所创建的 conntrack 则无法纳入此套模型的管理,从而导致 EIM 或 EIF 行为的降级。 此外的一个实现缺陷是,它们对于 DNAT 阶段未匹配映射规则的 conntrack 未进行 rejectdrop,从而导致未经映射规则管理的连接 NAT 主机本身的 conntrack 创建,从而破坏 EIF 状态, 而该 conntrack 对应外部主机则永远无法通过相应外部源端口连通被 SNAT 的内部主机端口直至该 conntrack 超时被移除而重新可以被 fullcone DNAT 操作接管。

Footnotes

  1. https://github.com/torvalds/linux/blob/2d412262ccfd100218412b4b52d92d6a7bb043a4/net/netfilter/nf_nat_core.c#L725-L731

  2. https://github.com/torvalds/linux/blob/2d412262ccfd100218412b4b52d92d6a7bb043a4/net/netfilter/nf_nat_core.c#L667-L676

  3. https://github.com/torvalds/linux/blob/2d412262ccfd100218412b4b52d92d6a7bb043a4/net/netfilter/nf_nat_proto.c#L828-L857

  4. https://github.com/torvalds/linux/blob/ed30a4a51bb196781c8058073ea720133a65596f/net/netfilter/nf_nat_core.c#L610-L624

  5. https://github.com/torvalds/linux/blob/ed30a4a51bb196781c8058073ea720133a65596f/net/netfilter/nf_nat_core.c#L359-L384