文章出处:http://blog.csdn.net/dog250/article/details/5814023
SO_DONTROUTE并没有跳过路由表的查找,而只是将查找范围缩小到了直连的同三层网段主机,SO_BINDTODEVICE亦没有跳过路由表查找,而只是将外出设备固定,也就是增加了一个查找键,因此二者都无法跳过查找路由表的过程,本质上,SO_DONTROUTE也是增加了一个查找键。路由表的查找在linux实现的协议栈中是无法越过的,但是却可以增加若干的限制条件,以hash路由表为例,在fn_hash_lookup函数中:
if (f->fn_scope < flp->fl4_scope) //检查路由的范围(**)
continue;
err = fib_semantic_match(f->fn_type, FIB_INFO(f), flp, res);
if (err == 0) {
//找到
}
在fib_semantic_match中:
if (!flp->oif || flp->oif == nh->nh_oif) //***
break; //找到
可见在核心的查找函数中,DONTROUTE和BINDTODEVICE只是做了一些限制,并且在核心查找函数的任意层次调用函数中都没有绕过路由查找的逻辑。
在正常的数据发送过程中,查找路由的时候是没有出口设备信息的,出口设备由路由表的匹配结果来决定,而在通过setsockopt设置了SO_BINDTODEVICE之后,在查找路由之前就有了出口设备信息,ip_queue_xmit中,查找键fl中会添加.oif = sk->sk_bound_dev_if这个信息(没有bindtodevice的情况下,sk->sk_bound_dev_if为0),然后路由查找按照往常的方式继续,到达fib_semantic_match的时候,在***处起作用;对于SO_DONTROUTE这个选项,同样的道理,在**处起作用,linux内核协议栈将“路由范围”定义成了一个枚举,一共N中类型:
enum rt_scope_t
{
RT_SCOPE_UNIVERSE=0, //任意的地址路由
RT_SCOPE_SITE=200, //用户自定义
RT_SCOPE_LINK=253, //本地直连的路由
RT_SCOPE_HOST=254, //主机路由
RT_SCOPE_NOWHERE=255 //不存在的路由
};
数值逐渐增大,越大的越不易匹配,因此在**处,如果由于设置SO_DONTROUTE而配置了RT_SCOPE_LINK的话,如果目的主机在外部,路由表中的scope就是RT_SCOPE_UNIVERSE,这样就不会匹配,因此也就找不到了,因此如果设置了SO_DONTROUTE的话,即使一个目的地址存在路由,只要它在外部,那就是不可达的。另外SO_BINDTODEVICE还有一个约束,那就是数据包从那里走必然从哪里回来,如果不是从出去的口回来的,那么将无法送到用户态处理,这由INET_MATCH来处理,进入__inet_lookup之后,目的是查找一个和该数据包关联的套接字,对于每一个哈西冲突链中套结字都会调用INET_MATCH来进行匹配,INET_MATCH中有一句:
!((__sk)->sk_bound_dev_if) || ((__sk)->sk_bound_dev_if == (__dif)
同样对于udp来说,udp_v4_lookup_longway中也会有相同的逻辑,这样就保证了从哪里走从哪里回来。
根据SO_BINDTODEVICE的特点来说,用它来做负载均衡是可以的,但是前提是需要均衡的数据是本机发出的,而不是forward的,因为在ip_route_input_slow中设置路由表查找键的时候出口设备设置为0,在ip_route_input中查找路由缓存的时候rth->fl.oif == 0说明出口设备必须不能设置(),因此要想对过路数据做负载均衡,必须首先将其redirect到本机的用户态,然后再分别建立多个(取决于可以均衡的网卡数量)socket,每一个绑定一个出口网卡设备,然后视负载情况在这些设备关联的socket将数据代理出去,不过这要求机器性能足够好,负载均衡带来的收益要远大于redirect带来的开销损失。负载均衡永远都是一个重要话题,特别是鱼龙混杂的linux世界,linux主机有的性能超棒有的却只是386的古董,加上linux内核的路由缓存机制使之并不能实现基于包的负载均衡,在不修改协议栈的情况下,必须在用户态想办法,一种办法就是redirect,另一种办法就是使用虚拟网卡,即使用虚拟网卡的字符设备接口将三层或者二层数据导入到用户态,然后再使用SO_BINDTODEVICE均衡到各个网卡,但要注意,此法到此为止只实现了一半,由于虚拟网卡的字符设备出来的并不是用户空间数据,因此再往socket发送相当于做了一个隧道封装,什么时候解封装,这是一个问题,最好是数据通过需要均衡的路段后就解封装,因此必然需要一个对称的主机负责解封装。
但是且慢,难道SO_DONTROUTE就没有别的作用,就没有别的说头吗?不是这样的,也不会这么简单!这里的关键在于“出口设备”,只要能确定设备,带有SO_DONTROUTE的套结字就能成功将数据发送出去的,这是事实,在ip_route_output_slow中:
if (fib_lookup(&fl, &res)) {
//查找失败的可能:1.本身就没有路由表项匹配;2.有路由表项匹配,但是不是本地的。
res.fi = NULL;
if (oldflp->oif) { //但是,只要有出口设备,就能发送成功:1.设置SO_BINDTODEVICE绑定一个设备;2.增加一条设备路由ip route add ip/mask dev device
if (fl.fl4_src == 0)
fl.fl4_src = inet_select_addr(dev_out, 0, RT_SCOPE_LINK);
res.type = RTN_UNICAST;
goto make_route;
}
if (dev_out)
dev_put(dev_out);
err = -ENETUNREACH;
goto out;
}
}
因此,不但SO_DONTROUTE没有跳过路由查找,还多了查找失败后的动作,DONTROUTE不是不查找路由表,而是对该包不进行路由,也就是该包是不会经过路由器的,设置了dontroute的套接字发送的包是永远不会发到网关的,但是本地路由查找是避不开的哦!SO_DONTROUTE 多用于仅能确定发送出口却没有路由的情况。不要太着急哦,why?还有arp呢?既然dontroute只关心是否有出口设备,那么如果有出口设备的话,数据到了链路层之后呢?稍微知道网络过程的家伙都知道arp,真实的数据通信是需要链路层信道的建立的,因此,arp是必须的。不必担心,arp完全按照以前的方式进行哦,如果一个主机确实是和发送主机是直连的,那么很显然,只要目的主机有路由,即使它们配置的ip不在一个网段,arp回复就会正确被收到的,so_dontroute也正是用于这一情况的,但是如果我们仅仅想做一下实验,也就是目的ip是不同网段的一个不存在的ip的话,arp还会有回应吗?如果没有回应,数据如何通信呢?很简单,如果没有回应,数据无法进行通信,只有有回应才可以的,但是显然,这个回应是不可能发回来的,然而考虑两种情况,第一是网关启动了arp代理,这样虽然发送套接字设置了SO_DONTROUTE的数据不被网关路由,但是由于网关回应了arp(实际上是一次欺骗),数据还是被发往网关了;第二种情况是网关没有启动arp代理,但是却使用了ip-mac保护,它侦测到有一个奇怪的arp请求(内部请求一个不同网段的ip地址的mac),虽说这并不是非法的,路由器也可以由于好奇而回应自己的mac,从而将数据包引入自己这里,如果说请求一个不同网段ip的mac并不是非法的,但是如果网关设定了ip和mac的绑定,它并没有在这些绑定中查到目的ip的任何信息,于是它就认为这个请求有问题,于是仍然可能回应自己的mac而引入这个数据包。所以除非你真的有一个直连的机器,否则数据不但发不到,一些arp的信息都足以是你疑惑万分。
SO_BINGTODEVICE仅仅为套接字绑定了一个接口,而SO_DONTROUTE仅仅是不通过网关发送,不管你设置网关是什么,它总是以数据的目的地址作为下一跳。