本人曾经在Windows以及Mac OS上尝试过不止一次的多虚拟网卡合并,效果均不好,效果不好不是因为没有成功,而是合并成功后就结束了,剩下的什么也做不了,特别是Windows(NDIS本人只知道个名称,BSD的驱动开发也仅仅触及到框架),也许是本人对操作系统有偏见,也许是本人书也不精,不管怎样本文只针对Linux。
使用局限
注意,之所以说使用局限是因为这并不是TUN/TAP本身的局限,而是使用它们的应用程序的局限。不管是OpenVPN,还是OVS,或者其它的什么,使用虚拟网卡的方式都是有那么一点美中不足,既然如此,也就只能如此了...
内部解决
很早之前写过一篇文章《
OpenVPN性能-多OpenVPN共享一个虚拟网卡》,将虚拟网卡改为了支持多应用程序实例,这样在外部看来,只需要启用一块虚拟网卡服务多个应用程序或者多个线程,而在内部看来,多个应用程序/线程的数据是彼此隔离的(那篇文章只是一个思想索引,并没有实现数据隔离)。
这种内部解决方案解决了网络配置的复杂性,诸如bridge,bonding,多块网卡的IP地址设置,数据包过滤规则设置,IPMARK设置,策略路由设置等,但是也有其自身的局限性,比如不够灵活,也在相当程度上和应用程序发生了关联,毕竟,之所以采取这种方案修改tun.c的代码,是因为我们发现了OpenVPN单进程的局限,启用多实例在网络配置上又太复杂...但是并不能说明所有使用TUN/TAP网卡的应用程序都和OpenVPN一样,也不能确定OpenVPN在接下来的后续高版本中不会从应用程序自身而不是驱动层面改进这个问题,如果万一哪天OpenVPN社区放出了一个新的支持多线程的版本出来,N多个人日的工作量就白费了,当然作为一个通用的方案,这个代码可以直接提交给Linux kernel的社区而不是OpenVPN的社区。
外部解决
在Linux上,其基本哲学就是用外部的方式解决问题,能用shell粘合多个既有模块实现的功能就不写C代码,事实已经或者将要证明,目前Linux上的小程序(盗用了JAVA的概念,实际上JAVA Applet一点都不小!)特别多,你可以用这些小程序玩排列组合,即便有一个小功能发生缺位,暂时没有实现,你也可以动手写一个
该小功能的实现,而不是直接实现一个
大工程。
一般而言,想将多块网卡对外展现为一块逻辑网卡,办法有二,一个是bridge,一个是bonding,这两种方式是最简单且常用的方式。但是二者是有区别的,区别甚大!
0.目标和限制
将多块TAP/TUN虚拟网卡合为一块逻辑网卡之后,目标很明确,就是为了提供性能且降低复杂性。这两点很重要,为了突破类似OpenVPN程序的单进程限制,我们启用多个进程的OpenVPN,但是同时也启动了多块虚拟网卡,而这带来了网络配置的复杂性,为了降低网络配置的复杂性,我们把多块虚拟网卡合并为一块逻辑意义上的虚拟网卡,合并之后,真实的虚拟网卡不可见了,现在要做的就一件事就是即便真实的虚拟网卡不再可见,也要
能将欲发往该合并后的逻辑虚拟网卡的数据正确路由到它本应该去的真实虚拟网卡!
1.TAP的bridge方式
TAP模式的网卡是一个模拟以太网的二层网卡,它收发的数据是带有MAC头的以太帧,鉴于此,我们就有一个完全现成且自动的行为可用,从而在bridge后的多个真实TAP网卡中选择应该把数据发给谁。该机制就是以太网的ARP机制以及以太网交换机的自动学习机制。其中ARP机制负责找到需要把数据包发到哪块具体的虚拟网卡,而以太网交换机的学习机制则会记住那块网卡,在MAC/端口以及ARP cache到期之前,从且仅从那块虚拟网卡发送相关数据包。这就是TAP模式网卡的bridge这种完美解决方式。
TAP网卡可以bonding吗?肯定是可以的!但是bonding的话没有bridge方便,不管你采用什么bonding slave调度方案,你都要自行解决寻找正确的具体TAP网卡的问题。
2.TUN的bonding方式
TUN模式的网卡是一个点对点的非广播网卡,没有ARP,收发的数据是纯粹的IP数据报,没有任何链路层信息,因此你只能用IP层的机制来
寻找正确的具体TUN网卡,因此你不能用bridge的方式将多个TUN网卡整合成一个网桥,非广播网桥在此毫无疑义。还有一种整合方案就是bonding!
使用bonding的整合TUN网卡的话,可能会遇到一点障碍,比如硬件地址的问题,比如IP地址配置的问题,比如ARP标志的问题(使用ifconfig bond_tuns -arp去掉ARP标志),诸如此类的问题网上已经有了一大堆的文章,本着浅拷贝的原则,本文就不再赘述。最大的问题是采用什么策略调度bonding slave网卡,也就是说如何寻找正确的TUN网卡发送数据包。显而易见,轮询,active-backup,基于IP的LB等等均不适合,因为所有这些策略都无法和应用程序(比如OpenVPN)通信,剩下的只能用Broadcast这种广播方式了,这肯定能解决问题!然而,最终不该收到数据包的和该TUN网卡对应的字符设备关联的应用程序会收到数据包,然后判断一下丢弃之,由于字符设备操作为系统调用会造成上下文切换,且应用程序判断也会浪费相应的CPU时间,这将不是一个好的方案。
好的方案是直接在内核态即丢弃数据包,常规的做法是使用IPMARK+ip_conntrack这种,为每一个数据包都关联一个MARK,然后每一个MARK关联一块TUN,然而这样做比较复杂,需要为xmit_hash_policy参数增加一种算法,即layer3+ipmark的算法。为此,我们看一下能不能用TUN本身的机制来解决这个问题,如果你看到了tun.c中有run_filter这个函数调用,千万不要高兴太早,但是还是要小兴奋一下,毕竟也算找到了突破口,这说明TUN/TAP网卡自身有过滤机制,不能高兴太早是因为这个filter是用来过滤TAP模式网卡的MAC地址的,我们现在需要的是过滤通过TUN模式网卡的IP地址,需求明确后就好做了,仿照现有的针对MAC地址的filter做一个针对TUN网卡的IP地址的filter,而这个并不难!
新增了IP地址的filter之后,我们来看看接口问题,目前的对过滤条件的设置仅仅可以通过ioctl系统调用进行,而这个调用需要一个文件描述符参数,因此该调用对shell脚本是非常不友好的,你要么在使用TUN虚拟网卡的应用程序进程空间内调用,要么传递文件描述符,对于shell脚本而言,这非常不便,而我们知道,设置Linux网络策略,不管是iptables,route还是rule,都是方便的命令行操作,因此我们需要再开发一个procfs接口,用IP地址字符串来write/read,命令行格式类似:
增加过滤地址:echo +1.1.1.1 >/proc/tuntap/tun/tun0/filter;
删除过滤地址:echo -1.1.1.1 >/proc/tuntap/tun/tun0/filter;
查看过滤地址:cat /proc/tuntap/tun/tun0/filter
procfs相关文件结构如下:
proc
`-- tuntap
|-- tap
| `-- tap3
| |-- filter
| `-- info
`-- tun
|-- tun0
| |-- filter
| `-- info
`-- tun1
|-- filter
`-- info
这个开发也不难,中间涉及到很多的小技巧,包括如何组织procfs文件的属性结构,如何实现展示过滤IP地址(搞了这么多年kernel,还第一次看到可以printk("%pI4 ", addr)这样把32位IP地址转换为点分十进制的...)等。
有了TUN模式的IP地址过滤以及procfs接口,事情变得好起来,对于OpenVPN来讲,完全可以起N个进程,侦听连续的不同端口,然后用iptables DNAT规测将请求random到这些端口中的一个,把所有的N个TUN网卡做一个bonding。每一个OpenVPN实例挂接一个client-connect和一个client-disconnect脚本,在客户连上的时候,将和该客户端相关的IP地址(包括为其分配的虚拟IP地址以及和它相关的物理IP地址)设置到对应dev环境变量的filter这个procfs文件中,客户端断开的时候再删除它们...
针对的一个问题
TAP网卡的OpenVPN不是挺好吗?为何要折腾TUN模式的?这TMD的是因为要服务Android系统的OpenVPN客户端,Android为何不支持TAP模式我到现在都不知道,我需要知道的是具体原因,而不是一句sorry!这才符合开源精神!针对这个问题,除了谩骂以及鄙视,看不起之外,我还有两个方案,第一个就是上述写下来的如何针对TUN网卡做bonding,第二就是服务端使用TAP模式的网卡,修改Android客户端的OpenVPN代码做修改,加一个适配层,在OpenVPN代码中实现TUN到TAP的适配,这意味着你需要做两件事,第一件事就是实现MAC层的封装/解封装逻辑,第二就是实现ARP逻辑。
TUN/TAP适配层-单独的程序
如何适配呢?直接修改OpenVPN代码?场面太大,太惨烈了,我不得不找出需要添加上述两个逻辑的位置,然后就是没完没了的debug。遵循UNIX/Linux的方法是,单独实现一个程序,实现MAC的封装/解封装逻辑,实现ARP逻辑以及MAC地址的管理,然后用AF_UNIX域的socket发出去来一个IPC,另一端是谁?是OpenVPN!因此需要做的仅仅就是将OpenVPN从字符设备收发数据包改为从AF_UNIX域socket收发数据包而已,你可以全心全意实现你的小程序了,姑且叫做A吧,在A中,很简单,读取过程如下:
1.从对应的字符设备读取TUN网卡发出的IP数据报;
2.找出其对应的MAC地址(涉及MAC地址管理),如果没有对端的MAC地址,则用UNIX socket发送一个ARP广播;
3.封装MAC地址;
4.用UNIX socket将封装好的数据帧发送出去。
发送的过程反过来即可。对于ARP,可以用一个单独的线程实现,有两件事要做,第一是发送ARP广播解析对端IP地址,第二是回应针对自己IP地址的ARP回应,这也不难。
本文最后之所以搞出这个一段,就是为了展示UNIX/Linux的哲学,最好不要将一个程序越扩展越大,而是应该适时应用IPC办法,用丰富的IPC方法将事情交给其它的
小程序去做!
人的一生,有多少时间在排队中度过,浪费了多少生命元素,办证,旅游,吃饭,三座大山,让我情何以堪!