by fanxiushu 2021-07-07  转载或引用请注明原始作者。
今年早些时候,实现了xFsRedir的虚拟局域网功能,
包括普通的创建独立的虚拟节点组建的虚拟局域网,也包括跟同网段的真实局域网混合到一起的混合网络。
前段时间实现的 NAT路由功能,以及根据各种过滤条件进行网络重定向(网络代理)的功能。
(有兴趣可以去 https://github.com/fanxiushu/xFsRedir 下载最新版本的程序把玩把玩。)

还将会包括最近正在升级的能把不同地区,不同网段的局域网组合起来的功能,
这么说可能不大能理解,举个例子应该就好理解了。
比如一个公司,在北京,上海,深圳,广州,都成立了分公司。
公司要求把四个分公司的内网都连接起来,让员工们能无阻碍的访问各个分公司的内部资源。
因为分公司在不同地区,直接使用路由器连接起来是不现实的。
所以要么找ISP运营商拉专线,这个价格肯定昂贵,当然有资本的公司可以这么干。
当然像银行啊,政府部门等这些对安全要求非常高的部门,使用专线是必须的,这样能达到物理隔离的效果。
大部分普通公司无此必要,可以借用Internet公网建立隧道,使得多个不同地区的局域网互访。
这就是xFsRedir目前正在增加的功能。

而且xFsRedir有个特别之处,
内核驱动层并不是采用windows平台下的虚拟网卡实现的,而是清一色使用WFP驱动实现。

实现虚拟局域网通常的思路也是虚拟网卡,所以这里的macOS和linux还是以虚拟网卡为主。
(如果对xFsRedir没啥兴趣,也可以阅览此文,因为下面的描述与xFsRedir没啥关系,都是对应linux和macos平台下的内容。)

linux和macOS下的虚拟网卡实在太简单了,因为都是现成的,我们只需要使用它提供的接口就成。
所以本来也没啥好讲述的,只是为了普及一下知识,以及能让xFsRedir尽量做到跨平台
(目前核心部分目录重定向还无法做到跨平台,至少macOS我还做不到),

先来讲述linux下的虚拟网卡驱动。
领教了windows平台下NDIS驱动,以及虚拟网卡驱动开发的复杂性,
再来看看linux平台下的这些东西,简直是小儿科一样的东西。

linux内核集成了虚拟网卡功能,而且是非常早期的版本就有,我使用的最早的内核版本是2.6。
这个版本就已经集成了虚拟网卡。
我们只需要调用open函数打开 /dev/net/tun 的设备就可以了,
tun设备提供了两种工作模式:
一种是 IFF_TUN,就是只截获处理IP数据包,
一种是IFF_TAP,这个是截获处理以太网卡数据包,基于链路层。也就是实现整个以太网卡的完整功能。
通常的应用 使用IP数据包就可以了,而且还能达到通用的目的,
比如PPPoE拨号协议,是在链路层增加了另外一个协议来处理,
如果我们上升到IP层,就只关心PPPoE拨号的IP数据包处理,而不必关心底层如何运作,
因为绝大部分系统都会虚拟出一个拨号的虚拟网卡来为上层提供IP数据包的处理,也就是上面说的IFF_TUN模式。
也有其他情况,比如工业控制中的PLC编程器,有些通讯是直接链路层传输,所以这种情况下,我们就只能使用IFF_TAP模式。

以下是一个简单的创建虚拟网卡的例子(创建IFF_TUN模式的虚拟网卡):
    static int create_tun(const char* name)
    {
        int tunfd = open("/dev/net/tun", O_RDWR);
        if (tunfd < 0) {
            printf("open tun error=%d\n", errno);
            return -1;
         }

          struct ifreq ifr;
          memset(&ifr, 0, sizeof(ifr));
          ifr.ifr_flags = IFF_TUN | IFF_NO_PI; ///
          strcpy(ifr.ifr_name, name);

         if (ioctl(tunfd, TUNSETIFF, &ifr) < 0) {
             printf(" TUNSETIFF Error=%d.\n", errno);
             close(tunfd);
             return -1;
         }
         ///是否让这个虚拟网卡持久保存,默认情况下,随着进程退出,或者tunfd句柄的关闭,虚拟网卡自动会被删除。
//       ioctl(tunfd, TUNSETPERSIST, 1 ); // set present..
         
         return tunfd;
     }

再然后,我们使用 create_tun的返回值,也就是创建的tun的文件句柄,
调用read函数,读取IP数据包,然后使用write写入ip数据包,
这样整个虚拟网卡就工作起来了, 我们只需集中精力处理IP数据包的通讯转换等业务逻辑 。
够简单的吧,(简单得都不好意思写这篇文章了,得再加点别的知识才会有吸引力)

即便我们要作死,自己实现linux内核下的虚拟网卡驱动,也不是个难事。
很早前的文章:
https://blog.csdn.net/fanxiushu/article/details/8525749  (linux虚拟网卡驱动代码)
也就几百行的代码而已,如果再压缩一下,估计也就三四百行。
虽然当时是基于kernel 2.6的内核编译的,现在的内核已经到5以上了,可能某些函数名已经改变了,但总体流程也差不多。

以上虚拟网卡模型基本都是在驱动层模拟真实网卡,然后数据包都被发送到应用层来处理。
这种处理方式也没什么问题,而且如果业务逻辑非常复杂的情况下,还会带来很多好处,因为应用层总比驱动层处理起来容易得多。
比如Open-V-P-N软件,很早就出现的软件,都是采用数据包发送到应用层来,然后加密,然后转发。
但是我们也能看到Open-V-P-N的总体数据传输效率并不高。
比如千兆网卡, Open-V-P-N能达到千兆网的 25%的传输率就不错了,也就是在1Gbps带宽下,能达到 20-30Mbps 的速度率。

这也不是Open-V-P-N的错。在CSDN文章上阐述windows虚拟网卡驱动开发的时候,讲述过性能问题。
https://blog.csdn.net/fanxiushu/article/details/69415282  (windows虚拟网卡驱动开发)
如果千兆网卡满载的话,每秒传输1Gbps带宽,以太网卡每个包大小 1500字节,自己有兴趣可以计算一下每秒需要处理多少个数据包。
如果是万兆网卡,百万兆网卡,每秒传输的数据包个数更加恐怖。

数据包达到网卡驱动之后,按照正常逻辑,就会立即发给底层硬件去传输了。
但是虚拟网卡却是到处绕圈子:
先是虚拟网卡把数据包传输到应用层,应用层某个线程接收虚拟网卡传来的数据包,然后加密,封包,
然后再通过应用层的socket套接字建立连接传输,
再然后处理过的数据包才能再次进入驱动层,然后才被硬件传输出去。
这么绕来绕去的,总体性能会得到提升才怪呢!

当然我们可以减少每秒处理的数据包个数来提高性能,具体做法就是增加虚拟网卡的MTU,比如设置MTU  64K,甚至1MB,
但是这个做法与真实硬件无法兼容性,扩展性不好。

我们得从减少虚拟网卡绕圈子的地方去提高整体性能。
因此最好的做法就是全部在驱动层处理各种逻辑,比如加密,隧道封包,再然后直接通过硬件传输出去。
比如IPSec,全部核心逻辑全在内核层处理,因此性能是很高的。
当然因为IPSec的加密算法复杂,加密复杂的同时也会带来更高的安全性,
进行加密的时候占用不少CPU资源,因此会拖慢整体速度,但是也有IPSec硬件加密解决方案。
linux新内核集成了WireGuard,也是全部处理逻辑集中到内核层去处理。
号称代码只有区区4千多行,能集成所有加密,隧道封包功能
(其实linux内核虚拟网卡驱动也就几百行搞定,然后剩下几千行代码处理不复杂的加密,封包等业务逻辑完全绰绰有余)
号称是要取代Open-V-P-N,IPSec等这些,当然吹是这么吹,吹牛谁都会。
因为各有各的好处,比如Open-V-P-N虽然速度不高,但是好在因为数据包都在应用层处理,修改和定制起来都不会太难,
毕竟不是人人都熟练驱动层,而且一不留神容易搞死整个系统,而应用层出问题,顶多程序挂掉。
在对速度需求不是太高的场所还是很适用的,比如游戏加速,估计没有哪个游戏厂商闲的蛋疼允许你无限制的上传下载数据。
而IPSec全部在内核层处理,速度其实跟WireGuard差不多,损失部分顶多就是复杂的加密部分的损失。
但是正是IPSec加密算法复杂,提供的功能更多,适用范围也更加广泛。
WireGuard好在简单,与最新linux内核集成,以后可以直接拿来使用(当然仅限linux平台),
在对速度有要求,需求功能不是太多,而对加密不像IPSec那样高要求的场所适用。

说到WireGuard,倒是想起了有个windows对应版本的 wintun 驱动,
也是为了解决windows平台下内核没有集成虚拟网卡,想要模拟出linux平台下的tun效果。
但是windows毕竟是windows,驱动总是会比别的系统更加复杂。
只使用 wintun.dll 动态库提供的API接口,就能顺利的创建虚拟网卡,读写IP数据包或者网卡数据包。
搞的来跟linux下的 /dev/net/tun 设备一样的简单好用。
其实是做了个封装而已,wintun.dll把sys,inf,cat等安装驱动必须的文件全部打包进wintun.dll动态库中
然后wintun.dll提供API接口自动解出sys等系统文件到某个临时目录下,然后进行安装。
驱动也去申请了WHQL认证,所以安装的时候也不会弹窗了。
所以看起来就只提供了API接口就能用的一个 dll 就行了。
工程师们有时为了就带一个独立的exe文件,经常也会这么干,因为省事(但是如果作为一个有规模的项目,并不推荐)。
其实虚拟网卡驱动层,安装层,windows下的该复杂还是得复杂,该怎么开发还是得怎开发,
这和Open-V-P-N提供的现成的虚拟网卡驱动或者我们自己开发的虚拟网卡驱动也无啥区别
(接口我没去留意过,这里指的是总体处理流程),因为也是传输到应用层处理,速度也快不到哪去。
也就是封装了,使用起来更加简洁些而已。这当然给只调用接口的人带来很大方便。

说完了linux,我们再来聊聊macOS系统下的虚拟网卡驱动,
macOS其实也集成了 虚拟网卡驱动,
macOS集成的虚拟网卡驱动,也是出现的很早,具体是哪个版本出现,我也不清楚。
再说以macOS这种更新即丢弃老旧兼容性的德性,所以我们不用担心还会有人使用老旧的macOS系统。
这与即使过了20年了,WINXP还在使用(估计至少还有上千万人在用)情况不同。

macOS下的虚拟网卡是utunX,其中X是数字序号,
我们可以自己创建utun0,utun1...utunX等多个虚拟网卡。
这个与linux就只提供一个 /dev/net/tun 设备,然后使用ioctl 的 TUNSETIFF命令创建多个虚拟网卡方式不大相同,
而且某个utunX 设备都会出现在 macOS系统的 /dev设备目录下,
也就是一块虚拟网卡,对应一个 /dev/utunX 驱动,
这个倒是和windows创建虚拟网卡很相似,windows也是一块虚拟网卡,对应一个驱动实例。

macOS下创建虚拟网卡也与linux不同,不像linux那样简单 open 一下,然后 ioctl 一下,就能创建一个虚拟网卡了。
需要首先创建 PF_SYSTEM的socket套接字,
然后使用ioctl的CTLIOCGINFO命令,获取 ctl_info 类型的参数,获取成功之后,
申明 sockaddr_ctl 类型的参数,填写相关参数,大致如下:
    sockaddr_ctl  sc;
    sc.sc_id = ctlInfo.ctl_id;
    sc.sc_len = sizeof(sc);
    sc.sc_family = AF_SYSTEM;
    sc.ss_sysaddr = AF_SYS_CONTROL;
    sc.sc_unit = utunnum + 1;
其中utunnum 就是创建的虚拟网卡序号,从 0 开始,因为之前可能已经创建了,
所以采用untunnum递增的办法创建,直到找到一个空闲的为止。

然后调用 connect函数 连接这个 sc地址,如果连接成功,
系统中就会出现 utunX(X是序号,对应utunnum值)这么个虚拟网卡。
大致伪代码如下:
     int utun_fd = -1 ;    //这个就是需要创建的fd句柄
     for( utunnum=0; utunnum  < 255;++utunnum){
           struct ctl_info ctlInfo;
           memset(&ctlInfo,0,sizeof(ctlInfo));
           strcpy(ctlInfo.ctl_name, UTUN_CONTROL_NAME);
           int fd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL);
           if(fd<0)return -1;
           if (ioctl(fd, CTLIOCGINFO, &ctlInfo) == -1) return -1;
           
           struct sockaddr_ctl sc;
           sc.sc_id = ctlInfo.ctl_id;
           sc.sc_len = sizeof(sc);
           sc.sc_family = AF_SYSTEM;
           sc.ss_sysaddr = AF_SYS_CONTROL;
           sc.sc_unit = utunnum + 1;
           if (connect(fd, (struct sockaddr *)&sc, sizeof(sc)) < 0){//可能已经被占用了
                  close(fd);
                  continue;
           }
           创建成功,
           utun_fd = fd;
           break;
     }

对这个utun虚拟网卡的IP数据包的读写,也不是简单read读取的就是IP数据包,而是加了个协议头,
同样的,写IP数据包也必须加个协议头:
如下所示:

      int utun_read(int fd, char* buf, int len)
     {
          u_int32_t type;
          struct iovec iv[2];

           iv[0].iov_base = &type;
           iv[0].iov_len = sizeof(type);
           iv[1].iov_base = buf;
           iv[1].iov_len = len;

           int r = readv(fd, iv, 2);

           if (r < 0)return r;
           if (r <= sizeof(type))return 0;
           return r - sizeof(type);
      }

      int utun_write(int fd, char* buf, int len)
      {
         u_int32_t type = htonl(AF_INET); // IPV4

          struct iovec iv[2];

          iv[0].iov_base = &type;
          iv[0].iov_len = sizeof(type);
          iv[1].iov_base = buf;
          iv[1].iov_len = len;

          int r = writev(fd, iv, 2);

          if (r < 0)return r;
          if (r <= sizeof(type))return 0;

          return r - sizeof(type);
       }

同样的,utun也是一个把数据包转发到应用层来处理的模型,也会面临着性能等问题,
但是对付大部分应用已经足够了。
因为我对macOS内核的了解情况不如linux和windows,所以对macOS系统下的虚拟网卡驱动也不能提供更多的信息。

有了linux的tun虚拟网卡和macOS下的utun虚拟网卡,
我们就可以做出xFsRedir提供的创建虚拟局域节点那样功能的虚拟局域网了,
但是这样只是局限于各个创建的虚拟节点之间通讯,还无法与真实局域网联系起来,
也就是xFsRedir提供的桥接到真实网络的功能还无法实现。
要实现这个功能,我们必须抓取真实网卡的链路层数据包然后再做处理。
linux下我们可以使用PF_PACKET套接字抓取网卡链路层数据包,这个记得以前的文章有描述过,其实这个也简单。
所以linux主要还是业务逻辑的处理。
至于macOS系统,因为跟BSD同宗,可以使用bpf设备来抓取链路层数据包。
至于如何让虚拟网桥接到真实局域网,我在CSDN上的很早文章都有描述过,有兴趣可以去查阅。

 

Logo

更多推荐