前面的《OpenVPN高级路由技术》系列文章阐述了OpenVPN中潜在的内部路由技术以及具体的配置方法,另外也谈到了如何使用OpenVPN将网络进行扩展,然而具体到这些内部路由是怎么工作的,并没有谈及。为什么直到现在才说这些呢?我以为最好先不要管内部原理,先要学会用,只有当你明白这是个黑盒子的时候,你才会有目的的想进入它,希望它成为你的白盒子。
本文谈一下OpenVPN内部路由的实现要点,如果我想以广义的思路讨论分层协议的话,我希望把链路层以太网交换机的“MAC地址-端口”映射表也当成路由表,在此先为那些按照教条较真的学者们(IT界颇多)做个说明,以免不必要的评论。
OpenVPN支持server模式,这个就不多说了,其实就是允许多个OpenVPN客户端连接到一个OpenVPN服务器,这分为两种情况:
1. tap模式下的OpenVPN节点可以被看作一个虚拟的以太网,节点之间的通信可以通过以太网协议来完成;
2. tun模式是三层模式,也就是说,tun模式下的OpenVPN节点可以被看作一个虚拟的IP网络,节点之间的通信完全靠路由来完成;
如此,OpenVPN可以既是以太网模型又可以是IP网络模型,但是同一时间只能是一种。那么在每一种模型下,OpenVPN就扮演了该网络模型的核心设备的角色,对于以太网,它是交换机,对于IP网络,它是路由器,当然这种设备是虚拟出来的,是软件实现,是由OpenVPN的代码实现的。前文总是说OpenVPN是一个虚拟的交换机,又说OpenVPN是一个虚拟的路由器,那么它到底如何做到的的呢?
我们首先看一下以太网交换机的实现,然后再说路由器的实现。我并不准备详述交换机的背板总线以及全双工交换避开CSMA/CD的概念,而是着重看一下交换机如何路由数据帧。其实以太网的路由最简单,总结起来就是两部分:能单播的就单播;不能单播的就广播。这两句话是通过一个称为“自学习”的机制实现的。
以太网交换机内部有一个简单的映射表,映射MAC地址和端口信息。注意,这个映射表的容量是有限的,这也常常成为一种攻击向量…(注意,最美丽的天使是断了翅膀再也回不了天国的天使),只要有数据帧经过交换机,哪怕是一个广播,交换机就记录下该数据帧的源地址和进入的端口,将之做成一个映射项,存入这个以太网交换机的“路由表”,隔一段时间,这个映射会失效。以上就是以太网的简单的“路由表维护机制”。
在OpenVPN中,如果它真的要实现一个虚拟的以太网交换机,那么它就应该实现一个类似的路由表,保存MAC地址和端口的映射项。在OpenVPN中,其实并不存在真正的“端口”,所谓的端口都是虚拟出来的,在OpenVPN中表现为一个结构体multi_instance,所谓的MAC地址还是MAC,这个并没有变。那么在OpenVPN中,如果想实现一个虚拟的交换机,那么就是要实现一个MAC地址和multi_instance的映射表,对于映射表之类的,用软件实现要比用硬件实现简单得多,C语言可以很简单的实现类似table,hash之类的数据结构。
由于tap模式下,所有的VPN节点构成了一个虚拟的以太网,那么这个虚拟的交换机的MAC地址-multi_instance映射表就要起码包容所有的这些VPN节点,至于是否要包容其它的MAC地址,那就看你的网络拓扑了,基本上tap模式下的拓扑有三类:
1. 全网桥接
这种拓扑几乎不用任何路由,整个网络都是桥接的,拓扑图如下:
这样子,OpenVPN虚拟出来的交换机就和各个VPN网段内部的物理交换机是完全并列的关系。这种拓扑下,OpenVPN的虚拟交换机需要学习所有VPN节点后面物理网段的所有的主机的MAC地址信息。
2. 仅VPN节点桥接,其它路由
这种拓扑最简单。只需要考虑接入的VPN节点即可。拓扑图如下:
这样子,OpenVPN虚拟交换机只需要学习VPN节点的MAC地址即可
3. 一些VPN节点桥接,另一些路由
这种拓扑其实上上述两种的综合。拓扑图就不再画了吧。 这样子,OpenVPN虚拟交换机需要学习的MAC地址信息完全取决于各个VPN节点所在的主机是怎么配置的。
理解了上述的拓扑,继续下去就简单了。我们知道在OpenVPN中有一个multi.c文件,里面有一个multi_process_incoming_link函数,该函数中有两个逻辑,其中之一是处理TUN模式的逻辑,这个一会儿再说,另一个就是处理TAP模式,这个正是我们现在要分析的。该函数处理网络上过来的使用OpenVPN协议封装的加密流量,将之解密,然后写入到虚拟网卡或者经由其它multi_instance加密发送给其它的OpenVPN客户端。
在分析代码之前,首先要明白的是,对于TAP模式,OpenVPN实现的虚拟设备主要处理虚拟交换机的MAC-“端口(multi_instance)”映射,而对于TUN模式,OpenVPN主要处理路由缓存,其实就是内部路由所体现的一张虚拟的路由表。不管是tap模式还是tun模式,路由信息都是保存在一个hash表中的,在OpenVPN的server模式中,这个hash表为multi_context的vhash字段。在multi_process_incoming_link中,处理TAP模式的逻辑如下(不考虑内部包过滤):
else if (TUNNEL_TYPE (m->top.c1.tuntap) == DEV_TYPE_TAP) {
//从解密后的载荷包中解析中源MAC地址和目标MAC地址
mroute_flags = mroute_extract_addr_from_packet (&src,
&dest,
NULL,
NULL,
&c->c2.to_tun,
DEV_TYPE_TAP);
if (mroute_flags & MROUTE_EXTRACT_SUCCEEDED) {
//虚拟交换机的学习,和物理交换机的学习机制一样
if (multi_learn_addr (m, m->pending, &src, 0) == m->pending) {
if (m->enable_c2c) {
if (mroute_flags & (需要广播的标识)) {
multi_bcast (m, &c->c2.to_tun, m->pending, NULL);
} else {
mi = multi_get_instance_by_virtual_addr (m, &dest, false);
/* 如果目的地址对应另外一个multi_instance,那么就直接将数据“路由”到该客户端,不再往虚拟网卡中写 */
if (mi) {
multi_unicast (m, &c->c2.to_tun, mi);
register_activity (c, BLEN(&c->c2.to_tun));
c->c2.to_tun.len = 0;
}
}
}
} else {
msg (bad source address);
c->c2.to_tun.len = 0;
}
} else {
c->c2.to_tun.len = 0;
}
}
在mroute_extract_addr_from_packet中解析出该数据包的目的地址(此时数据包已经被解密了),将其保存到dest变量,接下来就要地址学习了,和物理交换机一样,此时虚拟交换机学习的是发送该数据包的源地址,所谓的学习过程很简单:
- 在vhash中查找这个src,如果没有找到则添加,同时和该src的multi_instance关联;
- 在vhash中查找这个src,如果找到了,则更新vhash的对应项。 在地址学习之后,接下来调用multi_get_instance_by_virtual_addr试图找到目的地址对应的multi_instance,注意,这个对应关系也是虚拟交换机学习得到的,在multi_get_instance_by_virtual_addr函数中,实际上是在同一个vhash中进行查找的,就是刚才虚拟交换机地址学习时更新的那个vhash。如果找到了,则直接将数据包路由给对应的multi_instance,也就不再往虚拟网卡中写了,否则就写入虚拟网卡。
值得注意的是,前一段的一句话“如果找到了,则直接将数据包路由给对应的multi_instance”,为什么不是“对应的OpenVPN客户端”而是“对应的multi_instance”呢?这是因为在tap模式下,如果有虚拟网卡和物理网卡桥接的情况,那么实际上OpenVPN节点后面的节点也处于同一个虚拟以太网中,此时对应的目的地址就不再是OpenVPN客户端本身了,而是该OpenVPN客户端后面的一台主机。在tap模式下,主要有两个函数调用值得注意,一个是multi_learn_addr,另一个是multi_get_instance_by_virtual_addr,前者学习MAC地址和multi_instance的映射,以src作为输入,后者根据学者学习的结果,从OpenVPN的虚拟交换机的映射表中取出和dest对应的multi_instance,以dest作为输入,learn函数将一个项插入这个映射表,而get函数则试图从这个表中取出一项,其实就是查表的过程。
对于TUN模式,情况比较复杂,因为全部要通过路由走,这里的路由分为三部分:第一部分是OpenVPN客户端和OpenVPN服务器之间建立连接时两个TUN网卡之间的点对点路由,这个路由是很显然的;第二部分是为了和VPN节点之外的目的网络通信而设置的系统路由,该路由要push到OpenVPN客户端;第三部分是服务器进行client-to-client路由以及验证数据包IP地址或者从服务器端主动发起向客户端访问时使用的OpenVPN内部路由。其中内部路由是最复杂的,内部路由不光可以限制数据包的发起地,还可以路由从服务器端以及其后发起的数据包到客户端或者客户端后面,这段代码体现在multi_process_incoming_tun中:
multi_set_pending (m, multi_get_instance_by_virtual_addr (m, &dest, dev_type == DEV_TYPE_TUN));
可见,只要服务器从tun字符设备读取了IP数据报(该数据报通过系统路由到达tun网卡并从中发出,而该系统路由要么通过手工配置,要么通过客户端push-peer-info推送至服务器端),就会以目的地址为输入调用get_instance函数,如果该目的地址就是OpenVPN的客户端虚拟地址或者某个客户端的iroute可达的地址,那么就会找到相应的multi_instance,找到的这个映射关系也会被加入到内部路由缓存中,实际上OpenVPN通过tun直连路由和iroute配置的路由生成了一张内部路由表,即使没有缓存,每次查询这个路由表也会得到正确的结果的,只是每次都执行最长掩码匹配效率会大打折扣,这正如Cisco以及很多高端路由器将路由表和转发表分开是一样的道理。在看tun路由学习实现之前先看看tun模式的拓扑图:
下面看一下TUN模式的内部路由实现,还是multi_process_incoming_link函数中,不过这次处理的是另外一个分支:
if (TUNNEL_TYPE (m->top.c1.tuntap) == DEV_TYPE_TUN) {
//同tap模式一样,从解密后的载荷包中解析中源地址和目的地址,不过这次是IP地址
mroute_flags = mroute_extract_addr_from_packet (&src,
&dest,
NULL,
NULL,
&c->c2.to_tun,
DEV_TYPE_TUN);
if (!(mroute_flags & MROUTE_EXTRACT_SUCCEEDED)) {
c->c2.to_tun.len = 0;
//以src作为输入调用get_instance函数,学习路由缓存
} else if (multi_get_instance_by_virtual_addr (m, &src, true) != m->pending) {
msg (D_MULTI_DROPPED, "MULTI: bad source address from client [%s], packet dropped",
mroute_addr_print (&src, &gc));
c->c2.to_tun.len = 0;
} else if (m->enable_c2c) {
if (mroute_flags & MROUTE_EXTRACT_MCAST) {
multi_bcast (m, &c->c2.to_tun, m->pending, NULL);
} else {
ASSERT (!(mroute_flags & MROUTE_EXTRACT_BCAST));
//以dest作为输入调用get_instance函数,得到路由项
mi = multi_get_instance_by_virtual_addr (m, &dest, true);
if (mi) {
multi_unicast (m, &c->c2.to_tun, mi);
register_activity (c, BLEN(&c->c2.to_tun));
c->c2.to_tun.len = 0;
}
}
}
}
大体上,tun模式的处理过程和tap的很类似,然而处理更对称了,学习过程和get_instance过程都是调用同一个函数。代码虽然比较一致,然而tun的处理过程更加复杂,本来IP路由就比以太网的路由模型复杂得多。OpenVPN的tun模式中,完全靠路由来通信,这些路由来自三个地方,第一个地方就是OpenVPN服务器本身维护的客户端到服务器的直接点对点的路由,也就是两个tun网卡之间的点对点路由;第二个地方是用户通过每用户的iroute指令配置的针对每一个用户后面挂接网络的内部路由;第三个地方就是服务器push到客户端的服务器后面挂接网络的路由。其中第二个地方的路由需要用户手工配置,然而修改了OpenVPN代码后,利用push-peer-info指令,这些路由也可以由客户端推送过来,详见《OpenVPN高级路由技术-反向推送信息》,不过一定要慎用这个功能,一定要对客户端反推上来的路由进行仔细鉴别之后再添加。
总的来讲,OpenVPN在以src作为参数调用multi_get_instance_by_virtual_addr的时候,一定要确保src要么是OpenVPN客户端本身的虚拟IP地址,要么来自管理员显式的iroute配置,后者体现在multi_get_instance_by_virtual_addr函数中CIDR匹配。如果找到了这么一个CIDR路由,那么就算学习了一个路由缓存项,这个项可能会被以后以此为目的地址的IP数据报寻址时用到,而这个基于内部路由的寻址过程体现在上面代码片段的第二次multi_get_instance_by_virtual_addr的调用处,这次调用使用dest为输入,查询OpenVPN的tun直接点对点路由或者通过第一次调用get_instance时learn到的路由。
最终,总结起来就是:OpenVPN实现了两类的虚拟设备,这两类设备处于两个协议层,分别是:
1. TAP模式下的虚拟交换机实现
Tap模式下,主要实现了一个MAC地址和multi_instance的映射,这个multi_instance对应物理交换机的端口。虚拟的交换机拥有和物理交换机一样的自学习机制。
2. TUN模式下的路由缓存实现
Tun模式下,主要就是实现一个路由缓存,因为tun模式下,所有的数据包都是靠路由走的,任意的VPN客户端和VPN服务器都是点对点连接的,期间没有类似以太网的arp广播的链路层地址解析包传送。因此对于tun模式而言,完全是靠IP路由转发数据包的,因此需要的不是别的,正是一个个的路由项。