Loopback实际上是个hole
但是如果它不是一个hole,它确实可以做一些事,类似Cisco的NVI那样。既然前期是“如果它不是一个hole”,那就需要对代码进行一些修改。在修改之前,你必须明白的是,Linux的loopback接口为什么是一个hole。
标准规定,所有试图经过loopback接口去往其它地方(非本机)的数据包要全部丢弃。Linux使用loop hole做到了这一点。Linux的限制loopback流量在本机范围的方式是,所有的loopback流量肯定经由本机发送,那么在ip_output的时候就会将其设置为loopback_dst,然后进行入IP接收例程的时候,它已经有关联的路由项了,进而就不会再去查询路由表,因此凡是进入ip_input逻辑的数据包都不是本机发出的,于是在其内部就可以做比较狠的判断的,凡是源地址是本机地址的,一律丢弃!这样本机发出的包就不会先经由loopback口然后去往外部,下面我们看一下外部进入的包是否能经由loopback口去往外部。答案无疑是否定的,看下面的流程:数据包从物理网卡进入->被路由到lo口->将loopback_dst这个路由项关联给数据包->loopback接口xmit数据包->模拟loopback接口接收数据包->进入ip_input路由判断->由于已经有了路由项故按照路由项转发。路由项的转发方式有两种,对于外部进入的数据包,将不断调用ip_forward,直到TTL变为0。因此只要进入了loopback,要么直接丢弃,要么疯狂loop,是绝对出不去的。
下面我就来说一下如何来破除这些约束。首先说一下本机发出的数据包如何先经由loopback再出去,然后说明外部进入的数据包如何先经由loopback再出去,最后说明,当做NAT的时候会碰到什么问题以及如何结合上述针对本机发包以及外部发包两种场景的措施来解决NAT问题。
1.本机发包经由loopback发出
修改代码是不必可少的了,因为我这是在破坏原则。幸运的是,代码只是修改一点点而已。修改的部分就是将这种“经由loopback发往别处”的包识别出来,然后删除其关联的路由项。这个用Netfilter在PREROUTING上做比较简单。另外就是将表示该本机地址的Local路由从Local表删除,然后作为unicast路由加入main表中,这样在做反向路由查询的时候,就不会匹配到Local表的路由了(Linux要求反向路由的类型必须是unicast的),到此即OK!
2.外部发包经由loopback转发
对于这种情况,只要是删除了数据包的loopback路由项关联,即可被顺利转发。因为数据包的源IP地址不可能是本机的IP,因此也就不可能是Local,如果数据流想原路返回的话,它就一定有反向的unicast路由。
3.NAT的问题
在配置了SNAT的情况下,要看SNAT成了什么地址,如果是SANT成了本机地址,那就面临上述第1节的问题,解决方法就是将该地址从Local表中删除,但是删除了之后会导致其它机器arp该地址的时候,本机不再回复,因此删除了之后还要显式arping一下该地址的arp更新;如果SNAT成了别的地址,就涉及到了反向可达性的问题,因为下一跳不一定知道该地址的可达性。
4.NAT问题的解决
NAT的问题仅仅是在SNAT成了别的地址时才会存在。这里又分为两种情况,第一种情况就是SNAT成了一个不相关的其它网段的地址,这样仅仅要求下一跳配置到该地址的路由就可以保证数据流的反向包能返回到此BOX,这个路由配置在简单环境下可以手工配置,复杂环境下可以用动态路由的方式进行SNAT地址的宣告;第二种情况就是SNAT的地址是和下一跳同一网段的情况,这会导致数据流反向包返回到下一跳的的时候,该SNAT的地址此时成了目标地址,由于处于同一网段,所以会被直接ARP,因此需要添加一条ARP转换规则:
arptables -t mangle -A OUTPUT -d 下一跳网关地址 -j mangle --mangle-ip-s SNAT成的地址
知道了问题所在以及解决方案,现在就可以动手了。本文的目标是实现一个类似Cisco NVI的东西,也就是一个虚拟网卡,在虚拟网卡的发送流程中实现NAT。鉴于有loopback这么好的现成的东西,我也就不再写虚拟网卡了,直接用loopback模拟一个也好。大体流程如下:
数据包从物理网卡进入->执行DNAT->路由到loopback->执行SNAT->loopback口发出->策略路由->物理网卡发出
可以看到,路由执行了两次,第一次是为了NAT,第二次是真正的路由。
除了使用loopback,编写一个类似veth的虚拟网卡是一个更不错的选择:
Veth stands for Virtual ETHernet. It is a simple tunnel driver that works at the link layer and looks like a pair of ethernet devices interconnected
with each other.
比loopback好的是,这基本可以不修改代码实现NVI,并且可以很容易取到数据包原始的进入接口。该驱动的逻辑非常简单,即一个pair中包含一个主接口和一个辅助接口,数据包从主接口进入被路由到该主接口的辅助接口,注意,不改变skb的接收接口,这个所谓的路由只是为了搞一次“从物理网卡接收到发送到某另一个网卡的动作”,此时PREROUTING/POSTROUTING都已经完成了,真正的路由之后就可以从另一个主接口发出了。