两台主机互为网关是否会像打乒乓球一样一直互发
一、臆想的一个问题
一直比较好奇一个问题,或者说是一个恶作剧:假设说A、B两个主机互为网关,A需要发送一个数据,根据自己路由配置数据被发送给B主机;数据到达B主机之后,B主机检查自己的路由,发现网关是A主机,这样就会将这个数据(递减TTL之后)再次回传给A主机。这个过程是否会这样一直继续下去呢(当然,是在TTL还没有降到零的前提下)?
二、测试环境的准备
在单PC的环境下其实是没有办法测试这个环境,为了搭建这个环境,可以考虑在Linux环境下搭建一个qemu模拟器,两个环境放在同一个局域网中,这样虽然不是真实的物理环境,但是对于我们这里实验的问题来说已经足够了。在这个环境的搭建过程qemu使用tap虚拟网卡接入宿主机环境,并且和宿主机在同一个网段中。下面是具体的环境配置:
1、Linux宿主机的环境配置
tsecer@harry: ifconfig
br0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.65.3 netmask 255.255.255.0 broadcast 192.168.65.255
inet6 fe80::20c:29ff:fe35:e07c prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:35:e0:7c txqueuelen 0 (Ethernet)
RX packets 10 bytes 692 (692.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 29 bytes 4098 (4.0 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eno16777736: flags=4419<UP,BROADCAST,RUNNING,PROMISC,MULTICAST> mtu 1500
inet6 fe80::20c:29ff:fe35:e07c prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:35:e0:7c txqueuelen 1000 (Ethernet)
RX packets 50 bytes 4948 (4.8 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 484 bytes 42748 (41.7 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 0 (Local Loopback)
RX packets 15 bytes 1530 (1.4 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 15 bytes 1530 (1.4 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
tap0: flags=4419<UP,BROADCAST,RUNNING,PROMISC,MULTICAST> mtu 1500
inet6 fe80::ace2:4eff:feea:a26c prefixlen 64 scopeid 0x20<link>
ether ae:e2:4e:ea:a2:6c txqueuelen 100 (Ethernet)
RX packets 8 bytes 648 (648.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 16 bytes 1785 (1.7 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
这里测试使用的目的IP地址为11.11.11.11,当然这个地址本身没有意义,只要和两者不在同一个局域网即可。为了环境更加准确,在宿主机上配置到11.11.11.11的路由经过qemu虚拟机上的网卡发送,并且开启网卡的抓发功能:
tsecer@harry: route add -net 11.11.11.0 netmask 255.255.255.0 gw 192.168.65.4
tsecer@harry: echo 1 > /proc/sys/net/ipv4/conf/br0/forwarding
tsecer@harry: route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.65.1 0.0.0.0 UG 0 0 0 br0
11.11.11.0 192.168.65.4 255.255.255.0 UG 0 0 0 br0
192.168.65.0 0.0.0.0 255.255.255.0 U 0 0 0 br0
tsecer@harry:
2、qemu虚拟机上的配置
虚拟机上进行类似的镜像配置,设置到11.11.11.0网段的路由经过宿主机上的网络地址,并且使能网卡转发功能。
tsecer@qemu: ifconfig
eth0 Link encap:Ethernet HWaddr 52:54:00:12:34:56
inet addr:192.168.65.4 Bcast:192.168.65.255 Mask:255.255.255.0
inet6 addr: fe80::5054:ff:fe12:3456/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:648 (648.0 B)
Interrupt:11 Base address:0xc000
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.255.255.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
三、测试过程
1、测试方法
环境配置之后,从虚拟机上开始通过telnet连接远程主机(当然这样的配置是不会发送外网的),
tsecer@qemu: telnet 11.11.11.11
并且在宿主机上使用tcpdump抓包:
tsecer@harry: tcpdump -i tap0 -v -nn
tcpdump: WARNING: tap0: no IPv4 address assigned
tcpdump: listening on tap0, link-type EN10MB (Ethernet), capture size 65535 bytes
13:13:28.074659 IP (tos 0x0, ttl 64, id 23030, offset 0, flags [DF], proto TCP (6), length 60)
192.168.65.4.46857 > 11.11.11.11.23: Flags [S], cksum 0x499c (correct), seq 3776155125, win 29200, options [mss 1460,sackOK,TS val 413284 ecr 0,nop,wscale 4], length 0
13:13:28.074748 IP (tos 0xc0, ttl 64, id 45796, offset 0, flags [none], proto ICMP (1), length 88)
192.168.65.3 > 192.168.65.4: ICMP redirect 11.11.11.11 to host 192.168.65.4, length 68
IP (tos 0x0, ttl 63, id 23030, offset 0, flags [DF], proto TCP (6), length 60)
192.168.65.4.46857 > 11.11.11.11.23: Flags [S], cksum 0x499c (correct), seq 3776155125, win 29200, options [mss 1460,sackOK,TS val 413284 ecr 0,nop,wscale 4], length 0
13:13:28.074763 IP (tos 0x0, ttl 63, id 23030, offset 0, flags [DF], proto TCP (6), length 60)
192.168.65.4.46857 > 11.11.11.11.23: Flags [S], cksum 0x499c (correct), seq 3776155125, win 29200, options [mss 1460,sackOK,TS val 413284 ecr 0,nop,wscale 4], length 0
13:14:00.159444 IP (tos 0x0, ttl 64, id 23031, offset 0, flags [DF], proto TCP (6), length 60)
192.168.65.4.46857 > 11.11.11.11.23: Flags [S], cksum 0xcc75 (correct), seq 3776155125, win 29200, options [mss 1460,sackOK,TS val 445322 ecr 0,nop,wscale 4], length 0
13:14:00.159576 IP (tos 0xc0, ttl 64, id 45797, offset 0, flags [none], proto ICMP (1), length 88)
192.168.65.3 > 192.168.65.4: ICMP redirect 11.11.11.11 to host 192.168.65.4, length 68
IP (tos 0x0, ttl 63, id 23031, offset 0, flags [DF], proto TCP (6), length 60)
192.168.65.4.46857 > 11.11.11.11.23: Flags [S], cksum 0xcc75 (correct), seq 3776155125, win 29200, options [mss 1460,sackOK,TS val 445322 ecr 0,nop,wscale 4], length 0
13:14:00.159601 IP (tos 0x0, ttl 63, id 23031, offset 0, flags [DF], proto TCP (6), length 60)
192.168.65.4.46857 > 11.11.11.11.23: Flags [S], cksum 0xcc75 (correct), seq 3776155125, win 29200, options [mss 1460,sackOK,TS val 445322 ecr 0,nop,wscale 4], length 0
13:14:05.138528 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.65.3 tell 192.168.65.4, length 46
13:14:05.138588 ARP, Ethernet (len 6), IPv4 (len 4), Reply 192.168.65.3 is-at 00:0c:29:35:e0:7c, length 28
^C
8 packets captured
27 packets received by filter
0 packets dropped by kernel
2、测试结果
从测试结果上看,报文的确是递减了TTL之后被转发给了发送源(qemu虚拟机),但是并没有我们期望的再次被A转发出去,也就是这样的路由错误发送源只犯了一次,而不是持续不断的再次发送,就好像刮彩票的时候,看见一个“谢”字就没必要把后面剩下的“谢惠顾”这三个字再刮出来了,说明他是一个知趣的屌丝。
另外中间穿插了一个抢镜的ICMP REDIRECT报文。
四、REDIRECT ICMP的缘由
1、为什么发送REDIRECT
这个其实比较好理解,假设主机从自己的网口N上收到一个转发报文,查找路由之后发现这个报文要经过网口N再次发送出去,此时主机就会觉得这个报文其实你自己处理可能还会更便捷一点,可能只是发送源不知道这个更短路径而已,所以这个中转主机在中转的同时通过ICMP告诉发送源:你可以通过这个网关来转发,这样会更快一些。
从代码中看,这个路由为
/* called in rcu_read_lock() section */
static int __mkroute_input(struct sk_buff *skb,
const struct fib_result *res,
struct in_device *in_dev,
__be32 daddr, __be32 saddr, u32 tos)
{
……
if (out_dev == in_dev && err && IN_DEV_TX_REDIRECTS(out_dev) &&
(IN_DEV_SHARED_MEDIA(out_dev) ||
inet_addr_onlink(out_dev, saddr, FIB_RES_GW(*res)))) {
flags |= RTCF_DOREDIRECT;
do_cache = false;
}
……
}
然后在转发的时候判断RTCF_DOREDIRECT标志位,在转发前给发送源发送REDIRECT ICMP消息
int ip_forward(struct sk_buff *skb)
{
……
/*
* We now generate an ICMP HOST REDIRECT giving the route
* we calculated.
*/
if (rt->rt_flags&RTCF_DOREDIRECT && !opt->srr && !skb_sec_path(skb))
ip_rt_send_redirect(skb);
skb->priority = rt_tos2priority(iph->tos);
return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev,
rt->dst.dev, ip_forward_finish);
……
}
2、发送源对ICMP报文的处理
发送源接收到ICMP报文之后,它查找了下新指定的网关竟然是自己(inet_addr_type(net, new_gw)返回的类型是RTN_LOCAL),这个多少有些震惊,所以默默地拒绝了这个REDIRECT提示。所以这个ICMP并没有避免发送源在错误的道路上越走越远。
static void __ip_do_redirect(struct rtable *rt, struct sk_buff *skb, struct flowi4 *fl4,
bool kill_route)
{
……
if (inet_addr_type(net, new_gw) != RTN_UNICAST)
goto reject_redirect;
}
五、发送源如何意识到路由的错误
我们回过头来再看下路由函数:当发送源接收到反射回来的转发请求的时候,它同样是继续查询自己的路由表:
static int __mkroute_input(struct sk_buff *skb,
const struct fib_result *res,
struct in_device *in_dev,
__be32 daddr, __be32 saddr, u32 tos)
{
……
err = fib_validate_source(skb, saddr, daddr, tos, FIB_RES_OIF(*res),
in_dev->dev, in_dev, &itag);
if (err < 0) {
ip_handle_martian_source(in_dev->dev, in_dev, skb, daddr,
saddr);
goto cleanup;
}
……
}
static int __fib_validate_source(struct sk_buff *skb, __be32 src, __be32 dst,
u8 tos, int oif, struct net_device *dev,
int rpf, struct in_device *idev, u32 *itag)
{
int ret, no_addr, accept_local;
struct fib_result res;
struct flowi4 fl4;
struct net *net;
bool dev_match;
fl4.flowi4_oif = 0;
fl4.flowi4_iif = oif;
fl4.daddr = src;
fl4.saddr = dst;
fl4.flowi4_tos = tos;
fl4.flowi4_scope = RT_SCOPE_UNIVERSE;
no_addr = idev->ifa_list == NULL;
accept_local = IN_DEV_ACCEPT_LOCAL(idev);
fl4.flowi4_mark = IN_DEV_SRC_VMARK(idev) ? skb->mark : 0;
net = dev_net(dev);
if (fib_lookup(net, &fl4, &res))
goto last_resort;
if (res.type != RTN_UNICAST) {
if (res.type != RTN_LOCAL || !accept_local)
goto e_inval;
}
……
}
当发送源收到“反射”回来的转发请求的时候,此时它通过__fib_validate_source验证了下发送源的合法性,具体做法就是将报文的目的和源路由调换个儿,以IP源为目的,以IP目的为源,然后在自己本机上查询路由。对于我们这里构造的情况,此时原始报文的IP源就是本机地址,转换为目的之后,fib_lookup返回的res.type值为RTN_LOCAL,所以发送源意识到这里有严重的问题,直接goto e_inval丢弃了这次转发请求,从而避免了这个报文在两个主机中打乒乓。
六、对测试过程使用的虚拟网桥设备的说明
这个虚拟设备是工作在链路层上的虚拟设备,也就是它转发的规则使用的是网络设备的MAC地址,这一点和路由器根据MAC地址路由的规则不同。
1、新网口的添加
br_add_if===>>>br_fdb_insert(br, p, dev->dev_addr, 0)===>>>fdb_insert(br, source, addr, vid)===>>>fdb_create
这里可以看到fdb_insert在添加的时候使用的是设备的MAC地址作为键值,也就是说当添加一个设备到网卡的时候,网桥更关心的是设备的MAC地址,并且根据这个来作为转发的依据。
2、MAC地址的学习
在网桥收到任何一个报文的时候,如果使能了学习模式,会更新自己的MAC地址库(使用收到报文的源地址作为键值,对应到网口),
int br_handle_frame_finish(struct sk_buff *skb)
{
……
if (p->flags & BR_LEARNING)
br_fdb_update(br, p, eth_hdr(skb)->h_source, vid);
3、在需要发送报文时
br_dev_xmit===>>>((dst = __br_fdb_get(br, dest, vid)) != NULL) br_deliver(dst->dst, skb);===>>>__br_deliver===>>>br_forward_finish===>>>br_dev_queue_push_xmit===>>>dev_queue_xmit
其中__br_deliver修改了数据包发送时使用的dev为fdb中查找出的网口信息。
4、网口收到报文时
br_handle_frame===>>>br_handle_frame_finish
} else if ((dst = __br_fdb_get(br, dest, vid)) &&
dst->is_local) {
skb2 = skb;
/* Do not forward the packet since it's local. */
skb = NULL;
}
if (skb) {
if (dst) {
dst->used = jiffies;
br_forward(dst->dst, skb, skb2);
} else
br_flood_forward(br, skb, skb2, unicast);
}
if (skb2)
return br_pass_frame_up(skb2);
5、和路由表的一点类比
这里使用的fdb是forwarding database的缩写,而路由表中fib则是forwarding infomation base 的缩写,本质上一样的,都是根据目的地址来查找路由信息,只不过一个使用的是MAC地址,另一个使用的IP地址而已。