server模式以及点对点模式的OpenVPN
前文好几次说过,虽然OpenVPN也可以创建隧道,该隧道封装了IP数据报或者以太帧,然而却和使用IPSec VPN的网络拓扑无法做到兼容,这是因为在网对网模式下,默认配置下,使用tun虚拟网卡模式的OpenVPN的客户端虚拟网卡上要强制做SNAT,否则便通不过OpenVPN服务器端的检查。这是因为OpenVPN服务器是根据分配给客户端的虚拟IP地址来维护客户端和自己保存的session(也就是multi_instance)的对应关系的。为了定位是哪个客户端发来了封装的数据包,OpenVPN需要解析出封装载荷中的源IP地址,从而找到和自己保存的multi_instance的对应关系。初看起来,这好像是OpenVPN服务器设计时端偷了一个懒,然而下一节会说明,事实并不是那样。
实际上,一般认为万万不该使用带内数据来定位session。OpenVPN这么做了,于是它将抹去真实客户端的IP地址(仅限TUN模式),所有的客户端网络内的数据发起者的IP地址都将伪装成OpenVPN客户端的虚拟IP地址。实际上,完全可以使用客户端提交的证书信息生成一个唯一的键值,用这个键值来标识具体客户端,控制数据永远不要和真实数据杂糅(一个例外是xml文件包含base64编码的情景,可是巧的是,base64编码原则保证它永远不会编码到”<”,”>”等字符)。然而这不容易做到。我们来看看这是为什么。
本质原因就是OpenVPN协议的设计缺陷–姑且先这么说吧,在OpenVPN的协议头中,丝毫找不到任何可以定位多个安全session中一个的信息,而这个信息在IPSec的封装比如ESP头中是有的,由SPI来标识。对于OpenVPN的协议头中(OpenVPN协议解析-网络结构之外),只有16位长度,5位操作码以及3位的和sesion软切换相关的key ID,并没有什么能标识该封装属于哪个安全session,安全session都在在外部维护的,因此必须在数据封装中找一个字段能标识它属于哪个session,也就是属于哪个multi_instance。最终确定的就是载荷中的原始IP地址。这么做当然很简单,但是为server模式下的网对网扩展制造了困难。
虽然完全在socket和reliablity层,multi_instance的context中的tls_multi已经可以定位是哪个客户端了,但是这些都是在OpenVPN数据包封装外部维护的,因此正如代码注释中所说,要确保这个封装载荷确实属于“那个客户端”,这也是防止地址欺骗的一种方式,后面会谈到,必须通过复杂的配置才能做到网对网的透明互联。
不要把OpenVPN想成什么太伟大的东西,它有缺陷是超级正常的,本着实用主义和拿来主义的原则,难道这个真的很重要么?只要够我们用就可以了吧。还好,如果你非要用最简单的方式实现和IPSec的网络拓扑兼容,那么OpenVPN也不是做不到,那就是它的点对点模式。我们之前说的都是它常用的server模式,对于点对点模式,是无需检查载荷的源地址的,和IPSec VPN的拓扑是兼容的,但是却牺牲了多对一的良好扩展性。
事实上,server模式默认本来就不是让你用于网对网拓扑的,而是适用于主机对网络的拓扑,换句话说,一般而言,这种模式的OpenVPN客户端一般都是直接安装于主机之上,而不是安装于路由器或者带有转发功能的服务器上的。对于点对点模式,OpenVPN的服务器和OpenVPN的客户端唯一的确定了一条隧道,此时也就不必再检查载荷中的源IP地址到底是不是这个客户端的了。确定自己的需求,配置不同的参数选项即可让OpenVPN为你服务,这也就是OpenVPN的强大之处。
OpenVPN的高级路由技术
如果你只知道OpenVPN在服务器端会对数据载荷的源地址进行检查从而确定它一定是OpenVPN的虚拟地址的话,那么你将丧失掉OpenVPN的一大部分功能。实际上OpenVPN可以通过较为复杂的配置做到透明的网对网的拓扑,但是默认情况下不行。这个也许就是OpenVPN胜出IPSec的地方吧,上一节说OpenVPN没有把session相关的信息封装于OpenVPN的协议头中是一种协议设计的缺陷,然而它这么做也许就是想将灵活性留给配置,这样看来,这也是它的一大优势之所在。 如果你看上了server模式的优势,并且一定要使用该模式而不选择使用点对点模式的话,你就必须需要进行相对复杂的配置。
IPSec默认就是网对网的透明互联,而OpenVPN却需要复杂的配置,这就是它们的不同之所在。下面说一下OpenVPN的路由原理以及如何进行具体的配置
TAP模式虚拟网卡对源地址的检查以及路由配置思路
最好的资源是源代码。在server模式下,multi.c文件处理数据包的路由,OpenVPN封装的数据到达服务器端的时候,multi_process_incoming_link函数处理之,tap情况下,OpenVPN解析载荷包的源MAC地址而不是IP地址,它必须通过检查才可以,这个检查可以通过用户定义的plugin或者script来完成,默认情况下,源MAC地址必须是分配给该客户端的虚拟网卡的MAC地址。然而同时也允许客户端修改自己虚拟网卡的MAC地址,这种情况发生在MAC地址冲突的情况下。修改过的地址具体能否通过检查,就要看服务器端的plugin或者script的策略了。
在tap模式下,OpenVPN所在的节点可以看做是一台网桥,这种情况下,IP包都是透传的,不管源IP是什么,OpenVPN服务器都是不管的。
因此,tap模式下,IP方式的网对网拓扑是可行的。最关键的是添加路由,只要有路由,数据通信就可能。OpenVPN客户端是一台网桥,同时它也是一台路由器,其上需要配置到达OpenVPN服务器侧网络的路由,而这个路由一般都是OpenVPN服务器推送下去的。我们看看TAP模式下如何来进行配置:
TUN模式虚拟网卡对源地址的检查以及路由配置思路
同样在multi_process_incoming_link函数中,tun模式对载荷数据包源地址的检查是针对IP地址进行的而不是MAC地址。对于IP互联网络而言,这就需要更多的配置来让执行逻辑通过检查。默认情况下,载荷数据包的源IP地址必须是OpenVPN服务器分配给该客户端的虚拟IP地址。
综合2.1和2.2我们发现,针对载荷源地址的检查其实是在OpenVPN执行用户接入控制之后的又一层检查,该检查一般可以用于和防火墙的联动。
下面看一下代码是怎么处理这个检查的。在函数multi_process_incoming_link中,如果判断是TAP网卡模式,那么调用mroute_extract_addr_from_packet解析出载荷的源地址,解析的过程在函数mroute_extract_addr_ether中:
memcpy (src->addr, eth->source, 6);
可见解析出来的是以太头的MAC地址。再接下来会调用:
if (multi_learn_addr (m, m->pending, &src, 0) == m->pending)
来判断是否可以为源地址为src的载荷发起者转发该数据包。在判断中会调用用户配置的plugin或者script来协助抉择。
如何配置TUN模式的OpenVPN从而通过检查
在得到配置之前,首先要理解一个OpenVPN中的参数选项iroute,解释起来就是internal route,其实就是独立于系统路由之外的OpenVPN的路由,该路由起到了访问控制的作用,特别是是在多对一即server模式的OpenVPN拓扑中,该机制可以在防止地址欺骗的同时更加灵活的针对每一个接入的客户端进行单独配置。在多对一的情况下,必须要有机制检查访问内网资源的用户就是开始接入的那个用户,由于OpenVPN是第三层的VPN,而且基于独立于OpenVPN进程之外的虚拟网卡,那么一定要防止单独的客户端盗用其它接入客户端的地址的情况。在特定客户端的上下文中配置iroute选项,它是一个ip子网,默认是客户端虚拟ip地址掩码是32位,你可以在保证路由以及IP地址不混乱的前提下任意配置它,OpenVPN仅仅让载荷数据包的源IP地址在iroute选项中配置的子网内的主机通过检查,其它数据载荷一律drop。比如客户端虚拟IP地址是172.16.0.2,而OpenVPN服务器针对该客户端的iroute参数是10.0.0.0/24,那么只要载荷数据包的源IP地址在10.0.0.0/24这个子网中,一律可以通过检查。
iroute是OpenVPN内部维护的一个路由,它主要用于维护和定位多个客户端所在的子网以及所挂接的子网,鉴于此,OpenVPN对所谓的网对网拓扑的支持其实超级灵活,它能做到这个虚拟专用网到哪里终止以及从哪里开始。
对于TUN模式的虚拟网卡构建的VPN,源代码里是这样说的(还是在multi_process_incoming_link函数中):
…
else if (multi_get_instance_by_virtual_addr (m, &src, true) != m->pending)
…
在multi_get_instance_by_virtual_addr里面实现判断是否可以转发。其参数src是载荷包的源IP地址,在multi_get_instance_by_virtual_addr函数中,有以下的循环:
for (i = 0; i < rh->n_net_len; ++i) {
tryaddr = *addr;
tryaddr.type |= MR_WITH_NETBITS;
tryaddr.netbits = rh->net_len[i];
mroute_addr_mask_host_bits (&tryaddr);
/* look up a possible route with netbits netmask */
route = (struct multi_route *) hash_lookup (m->vhash, &tryaddr);
if (route && multi_route_defined (m, route)) {
/* found an applicable route, cache host route */
struct multi_instance *mi = route->instance;
multi_learn_addr (m, mi, addr, MULTI_ROUTE_CACHE|MULTI_ROUTE_AGEABLE);
ret = mi;
break;
}
}
该循环就是判断特定客户端是否配置了iroute参数指定的内部路由,如果配置了,那么载荷包的源IP地址是否在iroute参数指定的子网内,如果是,则向tun网卡转发该包,如果不是,则丢弃之。举个例子就是,如果OpenVPN服务器中配置一个特定客户端的iroute参数为:
10.0.0.0/24
192.168.0.0/24
102.168.1.0/24
那么只要载荷包的源IP地址在这些网段内,都可以被OpenVPN转发。 我们看看TUN模式下如何来进行配置:
2.4. 为何说网对网通透拓扑的配置很复杂
上文看下来,配置也不算太复杂。然而复杂性不在于配置本身,而在于和UI的联动。没有几个人会在使用的时候手工去进行配置,少数几个这样做的人几乎都是技术人员,并且要精通IP路由技术,精通防火墙技术等。大多数的人只希望通过人性化设计的配置界面来进行配置,那么界面如何来配置这些参数,就需要大量的自动化脚本,编写这些脚本并且做到没有漏洞,不重复,无缺漏,其本身就是一件很有工作量的事情。
2.5. OpenVPN的redirect-gateway选项
作为一个第三层的VPN,OpenVPN推送的路由很容易和本地路由冲突从而造成数据包环路,要知道的是,OpenVPN实施的路由和系统的IP路由完全一样,在协议栈层面上看,没有任何区别,况且,操作OpenVPN的人大多数又没有操作Cisco设备的人的那种资质,因此把路由搞混乱从而导致网络down掉是常见的事。
有时候,手工配置路由的过程反而比通过UI来配置来的更清晰,然而这仅仅是针对对IP路由有着很深刻理解的人员来说的。OpenVPN作为一款通用的VPN,完全可以为用户着想,努力去避免路由配置失误导致的路由环路。下面举一个例子来说明路由环路: 客户端所在网段:192.168.1.0/24 默认网关:192.168.1.1
服务器外网口所在网段:192.168.2.0/24 服务器端的OpenVPN的push route配置:route 192.168.3.0/24;route 192.168.1.0/24… 我们发现,服务器将客户端自己的网段也推送下来了,这样难道不会造成混乱么,本来要通过客户端的默认网关寻址到OpenVPN的真实地址或者通过直连路由寻址到直连网段的主机,现在一律要走虚拟网卡了,而虚拟网卡出来的包最终经过封装后要由192.168.1.0/24网段的物理网卡出去的,出去时由于路由指向虚拟网卡,包又一次进入虚拟网卡…结果数据包一直在累计,越来越大,就是发不出去,在本机的192.168.1.0/24网段的物理网卡和虚拟网卡之间环来环去…
除了上述的问题,还有一个问题涉及到安全方面,那就是一旦连接到VPN,客户端主机就要断开其它不安全的网络连接,以防不安全因素通过VPN隧道渗入内网,那就要阻断这些不安全的网络,最直接的方式就是删去到达不安全连接的路由。幸运的是,OpenVPN可以解决这个问题。
redirect-gateway选项解决了这个问题,它将客户端的默认网关指向了虚拟专用网,也就是VPN隧道本身。然而它并没有很鲁莽的删除掉原来的默认网关,而是首先在原始的路由表中查询到到达OpenVPN服务器的路由,然后根据查询结果OpenVPN添加一条到达OpenVPN服务器的主机路由,第二步就是删除原来的默认路由(删除前保存一份,以备恢复),这样的话,默认网关就指向VPN隧道了,既阻断了不安全网络,又保证了OpenVPN服务器的可达性。当然,这个redirect-gateway选项还有自身的参数,定制通过哪种方式来操作上述步骤,都比较简单,可以查阅man手册。
总结
OpenVPN是一个极其复杂又极其灵活的VPN,它开放了甚多的网络配置接口,比如IP路由的接口,它可以通过事件机制对VPN隧道状态进行实时的监控和保持更新。OpenVPN将网络层的诸多操作集于自身,同时又将策略留给配置人员,保持了极大的灵活性,在强大的功能和配置的简洁之间找到了一个极佳的平衡点。OpenVPN在横向拓扑上支持server模式和点对点模式,在纵向协议上支持二层模式和三层模式,可以通过配置进行任意的扩展。实际上,它可以模拟任意的网络拓扑,满足你的任意需求,当然,功能强大的代价就是配置的复杂,所谓的配置的复杂并不单单是指如何可以手工的配置这些,而更多的是指如何可以保持以及维护这些配置,根据现实环境的变化灵活的更改配置,在整个过程中,最小化工作量。