目录
一、概述
参考关于Linux内核引入的accept_local参数的一个问题,自己分析一下
二、分析
2.1 问题描述
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
ip link set veth0 up
ip link set veth1 up
# 以下一条为关键,down掉loopback
# ip link set dev lo down
要求: 以源IP 1.1.1.2, ping 通 1.1.1.1,不使用namespace,docker等技术
2.2 分析流程
这里先执行ping 1.1.1.2发现是通的,但是抓包发现,报文的发送和接受都是从lo口进行的:
[root@localhost a]# tcpdump -i lo -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
18:44:43.510145 IP 1.1.1.1 > 1.1.1.1: ICMP echo request, id 5013, seq 125, length 64
18:44:43.510186 IP 1.1.1.1 > 1.1.1.1: ICMP echo reply, id 5013, seq 125, length 64
这不满足题目的要求,我们先分析一下为什么会产生上述结果。先分析 ping 1.1.1.1过程,报文先查路由,我们就先分析一下路由的情况,一般的,Linux有如下不同策略的路由表:
[root@localhost a]# ip rule
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
上述每一行代表一条策略,从上到下优先级降低,报文匹配到策略就会去查找对应的路由表。观察上图,每条报文都会依次查local,main,default表,对于ping本机的报文来说,一定会命中local表。因为在配置IP地址时(以veth1为例),生成如下路由:
首先查路由,在local表命中,linux路由查找返回local,于是端口替换为loopback口,可以看到,源IP并不是1.1.1.2,因为在未指定源IP的情况下,源IP设置成与目的IP一致:
icmp_route_lookup-> ip_route_output_key_hash->__ip_route_output_key_hash:
if (res.type == RTN_LOCAL) {
if (!fl4->saddr) {
if (res.fi->fib_prefsrc)
fl4->saddr = res.fi->fib_prefsrc;
else
fl4->saddr = fl4->daddr;
}dev_out = l3mdev_master_dev_rcu(FIB_RES_DEV(*res)) ? :
net->loopback_dev;}
而loopback发送直接调用netif_rx,所以这时候会收到loopback来的报文。
那如何指定由veth0(1.1.1.2发送呢),可以使用ping 1.1.1.1 -I veth0,结果发现是不通的。下面我们来分析不通的原因,抓包发现veth1收到veth0的arp request而没有返回,这是由于arp reply时查路由失败。
[root@localhost a]# tcpdump -i veth1 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes
19:38:21.755358 ARP, Request who-has 1.1.1.1 tell 1.1.1.2, length 28
19:38:22.755686 ARP, Request who-has 1.1.1.1 tell 1.1.1.2, length 28
参照代码
if (!skb_valid_dst(skb)) {
int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
iph->tos, dev);
这时候1.1.1.1查找结果一定是LOCAL,而上述函数对于LOCAL的路由有一个源地址校验::
if (res.type == RTN_LOCAL) {
err = fib_validate_source(skb, saddr, daddr, tos,
0, dev, in_dev, &itag);
if (err < 0)
goto martian_source;
goto local_input;
}
具体为:
fl4.daddr = src;
fl4.saddr = dst;if (fib_lookup(net, &fl4, &res, 0))
goto last_resort;
if (res.type != RTN_UNICAST &&
(res.type != RTN_LOCAL || !IN_DEV_ACCEPT_LOCAL(idev)))
goto e_inval;
这里进行了反向路由查找,就是将源、目的地址调换再次查找路由,显然1.1.1.2也是local的,在没有配置accept local的情况下,就会出错。
我们将local_accept选项打开:
- sysctl -w net.ipv4.conf.veth0.accept_local=1
- sysctl -w net.ipv4.conf.veth1.accept_local=1
为什么会这样呢,我参考了概述中的文章里面的一句话:
任何从非loopback网卡进来的任何数据包的源地址不能是本机地址
这应该算作一个设计约束,防止非法报文进来。
那么这里深究一下:从loopback进来的报文为什么没有被这个条件拦住呢?看下loopback的实现:在loopback接口发送报文时:
[drivers/net/loopback.c]
static netdev_tx_t loopback_xmit(struct sk_buff *skb,
struct net_device *dev)
{
/* Before queueing this packet to netif_rx(),
* make sure dst is refcounted.
*/
skb_dst_force(skb);skb->protocol = eth_type_trans(skb, dev);
if (likely(netif_rx(skb) == NET_RX_SUCCESS)) {
}return NETDEV_TX_OK;
}
loopback在发送时 skb_dst_force,这样在报文接收时,就可以跳过查找输入路由,从而不会在检查时出错!
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{if (!skb_valid_dst(skb)) {
int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
iph->tos, dev);}
修改了accept local ping还是不通,这是为什么呢,这个和linux反向过滤机制有关(rp_filter),反向查找以后,如果出口和入口不一致,则认为反向过滤失败,进行丢包,使用1.1.1.2查找出接口会命中local表项,出口为veth0,报文是从veth1来的,所以这里会失败,如下:
if (FIB_RES_DEV(res) == dev) //不匹配
dev_match = true;
if (dev_match) {
ret = FIB_RES_NH(res).nh_scope >= RT_SCOPE_HOST;
return ret;
}
if (no_addr)
goto last_resort;
if (rpf == 1) //如果启用非rp_filter,进入失败流程
goto e_rpf;
那我们将rp_filter关掉。
- sysctl -w net.ipv4.conf.veth0.rp_filter=0
- sysctl -w net.ipv4.conf.veth1.rp_filter=0
- sysctl -w net.ipv4.conf.default.rp_filter=0
- sysctl -w net.ipv4.conf.all.rp_filter=0
修改完成后还是不通,这时候抓包发现arp问题已经没有了,两个口抓到icmp request,但是没有回复
[root@localhost ~]# tcpdump -i veth1 -qnnvv
tcpdump: listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes
00:27:05.040687 IP (tos 0x0, ttl 64, id 26224, offset 0, flags [DF], proto ICMP (1), length 84)
1.1.1.2 > 1.1.1.1: ICMP echo request, id 4901, seq 2966, length 64
00:27:06.040804 IP (tos 0x0, ttl 64, id 26332, offset 0, flags [DF], proto ICMP (1), length 84)
1.1.1.2 > 1.1.1.1: ICMP echo request, id 4901, seq 2967, length 64
这时候input方向应该是没有限制了,我们考虑icmp response,这时候没办法向前面那样指定发送端口了,而根据我们前面的分析,本地地址会自动使用loopback口替换,在lo抓包:
[root@localhost ~]# tcpdump -i lo -qnnvv
tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
00:31:20.068443 IP (tos 0x0, ttl 64, id 38157, offset 0, flags [none], proto ICMP (1), length 84)
1.1.1.1 > 1.1.1.2: ICMP echo reply, id 4901, seq 3221, length 64
00:31:21.069468 IP (tos 0x0, ttl 64, id 39082, offset 0, flags [none], proto ICMP (1), length 84)
1.1.1.1 > 1.1.1.2: ICMP echo reply, id 4901, seq 3222, length 64
那么如何让icmp response包通过veth0口回复呢,我们知道在查路由结果如果时local才会将端口替换成lo,那么我们使用一种方法使查询结果不是local就好了,怎么办,使用策略路由!提供一个比local表优先级更高的表项:
[root@localhost ~]# ip rule show
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
可以看到默认情况下,优先级最高的是0,对应local表,所以先将local表的优先级降低一下:
- ip rule add pref 1000 tab local
- ip rule del pref 0 tab local
接下来加入我们的策略
- ip rule add fwmark 100 pref 100 tab 100
- ip route add 1.1.1.2/32 dev veth1 src 1.1.1.1 tab 100
此时看下rule
[root@localhost a]# ip rule
100: from all fwmark 0x64 lookup 100
1000: from all lookup local
32766: from all lookup main
32767: from all lookup default
表100
[root@localhost a]# ip route show table 100
1.1.1.2 dev veth1 scope link src 1.1.1.1
注意,查上面的表是不会返回local的,因此不会有lo的替换,而是按照table 100指定的口进行发包。最后使用iptable将ping response打上fwmark标记,使之能进入table 100查路由。
iptables -t mangle -A OUTPUT -d 1.1.1.2/32 -j MARK --set-mark 100
此时可以ping通了
[root@localhost a]# ping 1.1.1.1 -I veth0
PING 1.1.1.1 (1.1.1.1) from 1.1.1.2 veth0: 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=64 time=0.081 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=64 time=0.150 ms
当然,原文通过-m也匹配table100,而不通过-I veth0也是可以的,在table 100加额外的一条路由
- ip route add 1.1.1.1/32 dev veth0 src 1.1.1.2 tab 100
[root@localhost a]# ip route show table 100
1.1.1.1 dev veth0 scope link src 1.1.1.2
1.1.1.2 dev veth1 scope link src 1.1.1.1
此时也是通的:
[root@localhost a]# 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.083 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=64 time=0.114 ms
最后作者提到将iptable mangle换成如下指定:
- ip rule add to 1.1.1.2 pref 101 tab 100
这样是不行的,因为arp reply的时候会查路由,只有在结果是local的才会回复,而从1.1.1.1到1.1.1.2的arp在处理时会命中table100,table 100没有local,从而失败。
arp_process
if (arp->ar_op == htons(ARPOP_REQUEST) &&
ip_route_input_noref(skb, tip, sip, 0, dev) == 0) {rt = skb_rtable(skb);
addr_type = rt->rt_type;if (addr_type == RTN_LOCAL) {
int dont_send;dont_send = arp_ignore(in_dev, sip, tip);
if (!dont_send && IN_DEV_ARPFILTER(in_dev))
dont_send = arp_filter(sip, tip, dev);
if (!dont_send) {
n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
if (n) {
arp_send_dst(ARPOP_REPLY, ETH_P_ARP,
sip, dev, tip, sha,
dev->dev_addr, sha,
reply_dst);
neigh_release(n);
}
}
另外,作者在配置table 100的路由时,一开始写成
- ip route add 1.1.1.1/32 dev veth1 src 1.1.1.2 tab 100
- ip route add 1.1.1.2/32 dev veth0 src 1.1.1.1 tab 100
即将dev和src指反了,不匹配实际配置,这时候ping(ping 1.1.1.1 -m 100)是通的!
来分析一下原因,当ping发生时,命中上述第一条路由,但是出口选错了,本应该从veth0发出,但是查路由后却是veth1
口,这导致arp request从veth1口发出,下面是我用systemtap抓到的:
0xffffffffc06f94a0 : veth_xmit+0x0/0x60 [veth]
0xffffffff90beb166 : dev_hard_start_xmit+0x246/0x3b0 [kernel]
0xffffffff90bee0d9 : __dev_queue_xmit+0x529/0x660 [kernel]
0xffffffff90bee243 : dev_queue_xmit_sk+0x13/0x20 [kernel]
0xffffffff90c65ae3 : arp_xmit+0x33/0xa0 [kernel]
0xffffffff90c65b95 : arp_send_dst.part.16+0x45/0x50 [kernel]
0xffffffff90c65d0a : arp_solicit+0x12a/0x2e0 [kernel]
0xffffffff90bf8240 : neigh_probe+0x50/0x70 [kernel]
0xffffffff90bf93ae : __neigh_event_send+0xae/0x260 [kernel]
0xffffffff90bf9c7b : neigh_resolve_output+0x13b/0x220 [kernel]
0xffffffff90c3717c : ip_finish_output+0x2ac/0x7a0 [kernel]
0xffffffff90c37973 : ip_output+0x73/0xe0 [kernel]
0xffffffff90c35567 : ip_local_out_sk+0x37/0x40 [kernel]
0xffffffff90c383c6 : ip_send_skb+0x16/0x50 [kernel]
0xffffffff90c38433 : ip_push_pending_frames+0x33/0x40 [kernel]
0xffffffff90c5efa9 : raw_sendmsg+0x879/0xa30 [kernel]
0xffffffff90c6e5a9 : inet_sendmsg+0x69/0xb0 [kernel]
0xffffffff90bcc396 : sock_sendmsg+0xb6/0xf0 [kernel]
0xffffffff90bccaa1 : SYSC_sendto+0x121/0x1c0 [kernel]
0xffffffff90bce42e : SyS_sendto+0xe/0x10 [kernel]
veth_xmit dev_name:veth1
这样arp请求就会从veth0口收到,回复的时候用veth0口的mac进行回复,这样会导致veth0,veth1学到的mac地址是相反的!
16:36:42.700706 2a:59:93:9e:c5:d6 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 1.1.1.1 tell 1.1.1.2, length 28
16:36:42.700787 6a:61:8e:4b:71:80 > 2a:59:93:9e:c5:d6, ethertype ARP (0x0806), length 42: Reply 1.1.1.1 is-at 6a:61:8e:4b:71:80, length 28
16:36:42.700928 2a:59:93:9e:c5:d6 > 6a:61:8e:4b:71:80, ethertype IPv4 (0x0800), length 98: 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6754, seq 1, length 64
16:36:42.701010 6a:61:8e:4b:71:80 > 2a:59:93:9e:c5:d6, ethertype IPv4 (0x0800), length 98: 1.1.1.1 > 1.1.1.2: ICMP echo reply, id 6754, seq 1, length 64
16:36:47.705842 6a:61:8e:4b:71:80 > 2a:59:93:9e:c5:d6, ethertype ARP (0x0806), length 42: Request who-has 1.1.1.2 tell 1.1.1.2, length 28
16:36:47.706204 2a:59:93:9e:c5:d6 > 6a:61:8e:4b:71:80, ethertype ARP (0x0806), length 42: Reply 1.1.1.2 is-at 2a:59:93:9e:c5:d6, length 28
由于两端是对称的,这样处理起来也不会有什么错误。当然了,你用ping 1.1.1.1 -I veth0 肯定就不通了,对吧。