多线接入主机上的诡异NAT

昨天,mirrors.ustc.edu.cn 遇到了一件诡异的事情。mirrors 有三条接入线路,IP分别是 202.38.95.110,202.141.160.110,202.141.176.110。mirrors-lab 是 mirrors 上的一台 LXC 虚拟机,有三个IP:10.8.95.2,10.8.140.2,10.8.10.2。

在 mirrors 主机上,配置了 iptables 将主机上的 50000~51000 端口直接映射到虚拟机内:

-A PREROUTING -p tcp -d 202.38.95.110 -m multiport --dports 50000:50100 -j DNAT --to 10.8.95.2
-A PREROUTING -p tcp -d 202.141.160.110 -m multiport --dports 50000:50100 -j DNAT --to 10.8.10.2
-A PREROUTING -p tcp -d 202.141.176.110 -m multiport --dports 50000:50100 -j DNAT --to 10.8.140.2

在虚拟机的 50000 端口运行了 rsync daemon,但只有 rsync://202.38.95.110:50000 能够访问,另外两个IP都是超时。诡异的是,我们在 mirrors 虚拟机和主机上用 tcpdump 抓包,看起来 SYN 已经收到,ACK 包也已经送出。tcpdump 抓入站包是在 netfilter 之前,我们抓的是物理网卡 eth0,入站包在被抓到时应该还没到 iptables,出站包在被抓到时已经通过了 iptables。更诡异的是,在与 mirrors 同属于一个网段的 blog 服务器上,三个IP访问都是正常的。为什么回复包发不出局域网呢?

经过郭家华认真的调试(此处略去10000字),问题发生在 ip rule 上。下面是 mirrors 主机原来的 ip rule:

$ ip rule
0:      from all lookup local
32761:  from all to 221.224.40.18 lookup 101
32762:  from 202.141.176.110 lookup 102
32764:  from 202.141.160.110 lookup 101
32765:  from 202.38.95.110 lookup 100
32766:  from all lookup main
32767:  from all lookup default

Linux 采用了策略路由(Policy Routing)机制,也就是可以有若干张路由表,按照一定的优先级排列,如果一个路由表没有匹配上任何规则,就再去寻找优先级稍低的路由表。以上面的路由规则为例,首先匹配优先级为0的路由表,那里面都是本地路由规则,也就是匹配所有发往本机的数据包;如果匹配不上,就会匹配优先级为32761的,再匹配优先级为32762的,以此类推。

优先级32766是main,也就是“主要”的啦,我们用 ip route 命令添加的路由规则默认就在这个路由表里。main 里一般有个默认路由 default,也就是前面没有匹配上的都会走到这里。mirrors 上的默认路由是 default via 202.38.95.126 dev vlan95。32767是最后的默认,mirrors 上没有配置,其实也不会走到这里。

为什么要这么配置呢?试想 mirrors 从 vlan10 线路进来一个数据包。自然,回复的数据包也要从 vlan10 出去,不然如果所有出去的数据包都走一条线路,三线接入还有什么用呢?Linux 的路由表是基于目标地址的,我怎么保证回复 vlan10 的数据包,也就是本机发出的源地址为 202.141.160.126 的数据包,一定从 vlan10 出去呢?这就是策略路由发挥威力的地方了。通过下面的配置,回复包在匹配优先级32764的规则时,发现数据包的来源匹配上了,进入路由表101,路由表里正好有一条 default 规则,于是这个包就从 vlan10 出去了。

$ ip route show table 101
default via 202.141.160.126 dev vlan10

说了这么多,跟开头的古怪问题似乎没有关系啊。且慢,我们还要看看 iptables 的内核实现 netfilter。

   --->PRE------>[ROUTE]--->FWD---------->POST------>
       Conntrack    |       Mangle   ^    Mangle
       Mangle       |       Filter   |    NAT (Src)
       NAT (Dst)    |                |    Conntrack
       (QDisc)      |             [ROUTE]
                    v                |
                    IN Filter       OUT Conntrack
                    |  Conntrack     ^  Mangle
                    |  Mangle        |  NAT (Dst)
                    v                |  Filter

由上图(来源)可见,在 netfilter 中,NAT 事实上是分成 SNAT 和 DNAT 两部分的。而刚才我们说的 ip rule,则是在上图的 [ROUTE] 部分。也就是说,首先修改目标地址,再路由,再修改源地址。(顺便说一句,这也是 DNAT 在 PREROUTING,SNAT 在 POSTROUTING 的原因)

从外面某机器(设为 202.38.70.7)经网卡 vlan10 到达 202.141.160.110:50000 的 rsync 请求(TCP SYN),在 PREROUTING 阶段被修改目标地址为 10.8.10.2,送进虚拟机,虚拟机收到来自 202.38.70.7 的 TCP SYN,回复一个来自 10.8.10.2,目标为 202.38.70.7 的 TCP SYN+ACK。现在这个回复包到达了主机的 netfilter。由于修改源地址是在 POSTROUTING 阶段,因此在经过路由部件时,源地址仍然是 10.8.10.2。回顾上面的 ip rule,它不满足优先级 0~32765 的路由表的“进入规则”,因此进入了 main。在 main 中,它选择了默认路由,走到了 vlan95 网卡。

在同一网段的 blog 上测试时,由于链路层数据包的目标 MAC 地址直接是 blog 的网卡,看起来没有任何问题。而在不同网段的机器上测试时,vlan95 的网关设备(本来是管 202.38.95.110/25 的)看到源 IP 地址是 202.141.160.110,WTF?有可能就丢包了。搞明白了原理,这个诡异的问题看起来一点也不诡异了。

这个问题最终的解决,是添加了3条 ip rule:(同时我也把它放进了 rc.local)

ip rule add from 10.8.95.2 table 100
ip rule add from 10.8.10.2 table 101
ip rule add from 10.8.140.2 table 102