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都已经完成了,真正的路由之后就可以从另一个主接口发出了。
这次先不急着自己写虚拟网卡,先折腾完loopback再说,那么现在动手吧!
1.对代码的修改:
重新封装RAW表的NF_INET_PRE_ROUTING钩子函数,在ipt_hook的调用前调用下面的逻辑:
//判断有点太鲁莽,正常应该可以设计成一个匹配算法的
if (skb->dev->flags & IFF_LOOPBACK && skb->nfct) {
    skb->nfct = &nf_conntrack_untracked.ct_general;
    skb->nfctinfo = IP_CT_NEW;
    nf_conntrack_get(skb->nfct);
    skb_dst_drop(skb);
    return NF_ACCEPT;
} 
这段代码的意思是说,如果是数据包从物理网卡进入,显然是需要匹配和应用规则(比如NAT)的,如果这件事做完了,数据也就是要通过路由导入loopback接口了,此时就不要再使用conntrack了,然而此时skb的nfct可能已经被设置了,于是将其NOTRACK,并且将skb的路由缓存丢弃。Linux的IP路由是这么对待loopback的,如果路由查询的结果出口是loopback接口,就是直接设置dst,loopback的xmit将数据包发出,调用一个netif_rx重新接收,到达ip_rcv_finish的时候,由于已经有了dst,就不必再查询路由了。但是如果这样的话,我们的第二次路由查询-实际上是策略路由的查询将无法实现,因此必须drop掉原有的dst。
2.配置IP地址和Netfilter策略
IP地址:
3: eth1: mtu 1500 qdisc pfifo_fast state DOWN qlen 1000
link/ether 00:0c:29:90:66:c5 brd ff:ff:ff:ff:ff:ff
inet 192.168.2.249/24 brd 192.168.2.255 scope global eth2
4: eth2: mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 00:0c:29:90:66:cf brd ff:ff:ff:ff:ff:ff
inet 192.168.40.249/24 scope global eth2

说明:暂用两块网卡,eth1连接内部,eth2连接外部

NAT表:
-A POSTROUTING -j SNAT -i eth1 --to-source 192.168.40.249
说明:将所有的发起于内部的数据包源IP都转换成本机的IP地址。
RAW表:
-A PREROUTING -i lo -j MARK --set-xmark 100
说明:从lo口进入,说明第一次路由查询导致的NAT已经完成,打上MARK让后续的路由逻辑将其识别为第二次真正的路由查询。


策略:

0: from all lookup local
32764: from all fwmark 0x64 lookup loop
32766: from all lookup main
32767: from all lookup default
说明:这是一个策略,匹配一个FWMARK,该MARK由RAW表打上,用于识别是第一次路由查询还是第二次路由查询,第一次路由查询用来实现NAT和一切和conntrack相关的操作,第二次查询实现真正的IP路由。


主路由:
default dev lo scope link
说明:主路由表中什么都没有了,就有一条默认路由通过loopback接口发出。如上所述,这次的路由查询是“第一次有关NAT”的路由查询,目的就是要让数据包经过一次PRE-ROUTING和POST-ROUTING。


策略路由表loop:
192.168.40.249 dev eth2 scope link
192.168.2.0/24 dev eth1 scope link src 192.168.2.249
192.168.40.0/24 dev eth2 scope link
default via 192.168.40.254 dev eth2
说明:我是将所有的main路由表中内容全部搬到策略路由表了,包括直连的链路层路由。因为我希望我的这套地址仅仅作为IP路由+NAT的辅助地址存在,就连直连的主机也不能通过直连路由发出了,因为main表中不再有直连路由了,所以直连流量也会进入loopback,完成故作的第一次路由之旅。凡是数据包打上了100标签,就会去查询loop路由表,如上所述,这次路由查询是“第二次真正的路由查询”。此次查询就可以用source做policy routing了,因为源地址转换已经完成了。


Local路由表:
local 192.168.2.249 dev eth2 proto kernel scope host src 192.168.2.249
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
说明:参与SNAT的地址要从Local表中删除。因为IP路由不允许从本机发起的包经过loopback接口发往其它地方,如果SNAT将过路数据包的源地址转换成了一个本机的IP地址,那么在反向路由验证的时候,就会失败。详细点说还是由于Linux对待loopback流量的方式导致,由于Linux协议栈在output的时候就设置input的路由项,所以根本就不会到达ip_route_input,因此不允许loopback流量被forwarding。 但是,将物理网卡上的IP地址移出Local表有个副作用,那就是ARP逻辑不会再回复针对这些IP的ARP请求,因为将地址移出Local表相当于放弃了该地址的所有权。但是针对这个“下一跳解析协议”的问题有很多解决方法,比如静态设置,比如专门配置一个代理,比如arping。


缺陷:
本实现虽然大体实现了NVI的功能,但是还缺点什么。比如数据包经过loopback接口这么一绕,原始的入接口信息就会丢失,需要复杂的conntrack规则才能将其映射到IP-FWMARK。

分离的IP地址用途的体现

本例中,内网口和外网口上配置的IP地址完全用于路由,不再属于本机,你不能指望通过这些IP地址来访问BOX本身,因为为了让反向路由检查可以通过,这些地址标示的Local路由已经从Local表中被删除了,所以它们不再标示主机。另外值得注意的是,不单单如此,该BOX连针对这些配置在物理网卡上IP地址的ARP请求都不会回复,因为Linux是否回复ARP请求是基于可以在Local表中查到该IP表示的路由这个前提的。

总结一下

再次可以看到,你可以多么自由地对Linux进行修改和定制,甚至完全可以破坏一些公认的原则,但是前提是你必须知道这些原则被破坏掉以后的后果是什么,你有充分的这么做的理由并且必须这么做,也许,你还没有发现更好的方法,现在只能这么做。不管怎么说,做点自由的事情,总会有很多收获!