1.linux的虚拟网卡-tun
linux的虚拟网卡驱动可以配置为两种模式,一种是点对点的tun模式,一种是以太网tap模式,实质上tun模式中从虚拟网卡出来的是ip数据报,也就是三层数据,而tap模式中从虚拟网卡中出来的以太网帧,也就是二层链路层数据。tun模式封装的ip数据报可以直接传输给对端,这种点对点模式中对端是确定的,不需要寻址的,因此tun模式下的虚拟网卡是没有mac地址,即链路层地址的,因此tun模式的通信可以省去arp地址解析,但是tap模式下就不同了,tap模式下封装的是一个完整的以太网帧,按照802.3协议的格式,因此在早期版本的tun驱动中,一个tapX网卡的mtu是不能设置得超过以太网最大mtu的,然而最新的驱动解除了这个限制,tap模式的虚拟网卡完全模拟一个广播式以太网网卡,由于是广播网络,因此需要解析三层地址到链路层,故而arp就是免不了的了,所有通过类似OpenVPN之类的用户辅助进程连在一起的tap模式网卡都属于一个以太局域网而不管它们物理上离得多元,依靠arp进行地址解析,另外tap模式的虚拟网卡还可以封装非ip协议的三层数据,本质上它完全模拟802.3以太网。tun驱动的网卡初始化函数如下:
static void tun_net_init(struct net_device *dev)
{
...
switch (tun->flags & TUN_TYPE_MASK) {
case TUN_TUN_DEV:
dev->hard_header_len = 0; //tun设备没有mac地址,点对点,不需要arp解析地址,因为点对点的两台主机一个三层地址和一个二层地址一一对应,一般只区分主和辅,或者主动和被动
dev->addr_len = 0;
dev->mtu = 1500;
dev->type = ARPHRD_NONE;
dev->flags = IFF_POINTOPOINT | IFF_NOARP | IFF_MULTICAST;
dev->tx_queue_len = 10;
break;
case TUN_TAP_DEV:
dev->set_multicast_list = tun_net_mclist;
*(u16 *)dev->dev_addr = htons(0x00FF); //tap设备有mac地址
get_random_bytes(dev->dev_addr + sizeof(u16), 4);
ether_setup(dev); //按照以太网进行设置
break;
}
}
下面的tun_get_user是用户态进程写虚拟网卡字符设备的时候调用的函数
static __inline__ ssize_t tun_get_user(struct tun_struct *tun, struct iovec *iv, size_t count)
{
...
switch (tun->flags & TUN_TYPE_MASK) {
case TUN_TUN_DEV: //tun设备只需要解出数据即可
skb->mac.raw = skb->data;
skb->protocol = pi.proto;
break;
case TUN_TAP_DEV: //tap设备还要处理以太头
skb->protocol = eth_type_trans(skb, tun->dev);
break;
};
...
netif_rx_ni(skb);
...
}
2.关于PACKET_OTHERHOST
这个宏标记数据的目的地址不是本机,在比较数据帧的目的mac地址和入口网卡的mac地址之后发现它们不同的时候设置,但是却不是在这个时候丢弃,因为很多的协议模块可以处理这一类数据包,典型就是以太网抓包工具,因此无论是否设置了PACKET_OTHERHOST都要遍历三层协议逐个往上投递,由三层协议来决策该协议是否要处理这个PACKET_OTHERHOST的数据包。
3.源ip地址的选择
如果一个机器访问另一台机器,比如说最简单的ping,目的地址有了,源地址如何选择呢?特别是在主机有多块网卡每块网卡配置了多个ip地址的情况下。答案是通过路由查找结果来选择的,做以下实验:
PC1的配置:
eth0:192.168.1.3
eth0:1:1.2.3.4
eth0:2:7.6.5.3
eth1:172.16.1.3
eth1:1:4.3.2.1
默认网关:192.168.1.1
PC2的配置:
eth0:192.168.1.4
eth0:1:1.2.3.5
eth1:7.6.5.4 (没插网线)
默认网关:192.168.1.1
在上述配置下ping 119.75.217.56(百度的一个地址),然后在PC1上用tcpdump -i eth0 host 119.75.217.56查看源地址,发现是192.168.1.3,然后不停止ping,在PC1上添加一条主机路由:route add -host 119.75.217.56 gw 1.2.3.5,在看tcpdump的输出,这时源地址马上就变成1.2.3.4了。这仅仅在一块网卡上证明了路由决定源地址,如果我们想让源地址选择为4.3.2.1怎么办呢?在PC2上也配置一个4.3.2.0网段的地址可以吗?配置这个地址是可以的,但是增加路由时就会出错,因为PC1的eth1和PC2的eth0并没有直连,你不可能让数据以4.3.2.1为源地址但是却从eth0发出来。
那么反过来呢?PC1如何选择7.6.5.3作为源ip呢?很简单,那就是将119.75.217.56的网关设置成PC2的eth1即可,虽然它没有插网线,只要入口即eth0的arp_ignore为0或者为3且7.6.5.4不是host地址,就一定能成功。另外需要注意的是,7.6.5.4这个119.75.217.56的网关地址可以配置在任何网卡上,除了loopback有些特殊,原则上说loopback上的ip是不可路由的,但是只要你手工配置一些路由loopback上的地址还是可以被发现的,见实验1。
因此,网络通信的源地址一般都是在路由之后添加的,除非提前绑定一个源地址,路由之后会根据路由查找的结果添加源地址,如果是由网关发送,那么就添加和网关同一子网的ip地址,如果是直连路由,则选择直连路由子网的ip,顺便说一下,在添加路由的时候,如果指定了via参数,也就是通过网关发送,那么实际传输数据的时候arp请求会请求网关的mac,如果没有via参数而只有一个dev参数则是直连路由,通信时arp请求将会直接请求目的地址的mac
实验1:
PC1上的配置:
eth0:192.168.1.3
eth0:1:4.3.2.1/24(必须保留非32位的掩码,否则就要另外添加到4.3.2.2的路由了)
PC2上的配置:
eth0:192.168.1.4
eth0:1:4.3.2.2/32(或者lo:1:4.3.2.2/32)
PC1和PC2的eth0直连
仅仅考虑arp的情况,因为任何通信的第一步都是arp解析,只要arp解析到了mac地址,再配以路由通信就一定可以进行,因此这里只考虑arp情况。在PC1上ping 4.3.2.2,不通,这是PC2上因为没有回来的路由,可是PC2的eth0的mac已经解析到了,可以通过arp命令来查看。如果在PC2上添加一条回程的路由理论上就可以通了,但是这个回程路由有一个限制,那就是其type在默认情况下不能是非unicast的,如果你配置了一条诸如下面的路由,不但不通,连arp回复都收不到了:
ip route del table local local 4.3.2.1/32 dev eth0
注意,这条路由的type是local,可选的type有[unicast | local | broadcast | multicast | throw | unreachable | prohibit | blackhole | nat],这是为什么呢?因为在系统查找到路由的时候,一般是不在乎源ip地址的,在查找完路由表之后才会验证源ip地址,而不是在查找过程中验证,这是为了防止一些混乱发生,毕竟路由本身的含义旨在找到目的地址,如果源ip地址是可达的,那么一切顺利,否则就要进行一些特殊处理了,并且以源ip地址作为目的地址查找路由的结果的type一般不能也是local,因此如果查到了一个源ip地址的路由type也是local,那就有问题了,由于arp_process中需要查找路由,而查到的路由有问题,因此是不会回复arp请求的,故而通信无法进行下去,可以通过将上述路由的type从local改成unicast来修正。那么如果真的是本机访问本机的话,如此处理岂不出错?实际上这种情况linux的协议栈作了特殊处理,在ip_route_output_slow中如果查到目的地址路由的type是local的话,就将其dst_entry给设置了,并且发送设备设置成了loopback_dev,在loopback_dev的xmit函数中,skb以及其dst_entry丝毫没有被触动,因此在ip_rcv_finish的时候,下面的路由查找就根本不会进入而直接进入上层的处理:
if (skb->dst == NULL) {
if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))
goto drop;
}
代码中在哪里体现了不能配置type为非unicast的路由呢?在ip_route_input_slow中调用:
if (res.type == RTN_LOCAL) {
int result;
result = fib_validate_source(saddr, daddr, tos,
loopback_dev.ifindex,
dev, &spec_dst, &itag);
if (result < 0)
goto martian_source;
if (result)
flags |= RTCF_DIRECTSRC;
spec_dst = daddr;
goto local_input;
}
看一下fib_validate_source的具体逻辑:
int fib_validate_source(...)
{
struct in_device *in_dev;
struct flowi fl = ...; //将源地址作为目的地址来查找路由
...
if (fib_lookup(&fl, &res))
goto last_resort;
if (res.type != RTN_UNICAST) //如果不是unicast的路由也不行,这就过滤掉了本机路由和广播路由
goto e_inval_res;
...
if (FIB_RES_DEV(res) == dev) { //出口和入口是一致的,很不错,这就是一般需要的情况
ret = FIB_RES_NH(res).nh_scope >= RT_SCOPE_HOST; //路由是否是可以直达的?在添加路由的时候,可以添加网关(通过via参数/或者直接用route命令加gw参数),也可以不添加网关,对于强制添加网关的路由,其“下一跳”就是网关,因此其范围必然是link或者global的,对于没有添加网关的路由,其scope可能是host,也就是说,凡是nh_scope大于等于host的都是可以直达的,没有网关,要么就是本机路由
fib_res_put(&res);
return ret;
}
...
fl.oif = dev->ifindex; //出入口不一致,因此将出口设备强制成入口设备再次查询路由表,之前的那次查找没有限制出口设备,或许有很多条路由,但是仅仅返回了查找到的第一条,本次查找限制了出口设备,可能还会有可用路由返回
ret = 0;
if (fib_lookup(&fl, &res) == 0) {
if (res.type == RTN_UNICAST) {
*spec_dst = FIB_RES_PREFSRC(res);
ret = FIB_RES_NH(res).nh_scope >= RT_SCOPE_HOST;
}
fib_res_put(&res);
}
return ret;
...
}
4.路由表的下一跳问题
每一个路由表项都有下一跳的概念,所谓下一跳就是数据包下一步要发到哪里,一般有两种地方,要么直接发往目的地,要么发往网关。在linux中,rtable是一个路由缓存,其rt_gateway字段指示了需要解析成二层地址的ip地址,在查找完路由之后,其被初始化为数据包的目的地址:
rth->rt_gateway = fl.fl4_dst;
然后在后续的过程中,如果有网关的话,它会被替换成网关的ip地址,这一切发生在rt_set_nexthop中:
if (FIB_RES_GW(*res) && FIB_RES_NH(*res).nh_scope == RT_SCOPE_LINK)
rt->rt_gateway = FIB_RES_GW(*res);
FIB_RES_GW这个宏确定了网关的存在,&&后面的确定了下一跳的scope必须是link,也就是说必须是链路上可直达的,只有这样,接下来的arp解析才能成功。
在rt_intern_hash中会调用arp_bind_neighbour,其中有以下代码:
if (n == NULL) {
u32 nexthop = ((struct rtable*)dst)->rt_gateway; //取出nexthop,准备解析
...
n = __neigh_lookup_errno(&arp_tbl, &nexthop, dev);
...
dst->neighbour = n;
}
在添加路由的时候,内核协议栈代码作了以下的限制(不考虑多路径和NAT等复杂的情况):
a.你不能添加一个scope比host还大的路由,再大就是nowhere了;
b.如果路由的scope为host,那么下一跳的scope在大多数情况下就是nowhere;
c.如果配置了网关,那么下一跳的scope就是link;
d.如果没有指定网关,那么下一跳的scope就是host。
最后看一下icmp源地址掩码请求/应答,有上述的d,这个理解起来就简单了,由于icmp的源地址掩码的应答只在本子网发送,因此目的地肯定是不配置网关的,因此在fib_validate_source中,只要发现“下一跳”的scope是host(本地子网)或者nowhere(本机)的情况,就会返回1,然后在外部会置上RTCF_DIRECTSRC标志,当主机回复掩码应答的时候,如果没有发现有这个标志就不再发送。
5.arp_announce的作用
它限制了发送arp请求时的源地址的选择,和arp_ignore的作用正好相反,如果是0,则无条件按照路由查找的结果进行arp,如果是1则选择和请求目标在同一子网的地址,并且设备也要和路由选择的结果一致,不管怎样,即使没有找到这样的地址,还是会选择一个地址的,选择策略是,首先在路由建议的出口设备中找和请求目标同一子网的地址,如果不成则随意找一个出口设备的scope小于link的地址,如果还没有的话则遍历所有网卡选择ip地址scope不是link且小于link的地址,也就是host地址。其代码逻辑如下:
struct net_device *dev = neigh->dev;
u32 target = *(u32*)neigh->primary_key; //路由查找的结果
struct in_device *in_dev = in_dev_get(dev);
switch (IN_DEV_ARP_ANNOUNCE(in_dev)) { 得到arp_announce的值
case 0: //使用路由结果中的源ip,但是如果没有配置源ip的话,saddr会是0,很多时候配置路由的时候不会配置源ip的,只配置一个出口设备,因此很多时候都是0,比如ip route add 1.2.3.4/32 dev eth2
if (skb && inet_addr_type(skb->nh.iph->saddr) == RTN_LOCAL)
saddr = skb->nh.iph->saddr;
break;
case 1: //使用和请求的目的地址在同一子网的源ip,和上面一样,如果没有找到的话,那saddr就是0了
saddr = skb->nh.iph->saddr;
if (inet_addr_type(saddr) == RTN_LOCAL) {
if (inet_addr_onlink(in_dev, target, saddr))
break;
}
saddr = 0;
break;
}
if (!saddr) //如果没有找到源ip,那就要选择一个了,在scope link之下选择一个,首选dev上的,如果没有则遍历所有的网卡
saddr = inet_select_addr(dev, target, RT_SCOPE_LINK);
arp_announce远远没有arp_ignore复杂。
6.ip route命令
它真的是一个好东西。