关于Linux内核引入的accept_local参数的一个问题

版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! https://blog.csdn.net/dog250/article/details/78746198

我本没有工作日写东西的习惯,但是前些天跟同事一起研究了一个我很感兴趣的问题,最后总结了一道自认为比较好的题目想分享出来,同时感谢同事赠票之恩,就不得不放弃看动漫的时间,来写点东西了。送给这位同事!


前些天讨论了一个问题,在问题搞定之后,我就着这个话题又多想了一些,最终折腾出一道非常不错的题目,我觉得可以作为面试题选用,旨在考查应聘者对Linux IP路由实现,Policy Routing,iptables等知识点的掌握程度,从这道题目中,可以引申出更多的题目之外的东西,我会在描述的过程中一一指出。

  首先我给出一个单机拓扑(仅仅在一台Linux机器上说事儿,不涉及外部):

这里写图片描述

很简单,就是一台机器,配置一对儿veth网卡(忽略掉eth0,它仅仅作为管理用,用来远程登录),然后网卡的配置如下:

ip link add veth0 type veth peer name veth1
ip addr add dev veth0 1.1.1.2/24
ip addr add dev veth1 1.1.1.1/24
# 以下一条为关键,down掉loopback
ip link set dev lo down 

然后我给出问题:如何用1.1.1.2作为源IP来ping通地址1.1.1.1。

  值得注意的是,为了让问题复杂些,这里不允许使用虚拟化技术,比如docker,net namespace之类的东西,请回答上述的问题。


这里是思考!




如果你去执行

ping 1.1.1.1

或者如果你知道ping还有丰富的参数,比如

ping 1.1.1.1 -I veth0

无论如何,事实证明肯定是不通的,不信你去试试。看样子,这种对工具的表面上的熟悉似乎帮不了你什么忙,现在要沉下心来思考为什么ping不通。

  为什么ping不通呢?

  问题在于,数据包虽然可以从veth0发出最终通过其peer网卡veth1接收,然而当执行流经由veth1接收逻辑到达ip_route_input_slow的时候,以目标地址1.1.1.1查询路由表的结果显然是一个LOCAL路由,而Linux的IP路由实现中有个限制,即任何从非loopback网卡进来的任何数据包的源地址不能是本机地址,然而以源地址1.1.1.2来查询路由表,结果却也是一条LOCAL路由,显然1.1.1.2是一个本机地址,这违背了上面的约束,因此数据包被丢弃。

  具体在代码上,这是由ip_route_input_slow函数里的fib_validate_source调用来判断的:

int fib_validate_source(...)
{
    ...
    if (fib_lookup(net, &fl, &res)) // 查询失败是允许的,只要rp_filter未启用
        goto last_resort;
    if (res.type != RTN_UNICAST) // 然而更严格的约束是,以源地址查询路由必须是UNICAST而不能是LOCAL路由
        goto e_inval_res;
    ...
}

这意味着只要有上述的代码逻辑存在,这个就是避不开的。且慢,这里还有一个疑问,为什么题目中要有把loopback网口down掉这个关键的约束呢?答案在于,如果开启loopback,那么内核在路由到达1.1.1.1的数据包的时候,会直接将其路由到loopback接口,直接将本地环回路由绑定到数据包,从而在loopback的接收路径上会绕开ip_route_input_slow函数。

  这样就不会进入fib_validate_source判断逻辑。

  也就是说,在上述情况下,本文中我的这个问题的答案就可以毫不犹豫地回答:做不到!

挺失望的是吗?不。如果你能回答到上面的程度,说明你对Linux协议栈IP路由实现的理解已经达到一定深度了。然而这只是一半,另一半接下来详述。

  事情在4.x内核中起了变化。事情在下面的patch中起了变化
http://patchwork.ozlabs.org/patch/40152/
好的,就着这个patch,我们来看看问题如何回答本文最初的问题。

  首先,第一步能想到的就是执行下面的命令(既然patch说的就是accept_local,那必然是要把它打开咯),打开相关网卡的accept_local:

sysctl -w net.ipv4.conf.veth0.accept_local=1
sysctl -w net.ipv4.conf.veth1.accept_local=1

但是你去试着下面的命令:

ping 1.1.1.1 -I veth0

还是不通!在继续下一步排查之前,这里你知道为什么不去尝试下面的命令吗:

ping 1.1.1.1

因为这会直接将数据包在ip_route_output中直接导入loopback,然而loopback已经被禁用了,路直接就堵死了!所以只能用-I veth0指定从veth0中发出。

  但是为什么即便这样还是不通呢?我明明已经打开accept_local了啊!

  问题出在ping的返回包上,试想返回包的目标IP是1.1.1.2,也是一个本机IP,这里无法针对返回包也指定网卡设备参数,因此IP路由会尝试将其导入loopback接口,然而loopback已经被禁用,无法发送。这就是原因。所以问题简单了,我们尝试用fwmark策略路由来替代-I veth0参数,以下是详细的配置:


# 加入fwmark 100策略
root@debian:/home/zhaoya# ip rule add fwmark 100 pref 100 tab 100
# 以下的两条命令旨在将fwmark策略加入到local之前
root@debian:/home/zhaoya# ip rule add pref 1000 tab local
root@debian:/home/zhaoya# ip rule del pref 0 tab local

# 以下确认
root@debian:/home/zhaoya# ip rule ls
# 首先查询100表
100:    from all fwmark 0x64 lookup 100 
# 然后再查local表以及main表
1000:   from all lookup local 
32766:  from all lookup main 
32767:  from all lookup default

# 以下为策略路由表100添加两条明细路由
root@debian:/home/zhaoya# ip route add 1.1.1.1/32 dev veth0 src 1.1.1.2 tab 100
root@debian:/home/zhaoya# ip route add 1.1.1.2/32 dev veth1 src 1.1.1.1 tab 100
# 注解:之前我把veth0和veth1写反了,如下所示。感谢细心网友及时指出。不过即便按照下面错误的写法,也有好玩的地方可以斟酌...
#root@debian:/home/zhaoya# ip route add 1.1.1.1/32 dev veth1 src 1.1.1.2 tab 100
#root@debian:/home/zhaoya# ip route add 1.1.1.2/32 dev veth0 src 1.1.1.1 tab 100

# 确认路由添加成功
root@debian:/home/zhaoya# ip route ls tab 100
1.1.1.1 dev veth1 scope link src 1.1.1.2 
1.1.1.2 dev veth0 scope link src 1.1.1.1  

为了保证ping的返回包也能打上fwmark 100,这里采用了基于目标地址的策略路由,配置如下:
# 下面的iptables策略来为返回包打上mark 100
root@debian:/home/zhaoya# iptables -t mangle -A OUTPUT -d 1.1.1.2/32 -j MARK --set-mark 100

以上就是全部的配置了!

  谢天谢地,ping有一个-m选项,竟然可以将ping包直接打上mark。试试看:

root@debian:/home/zhaoya# ping 1.1.1.1 -m 100
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=64 time=0.263 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=64 time=0.178 ms
...

到此为止,问题解决了,如果能在五分钟之内把上述的内容完全理清楚,如果这真的是个面试题,相信肯定会给人留下好印象!但是问题解决了,事情并没有完。


注意上面的那条iptables策略,它的目的是为返回的数据包打上fwmark 100,而这个fwmark 100只是为了将针对1.1.1.2的IP路由引入到路由表100,那么是不是下面的命令能替代iptables策略呢:

# 加入to 1.1.1.2的策略
root@debian:/home/zhaoya# ip rule add to 1.1.1.2 pref 101 tab 100

同样是针对1.1.1.2引导到路由表100的策略。

  但是当你这么做之后,就会发现,ping不通了!!虽然理论上这条ip rule策略和那条iptables是如此的相似,简直一模一样,然而结果却是大相径庭,Why?问题出在ARP!这个时候抓包就会发现以下的未有ARP Reply的信息:

06:11:32.762042 ARP, Request who-has 1.1.1.2 tell 1.1.1.1, length 28
06:11:32.762208 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 1456, seq 27, length 64
06:11:33.786132 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 1456, seq 28, length 64
06:11:33.786206 ARP, Request who-has 1.1.1.2 tell 1.1.1.1, length 28
...

为什么得不到ARP Reply?

  因为ARP Reply也要查询路由表,当veth1询问who-has 1.1.1.2的时候,1.1.1.2的veth0显然收到了该ARP Request,是否回复ARP Reply取决于1.1.1.2是否能在LOCAL表中找到(以及和rp_filter的联动),仔细看ip rule add to 1.1.1.2这条策略,显然得到了命中,然而路由表100中却没有LOCAL路由,这就导致ARP Reply无法得到回复!


在理解了上面最后一个关于ARP的问题后,本文到此就要完结了。这里不得不再说几句。

  如果你想让事情简单些,你会用两个TUN模式的tun网卡替代veth不是吗?因为TUN模式的tun网卡不需要ARP,这就避开了ARP的路由问题…然而如果你能懂得这些,相信你也能搞得定ARP问题。相反,如果你想让事情更复杂,你可以联系net.ipv4.fwmark_reflect以及nf_conntrack相关的save/restore MARK。但是不管怎样,这都需要你对IP路由玩得得心应手信手拈来不是吗?所以啊,只有当你把这一切全都吃透的时候,才能出神入化玩转自如啊,偷懒是要不得的。

  我本没有工作日写东西的习惯,但也并不绝对…

没有更多推荐了,返回首页