想玩虚拟网卡一定要玩TUN/TAP(以下简称TAP),想玩TAP一定要知道uIP。uIP是一个用户态实现的一个超级轻量级的麻雀虽小五脏俱全的TCP/IP协议栈,相比lwIP要好用好玩得多,具体怎么个意思,还是请教它的大帅哥作者吧。本文要说的只是它的一个应用,既如何将TUN模式的虚拟网卡适配成TAP模式的虚拟网卡,这个需求确实是需要的。
到底什么时候需要把TUN模式的虚拟网卡装bi成一个TAP模式的虚拟网卡呢?在你想这么做的时候!我个人认为TAP模式的虚拟网卡特别适合做服务端,它可以任意桥接物理网段和虚拟网段,但是有些平台真的就是不支持TAP模式虚拟网卡,比如说Android系统,我一直都想知道为什么,但是得到就是一句sorry!作为一个开源的平台,我逐渐发现它不是那么的Free,任何平台都没有Linux那样任你随便蹂躏。Android限制API的使用,没有root,那么怎么在有限的API接口范围内达到效果呢?如果说Open×××的服务端使用的一直都是TAP模式的虚拟网卡,难道还要专门为Android单独启动一个TUN模式的服务吗?作为一个偏执的人,我绝不这么做!那么我会想方设法把Android的TUN网卡适配到TAP模式。
TUN和TAP的区别是什么?无非就是少了一个以太网链路层,内核驱动层面不给实现,那么我自己在用户态实现行不?大家都知道,从TUN网卡对应的字符设备读取出来的东西以及写入的东西就是一个IP数据报,那么很简单,我只需要实现以下三点即可:
1.将数据从TUN字符设备读出来后封装一个以太头;
2.将数据写入TUN字符设备前去除其以太头。
3.实现一个ARP逻辑,应付ARP请求以及为了封装目标MAC地址而主动发送ARP请求。
很简单是吧。但是要是真的写起代码来,还真的要费些时间和精力。
我早就承认我不是一个真正的程序员,然而我是猎手,我希望用现成的东西帮我完成以上这一切,能重用的东西为何不重用呢,做到这一点需要的是积累,如果你不知道有uIP这么一个东西,那么可能你现在真的需要自己写代码了。上周买了一本书,讲述iOS以及Mac OS X内核编程的,原因是我的苹果电脑已经成了垃圾玩具了,心想能像对待Linux内核那样蹂躏Mac OS内核,可是并不是那么回事,知识和能力需要积累,并不是想当然的,虽然操作系统原理都那回事,然而当你动手的时候,就会发现并非如此!
还好,我自认对TAP虚拟网卡还算比较熟悉,因此我相信自己可以做到这件TUN适配到TAP的事情,由于自己比较懒,所有使用了uIP,如果不用uIP的话,凭着自己对以太帧结构以及ARP协议的理解,也是可以完成代码的,这个代码的关键并不是什么高深的算法或者深刻的理论知识,而是对协议格式的理解以及简单地掌握C语言内存操作的技巧,仅此而已。如果你还对uIP不熟悉,那就就花10分钟时间浏览一下,它是超级简单的,它把TAP网卡当成网线,在用户态实现了一个完整的TCP/IP协议栈。既然uIP在用户态实现了一个完整的协议栈,那么它肯定也实现了ARP逻辑以及以太帧的封装逻辑,而这正是TUN适配到TAP的时候所要用到的。
那么该怎么办?很简单,修改uIP的代码便是,在给出具体的修改之前,首先说一下除了使用了uIP之外,还使用了simpletun,玩TAP虚拟网卡的如果一头扎进Open×××那就错了,应该先玩下simpletun,就一个C文件,直接gcc编译即可。我使用simpletun作为服务端:
simpletun -s -p 12345 -a -i zz0
我之所以使用zz0作为虚拟网卡的名字是因为这样在ifconfig的时候它会在最后,配置它的时候不用再拖动滚动条了。很显然,服务端使用的是TAP模式,那么客户端我准备使用修改后的uIP,由于标准的uIP使用的是TAP,于是我把它修改成TUN模式,只需要将tapdev_init中的IFF_TAP改为IFF_TUN即可。修改了虚拟网卡模式之后,剩下的就是重写读写逻辑了,之所以说是重写而不是修改是因为重写比修改更容易,uIP的好处在于其定义了很多好用的接口而不是它实现的现成逻辑,使用这些接口或者稍加改动的接口来实现一个新的逻辑或许更加容易。
和标准的uIP不同的是,tun2tap版本的uIP并不从虚拟网卡读取以太帧,而是从网络套接字读取以太帧,虚拟网卡只是tun2tap版本uIP的“上层”,这一点是和标准的uIP的unix/main.c逻辑完全相反的。以下就是新版的main逻辑代码:
int main(int argc, char **argv) { ....... // 设置本端的IP地址以及MAC,正常逻辑应该在获取虚拟IP地址以后再设置 uip_ipaddr(ipaddr, 1,2,3,5); uip_sethostaddr(ipaddr); uip_ipaddr(ipaddr, 1,2,3,4); uip_setdraddr(ipaddr); uip_ipaddr(ipaddr, 255,255,255,0); uip_setnetmask(ipaddr); ... // 设置MAC地址 // 初始化和simpletun连接的套接字 net_init(); while(1) { char raw_tun[UIP_BUFSIZE]; memset(uip_buf, 0, sizeof(uip_buf)); // 从套接字读取数据,要么为ARP,要么为IP uip_len = net_read(); if(uip_len > 0) { // 如果是IP的话,摘掉以太头送往TUN网卡 if(BUF->type == htons(UIP_ETHTYPE_IP)) { char *buf2tun = BUF; buf2tun += sizeof(struct uip_eth_hdr); // 送往TUN虚拟网卡 tapdev_send(buf2tun, uip_len-sizeof(struct uip_eth_hdr)); // 如果是ARP的话,回复ARP请求或者... } else if(BUF->type == htons(UIP_ETHTYPE_ARP)) { // 处理ARP uip_arp_arpin(); if(uip_len > 0) { // 回复ARP net_send(); } } } else if(timer_expired(&periodic_timer)) { timer_reset(&periodic_timer); for(i = 0; i < UIP_CONNS; i++) { uip_periodic(i); if(uip_len > 0) { uip_arp_out(); net_send(); } } if(timer_expired(&arp_timer)) { timer_reset(&arp_timer); uip_arp_timer(); } } // 继续处理TAP网卡读出的数据,封装上以太帧头 memset(uip_buf, 0, sizeof(uip_buf)); uip_len = tapdev_read(raw_tun, UIP_BUFSIZE); if(uip_len > 0) { memcpy(uip_buf+sizeof(struct uip_eth_hdr), raw_tun, uip_len); // 要么请求ARP,要么直接发送数据 uip_arp_out(); net_send(); } } return 0; }
以上代码基本就完成了TUN到TAP的适配,如此一来,Open×××的服务端就可以一直使用TAP模式,即便某些平台的Open×××必须使用TUN模式,也可以采用以上的方式实现一个用户态的以太层,在用户态封装/解封装以太帧。这样可以大大降低服务端的复杂性。
起初,我希望能直接在网上找到TUN适配到TAP的现成代码,于是我搜索“TUN TAP 转化”,“TUN TAP 适配”均无结果,换英文搜还是未果,于是按照老外的常用命名逻辑搜索“tun2tap”,“tap2tun”也依然没找到什么,反而获取了几个reset...因此我写下此文,还是老想法,希望如果有人想做类似事情的时候,能帮上点什么,起码不像我一样一无所获,互联网上应该应有尽有才对。