基本描述
tap-win32虚拟网卡
tap-win32虚拟网卡并不包含任何实际硬件,仅仅是一个驱动,该驱动中包含了一个DHCP服务器程序,可以回复DHCP协议的offer/ack/nak数据包。该驱动的DHCP服务器的参数是可配置的。 tap-win32驱动分为三大部分,首先它作为一个网卡驱动和NDIS中间驱动接口并且设置了一些回调函数,第二部分是一个DHCP服务器,但是这个DHCP的功能是简化的,这部分不和 NDIS接口。二者的关系在NDIS的网卡发送数据的回调函数中体现,网卡发送数据的回调函数中将特殊处理DHCP数据包,然后直接回复。第三部分,tap-win32实现了一个可以读/写/控制的文件,导出给用户态程序比如OpenVPN作为接口。
DHCP的方式配置虚拟网卡
由于windows的网卡的tcp/ip属性中存在“自动获得ip地址”的单选框,且自动获取ip这一DHCP动作又和windows的DHCP client服务绑定(不像Linux可以单独运行任何符合DHCP协议规范的开源或闭源的dhcp client仅仅获取ip,然后通过ifconfig配置到网卡),为了更加方便的配置虚拟网卡又不引起错误,多数情况下使用这种自动的方式来配置OpenVPN客户端的虚拟网卡。
OpenVPN以DHCP的方式配置虚拟网卡的过程如下:
1. OpenVPN客户端首先配置DHCP服务器,该服务器实现在tap-win32的驱动中,但并不和NDIS接口。OpenVPN配置DHCP服务器的地址;
2. OpenVPN客户端配置需要tap-win32中的DHCP服务器分配给虚拟网卡的ip地址;
3. OpenVPN在配置dhcp参数的情况下会以dhcp-internal为参数启动另一个OpenVPN进程,在该进程中调用iprelease/iprenew等win32 API,如果没有dhcp参数也可以不fork另一个进程(接下来的过程由windows自动完成,OpenVPN将不再参与)
4. 确定windows启动了DHCP client服务,该服务将作为DHCP客户端发起discover并终止于DHCP服务器的ack;
5.windows的DHCP client服务收到合理的DHCP服务器的ack之后,内部调用API为虚拟网卡添加ip地址
复制代码
OpenVPN服务端配置
标准配置,ip-win32默认adaptive,即首先采用dynamic(DHCP方式配置) ip地址池为: server 172.16.0.0 255.255.0.0
客户端配置
标准配置,即使用DHCP的方式,假设只有一个客户端,客户端将得到172.16.0.2这个地址。
常见的故障现象
现象一:DHCP问题
在系统日志中,出现大量的dhcp错误,错误描述基本两类,一类是虚拟网卡丢失了ip地址,也就是在租约过半时续约没有成功,另一类就是10049错误。
现象二:路由问题
在内网连接对外的OpenVPN服务器(192.168.81.28:1194)会出现每隔一分钟左右断开重连问题,即使不使用DHCP方式也会有问题,并且连OpenVPN服务器的虚拟网卡ip地址(172.16.0.1)都ping不通。 外网连接公司的OpenVPN服务器(128.42.53.17:1194)正常,仅存在DHCP问题。
故障重现与分析
故障现象一
故障重现
为了便于重现问题,将租约时间配置成最短的30秒,客户端配置中添加如下参数: –ip-win32 dynamic 0 30 重启客户端之后等待30秒的过程中,抓取网络数据包,并且ipconfig /all看一下网络配置:
Connection-specific DNS Suffix . :
Description . . . . . . . . . . . : VPN-Win32 Adapter Versio
Physical Address. . . . . . . . . : 00-FF-40-58-BC-FE
Dhcp Enabled. . . . . . . . . . . : Yes
Autoconfiguration Enabled . . . . : Yes
IP Address. . . . . . . . . . . . : 172.16.0.2
Subnet Mask . . . . . . . . . . . : 255.255.0.0
Default Gateway . . . . . . . . . :
DHCP Server . . . . . . . . . . . : 172.16.0.0
NetBIOS over Tcpip. . . . . . . . : Disabled
Lease Obtained. . . . . . . . . . : 2011年4月20日 17:24:49
Lease Expires . . . . . . . . . . : 2011年4月20日 17:25:19
复制代码
我们发现DHCP服务器的地址为一个网络地址:172.16.0.0。这个地址能访问吗?详见分析。
通过以上配置,30秒之后可以成功得到ip地址,然而超过90秒之后就再也得不到ip地址了,故障现象很容易重现。
故障分析
关于现象本身
分析抓包结果,第一次的DHCP客户端发起的discover可以正确以ack结束,然后到了30秒后会重新discover,中间也会直接以广播地址为目的广播request,这是不合理的,因此DHCP协议中在租约过半时提前续约的时候会直接用原先发现的DHCP服务器作为目的ip地址进行request,并不会用广播地址进行request。用ping试一下172.16.0.0这个地址,发现得到了:
Destination specified is invalid.
复制代码
的错误,错误代号为10049,微软说这个目的地址不对引起的,比如目的地址是一个子网号(掩码后面的所有位)全0网络地址。
但是客户端的172.16.0.2这个地址确实是172.16.0.0分配的地址,这是怎么回事呢?
原来DHCP协议大致有四个阶段:discover/offer/request/ack,
第一个阶段中,源/目的是:0.0.0.0/255.255.255.255,
第二个阶段中,源/目的是172.16.0.0/255.255.255.255,
第三个阶段为0.0.0.0/255.255.255.255,
第四个阶段为172.16.0.0,
没有一个阶段的目的地址是172.16.0.0,因此windows的DHCP客户端服务程序在获取ip的时候完全正常,没有错误,可是在续约的周期过了一段时间以后就会直接访问172.16.0.0(可以称为提前续约,直接向服务器续约,详情参考DHCP规范),如此一来windows的DHCP客户端服务就会得到10049错误(每点击一次虚拟网卡状态的“修复”按钮,就会阻塞在续订ip地址那里,然后在系统事件查看器中就会多出一条“在其上下文中 该请求的地址无效。”警告消息事件,它就是10049,也就是当DHCP client服务续订虚拟网卡ip时,尝试直接访问0.0地址,发生了错误),于是它尝试重新发现DHCP服务器,也就是说以广播地址为目的地址直接request和重新discover,由于tap-win32中的DHCP服务器收到request消息时只给客户端3次请求ip和虚拟网卡不一致的机会,详见tap-win32驱动代码(OpenVPN自带的tapdrvr.c中的ProcessDHCP函数):
if (msg_type == DHCPREQUEST
&& ((dhcp->ciaddr && dhcp->ciaddr != p_Adapter->m_dhcp_addr)
|| !p_Adapter->m_dhcp_received_discover
||p_Adapter->m_dhcp_bad_requests>= 3))//此处的3在源码中为宏定义
SendDHCPMsg (p_Adapter,
DHCPNAK,
eth, ip, udp, dhcp);
else
SendDHCPMsg (p_Adapter,
(msg_type == DHCPDISCOVER ? DHCPOFFER : DHCPACK),
eth, ip, udp, dhcp);
if (msg_type == DHCPDISCOVER)
p_Adapter->m_dhcp_received_discover = TRUE;
if(msg_type==DHCPREQUEST&&dhcp->ciaddr!=p_Adapter->m_dhcp_addr)
++p_Adapter->m_dhcp_bad_requests;
复制代码
3次过后将会返回nak,windows的DHCP client服务也会丢失第一次discover到的DHCP服务器的任何信息,用ipconfig /all查看发现DHCP server的地址成了0.0.0.0,由于tap-win32中的DHCP服务器再也不允许被request了,这样OpenVPN客户端除了重启之外没有任何办法获取ip地址了。
关于返回10049错误的地址
故障是由于172.16.0.0这个地址被配置成DHCP服务器地址引起的,然而这个地址却不能被作为目的ip地址访问,它可能被识别成了网络地址而不是主机地址。然而windows中直接ping网络地址不一定就返回10049错误,我的机器是windows XP SP2,经过测试发现以下情况:
- 为物理网卡添加一个172.16.0.3/16的地址,然后ping 172.16.0.0,10049错误;
- 可是如果添加了一个17.16.0.3/16的地址,ping 17.16.0.3,10049错误,在启动过OpenVPN客户端之后再次ping 17.16.0.0超时,起码链路是通的;
- 可是如果添加了一个17.110.0.3/16的地址,ping 17.110.0.0,10049错误,在启动过OpenVPN客户端之后再次ping 17.110.0.0,仍然10049错误; 没有任何规律,开始以为是windows保留了172.16等私有网段,可是不私有的网段也没有什么规律,windows的socket下面有很多各厂商(比如微软官方提供的或者一些杀毒软件厂商提供的)的LSP/BSP/NSP,winsock是基于spi的而不是基于系统调用接口直接进内核的,因此可以在用户态就经过很多的过滤,很难调试到底发生了什么。
以下较为详细叙述和分析这些微妙的情况:
情况一:
- 为物理网卡添加17.16.0.3/16,ping 17.16.0.0,10049错误,然后删除该ip地址;
- 启动OpenVPN客户端,以17.16.0.0为DHCP服务器地址,然后首先可以分配一个17.16.0.2给虚拟网卡(17.16.0.0这个地址是可以续约的,启动OpenVPN客户端之后ping 17.16.0.0只会超时,并不10049,然而172.16.0.0就不一样,见情况三)。
- 停止OpenVPN客户端,再次添加17.16.0.3/16,再ping 17.16.0.0,超时。
情况二:
- 为物理网卡添加17.16.0.3/16,ping 17.16.0.0,10049错误,然后删除该ip地址;
- 启动OpenVPN客户端,以17.16.0.254为DHCP服务器地址,然后首先可以分配一个17.16.0.2给虚拟网卡(即使续约也不会断开)。
- 停止OpenVPN客户端,再次添加17.16.0.3/16,再ping 17.16.0.0,仍10049错误。
分析一:
情况一最后ping通是因为DHCP客户端曾经知道有17.16.0.0这个DHCP服务器地址的存在(第一次DHCP请求的17.16.0.2就是17.16.0.0分配的),它以某种方式记住了这个地址(在winsock目录中),因此在ping 17.16.0.0的时候windows并没有将它当成“不可访问的”地址处理。情况二就不一样了,17.16.0.0这个地址从来没有被以任何形式发现过,因此winsock会将它作为“不可访问的”地址,从而直接返回10049错误,为了证实猜测,在情况一之后,停止DHCP客户端,禁用虚拟网卡,并且执行netsh winsock reset命令,然后重启机器,再次ping 17.16.0.0,就会返回10049错误码了。
情况三:
- 为物理网卡添加172.16.0.3/16,ping 172.16.0.0,10049错误,然后删除该ip地址;
- 启动OpenVPN客户端,以172.16.0.0为DHCP服务器地址,然后首先可以分配一个172.16.0.2给虚拟网卡(一段时间后由于续约会断开,和情况一的不同仅仅是ip地址的不同,在没有故障前ping 172.16.0.0就会返回10049)。
- 停止OpenVPN客户端,再次添加172.16.0.3/16,再ping 17.16.0.0,仍然10049。
情况四:
和情况三的不同仅仅是将公司的电脑换成了家里的电脑,同样是盗版windows XP SP2,ping网络ip地址172.16.0.0的时候竟然不再10049了,而是超时了,而且OpenVPN客户端使用172.16.0.0这个DHCP服务器地址也可以正常续约。
分析二:微软没有公开它的winsock实现的细节,因此就很难知道其对ip地址是怎样进行过滤的,在家里的电脑上启动OpenVPN之后访问172.16.0.0就不会10049,在公司就10049,也并不是微软保留了私有网段,在公司17.16.0.0不会10049,而17.110.0.0就10049,且17.161.0.0也会10049,没有任何规律可言,因此就没有必要浪费时间去继续这个问题了。
分析总结
由于OepnVPN服务端没有推送windows客户端配置ip的方式,且客户端也没有显式配置配置ip的方式,因此使用dynamic的方式也就是DHCP的方式获取ip,而tap-win32驱动程序内部实现了一个DHCP服务器,其地址需要OpenVPN显式设置,默认情况下,OpenVPN客户端将设置0.0的地址。 当虚拟网卡配置成自动获取ip的时候,windows的DHCP client服务将作为DHCP客户端为虚拟网卡获取ip地址,然后配置ip地址,DHCP协议的discover/offer/request/ack都没有问题,一切正常,discover将得到tap-win32驱动内部DHCP服务器的响应,发送offer,…然而待到租约时间的一半或者由于某种原因(比如手动续约,或者休眠/唤醒)需要续约的时候,某些以0.0为目的ip地址的对DHCP服务器的访问将会返回10049错误,经测试,这里的“某些”在不同的windows版本上是不一样的(在我的开发机上,172.16.0.0/172.161.0.0/17.110.0.0…不可用,而172.17.0.0/17.16.0.0是可用的,在家里的机器上测试结果又不一样…),如此一来windows的DHCP client服务将始终无法联系到当初租给它ip地址的地址以0.0结尾的服务器,那么当租约时间过了x(根据DHCP实现和现场配置确定)时,windows的DHCP client服务将自动丢失虚拟网卡的ip的ip地址,然后重新发起discover的过程,而tap-win32的驱动只给3次DHCP request包中client地址和OpenVPN配置的地址不相等的机会,也就是说discover的次数是受到限制的,次数到期之后,根据tap-win32的实现逻辑,虚拟网卡将再也无法从DHCP服务器0.0得到ip地址了,同时在windows系统日志中报出错误,此时只能重启OpenVPN客户端了
故障现象二
故障重现
在内网某些机器连接有问题,其它机器就没有问题,比如通过无线连接就没有问题。
故障分析
- 发现ping不通OpenVPN服务器的虚拟网卡ip首先查看其物理网卡能否ping 通,发现此时192.168.81.28都不通了,断开OpenVPN客户端就可以ping通;
- 查看OpenVPN服务器的日志以及OpenVPN客户端日志,发现服务器还在不断发送OpenVPN-PING给客户端,等待超过了keepalive设置的时间,便发送一个客户端重启信号,于是客户端重新连接;
- 启动OpenVPN客户端后通过route print查看客户端所在机器的路由表,发现有两条内网192.168.1.0/24网段的路由,一条是以物理网卡为出口,另一条以虚拟网卡为出口,而要想访问81.28则必须通过1.254,访问1.254必须以物理网卡为出口;
- 查看OpenVPN服务器的配置,发现其将40网段的路由给推送了下来,因此192.168.1.0/24网段的OpenVPN客户端在连接上OpenVPN服务器之后就无法和192.168.1.0/24网段以外的主机通信了。
分析总结
- 之所以这个问题很隐蔽,原因有两点,第一,OpenVPN服务器和OpenVPN客户端通信使用UDP协议,UDP不需要确认,因此从81.28发来的OpenVPN服务器的数据包都能被40网段的OpenVPN客户端接收,而反过来就不行了。如果使用tcp协议的话,则很快就会断开;第二,在测试的时候,只是将客户端挂在那里,没有任何数据在上面传输,如果传输数据,则马上就会发现数据传不过去;
- OpenVPN服务器推送下来的路由一定不能和OpenVPN客户端所在主机的路由相冲突,因此务必在分析日志时注意以下的警告信息: WARNING: potential route subnet conflict between local LAN [192.168.1.0/255.255.255.0] and remote VPN [192.168.1.0/255.255.255.0]
问题解决
路由问题的解决
一定确保没有路由表项冲突,除了通过路由表之外,还要查看路由缓存,Linux上的路由缓存可以通过route –C查看,windows未知。
OpenVPN的配置参数
OpenVPN中针对windows客户端有一个参数:ip-win32。一直以来这个参数都被忽略了,实际上就是因为这个参数被忽略引起了花了很久才解决的问题。我们一般并不配置这个参数,因此OpenVPN将使用adaptive自适应模式,首先将采用dynamic进行尝试,而adaptive模式中的dynamic不能跟任何参数,而实际上dynamic模式有两个参数可以设置:
dynamic [offset] [lease-time]
复制代码
其中第二个参数lease-time就是租约时间,默认是一年,第一个参数offset影响了DHCP服务器的ip地址,在OpenVPN的server模式下,假设地址池是x.y.0.0/16,那么客户端DHCP服务器的ip为: ip=(x.y.0.0&&offset==0)||(x.y.0.offset&&offset>0)||(x.y.255.255+offset&&offset<0)
dynamic方式解决
可见如果offset取默认值0,那么客户端的DHCP服务器的ip地址将是一个网络地址,OpenVPN并不假设windows是不可访问网络地址,同时也不假设winsock的任何*SP过滤行为,因此将0.0地址作为DHCP服务器地址对于OpenVPN来讲是合理的,但是在windows机器上可能就会出现上述奇怪的问题。 如果offset不为0,还要注意一种情况,那就是不能将DHCP服务器的ip地址和分配给OpenVPN客户端虚拟网卡的ip地址重复,比如offset为2,第一个连接的客户端将得到x.y.0.2这个地址,而DHCP服务器也将得到这个地址,这在初始化网卡ip地址的时候就会出现冲突,因此要注意这一点。建议offset使用-1,这样DHCP服务器ip地址将会是x.y.255.254,正常情况下很难使用到这个ip,除非特意分配。
非dynamic方式解决
使用netsh或者ipapi或者manual方式配置ip地址。netsh和ipapi的方式和DHCP client服务可能会冲突,比如虚拟网卡的状态会一直在“正在获取ip地址”然而实际上ip地址已经得到并设置上了,由于和windows的DHCP client服务相关,目前并没有对这种状态进行测试。netsh/ipapi和DHCP的奇怪现象和windows的版本也有关系。最干净的方式就是manual方式设置ip地址。然而这需要用户根据OpenVPN客户端的输出信息手工配置ip地址,因此并不推荐使用。
修改tap-win32驱动方式解决
修改ProcessDHCP函数逻辑并将DHCP的限制次数增加到一个很大的值: #define BAD_DHCPREQUEST_NAK_THRESHOLD 0Xffffffff 但是采用这种方式虽然解决了DHCP续约问题可能会带来新的问题,而且在内核空间,搞不好就蓝屏了,因此不推荐使用。
注意事项
注意在不修改tap-win32驱动的情况下,即使dynamic加上了offset参数,也不能使用ipconfig /release和ipconfig /renew来操作虚拟网卡,因为每次这样的操作都会发起一次release/discover/offer/request/*ck的过程,该过程中的request阶段的client IP为0,和tap-win32的DHCP服务器ip不相同,而tap-win32只给3次这样的机会,3次过后就会nak。但是却可以网卡状态中的“修复”按钮来续订ip地址,因为这并不会发起一次discover的序列。
结论
针对OpenVPN客户端断开的问题,确保路由没有问题,在测试过程中最好实际跑一下数据传输,而不仅仅将OpenVPN客户端挂在那里,针对DHCP的问题,推荐采用dynamic带offset参数的方式解决。
OpenVPN以及虚拟网卡相关问题
OpenVPN客户端和服务器的keepalive
OpenVPN的客户端和服务器之间通过keepalive保活,这个keepalive就是一个PING包(不是真正的icmp echo request),这个PING包通过物理链路发送和接收,而和虚拟网卡没有关系,因此OpenVPN的keepalive事实上只是保证了OpenVPN之间的物理链路的连通,而不能保证虚拟的VPN链路的连通性,比如如果手工将虚拟网卡down掉了(linux:ifconfig tapX down/windows:禁用网卡),OpenVPN进程是不检测的,此时只要PING可以正常发送/接收,则OpenVPN丝毫不知道VPN链路已经出了问题。因此需要在OpenVPN内部或者外部加入对虚拟网卡的状态监控。
内部监控虚拟网卡
内部监控虚拟网卡的方式便于和OpenVPN的keepalive机制(PING)完全同步,然而实时性较差。具体做法是每次或者每几次发送PING之前先检测虚拟网卡状态(网卡是否启用,网卡的IP地址是否被更改),如果发现不对则停止PING的发送,一段时间之后收不到PING之后,OpenVPN服务器端会促使客户端restart,从而重新初始化虚拟网卡,分配ip地址等等。
Linux内部监控
在send PING之前通过ifconfig检查tapX的状态并且获取ip地址,然后和保存的地址做对比,如果不一致则不发PING。在add_option函数中监控ifconfig的push,更新保存的虚拟网卡地址信息,解禁PING的发送。事实上,监控到了服务器端push了ifconfig地址信息,说明一个新的连接刚开始。
Windows内部监控
在send PING之前通过GetAdaptersAddresses之类的IP Helper API函数检查“安全连接”的状态并且获取ip地址,然后和保存的地址做对比,如果不一致则不发PING。在add_option函数中监控ifconfig的push,更新保存的虚拟网卡地址信息,解禁PING的发送。事实上,监控到了服务器端push了ifconfig地址信息,说明一个新的连接刚开始。
外部监控虚拟网卡
我们总是希望任何时刻只要虚拟网卡的状态或者ip地址发生了改变就会通知OpenVPN进程进行restart,这样实时性更高一些,然而OpenVPN内部并没有调用点让我们能插入这样的逻辑,因此必然需要在外部启用一个单独的监控进程或者在OpenVPN进程内部启用一个线程来完成监控,同时需要定义和OpenVPN客户端的通知机制。
Linux外部监控
可以使用iproute2中的ip minitor来完成监控,比较简单。这是因为Linux中是通过netlink来完成通知的,ip monitor在一个它感兴趣的netlink类型上进行poll,只要虚拟网卡的状态/ip改变了,内核通知链机制会发送通知给链上所有的节点,然而节点的回调函数将会发送netlink消息,用户态的ip monitor会得到这个消息。
Windows外部监控
在Windows上没有netlink机制,然而它有两种自身的机制完成网卡状态改变的通知。其中之一就是注册表监控,Windows使用GUID作为设备健值将设备保存在注册表中。HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/Tcpip/Parameters/Interfaces下面保存了所有网卡的GUID,找到我们的虚拟网卡之后,直接调用RegNotifyChangeKeyValue即可,因为只要改变网卡的状态/ip,上述的键值一定被改写,RegNotifyChangeKeyValue则会返回,我们可以将其作为一个中断信号,然后查询网卡当前的状态得到比对信息。第二种方式则是使用NotifyAddrChange之类的函数,和注册表无关,当函数返回的时候,我们主动调用GetAdaptersAddresses之类的API查询比对信息。