网络协议栈(7)tun/tap设备

一、网络设备
一般一个系统中如果可以连接外网的话,会有一个物理设备,也就是我们通常意义上所说的网卡。但是除了物理上的网卡,系统中还存在这个其他类型的网络设备,这些设备在网络中有着不同的应用场景。例如最为常见的loopback网卡,还有一些不那么常见的网络设备,例如tun/tap网络设备,bridge网络设备,它们都可以不对应具体的物理网络设备,但是可以在系统中存在,并且可以为套接口所感觉到。由于一个网络地址一般都是要依赖于一个网络设备才有意义,所以不同的网络设备就可以虚拟出不同的网络功能,例如最近在看的虚拟机和VPN技术,它们都是用了虚拟网卡,而且也都可能(可以)用到bridge(网桥)虚拟设备。
一个系统中具体有多少个网络设备可以从proc文件中读取。例如,我的虚拟机中所有的网络设备的列表为
[tsecer@Harry ~]$ cat /proc/net/dev
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo: 5500365   65797    0    0    0     0          0         0  5500365   65797    0    0    0     0       0          0
  eth0:  971038    6026    0    0    0     0          0         0   989858   10854    0    0    0     0       0          0
  tap0:  271253     402    0    0    0     0          0         0   110484     937    0    2    0     0       0          0
   br0:  272684    1550    0    0    0     0          0       516    66003     457    0    0    0     0       0          0
除了第二个eth0可以认为是物理网卡之外,其它的都是虚拟出来的网络设备。
二、设备注册
所有的网络设备可以通过register_netdev接口来向系统注册自己,从而可以在设备中看到该设备。在一个网络设备中,最为重要的就是hard_start_xmit接口,该接口负责进行数据的发送,这里要注意的是,设备也只是负责发送,但是此时输入的struct sk_buff *skb数据必须是已经包含了整个以太网所有数据,包括以太网帧的源MAC地址和目的MAC地址。这一点比较重要,这意味着从同一个网口中发送出去的以太网数据帧可能有不同的源地址。也就是,网卡设备发送数据的时候,不会对帧结构自动添加数据,它只是作为一个纯粹的信号发送设备。作为对比,当网卡在接收数据的时候,默认会对以太网帧数据的目的地址进行检测,只接收多播、广播,及目的MAC地址为自己地址的报文,如果希望接收所有到达网卡的以太帧,则需要将网卡设置为PROMISC。
对于网络设备还有我们比较感兴趣的rebuild_header、hard_header_cache之类的接口,它们用来给设备填充地址信息。因为网络设备虽然知道自己的格式,但是它可以发送给任何的网络设备,所有需要发送者提供发送的目的地址,从而可以让网络设备向该目的投递报文。
在\linux-2.6.21\net\core\dev.c中的两个局部变量定界了所有的网络设备,一个是起始地址,一个是结束地址指针。
struct net_device *dev_base;
static struct net_device **dev_tail = &dev_base;
当显示/dev/net/dev的时候,所有的网络设备即从此处遍历。
三、tun/tap创建
tun设备主要是工作在IP层的一个设备,这也就是说它不需要有MAC地址,或者说给它交互的时候用户不用关系MAC地址的处理,而tap设备和tun设备使用相同的设备号,但是它可以看到MAC地址。也就是它们本质上是相同的东西,只是tap工作在更低的MAC地址层。
这个种设备一方面作为网络设备,可以通过socket对它进行数据的发送和接收,例如socket的bind、listen、ioctl之类都可以使用这些设备,也就是说对系统API用户来说它们和物理网卡相同。另一方面,它又是一个字符设备,也就是它可以像串口设备一样通过open、read、write从里面直接发送和接受数据。但是这里的read/write和套接口的read/write不同,socket的read/write读取的是纯粹的报文数据,而对各种底层协议,例如IP/tcp/udp/MAC等概念透明,但是通过对这个字符设备的read/write,可以读取到网络协议中各个层的数据,包括源MAC和目的MAC地址(如果是tap设备的话)。事实上,这个设备的编程模式和伪终端的模式非常相似,其背后的思想也相似。
首先这个设备对应的是一个主设备号为10(作为一种MISC设备类),次设备号为200的字符设备,所以可以首先通过mknod tun c 10 200 来创建这个设备,之后open这个设备。open后通过ioctl来让内核创建一个tun设备。
那么这个open能够每次实现一个新的tun创建而不重复的本质原因在哪里呢?这里就是open系统调用执行的时候,VFS会为这次open分配一个独立的内核态file结构,也就是说,每次打开执行时,内核为此次打开分配的file结构实例不同,而根据这个实例的不同,就可以构造出更多的不同。file结构中也有一个对VFS透明而供各个文件自行解释的private_data指针,所以基于这种file实例的不同,就可以让每个文件对应一个不同的tun设备。
创建的触发是通过ioctl TUNSETIFF命令来实现的,当执行完open之后,事实上tun设备还没有创建,而只有等到执行这个ioctl的时候才会创建真正的网络设备,其具体的流程为:
tun_chr_ioctl-->>tun_set_iff
static int tun_set_iff(struct file *file, struct ifreq *ifr)
{
    struct tun_struct *tun;
    struct net_device *dev;
    int err;

    tun = tun_get_by_name(ifr->ifr_name);这里的ifr是用户传入的信息,我想一般为空吧。
    if (tun) {
        if (tun->attached)
            return -EBUSY;

        /* Check permissions */
        if (tun->owner != -1 &&
            current->euid != tun->owner && !capable(CAP_NET_ADMIN))
            return -EPERM;
    }
    else if (__dev_get_by_name(ifr->ifr_name))
        return -EINVAL;
    else {
        char *name;
        unsigned long flags = 0;

        err = -EINVAL;

        if (!capable(CAP_NET_ADMIN))
            return -EPERM;

        /* Set dev type */
        if (ifr->ifr_flags & IFF_TUN) {这里传入的参数就决定了此次创建的是一个tun设备还是一个tap设备
            /* TUN device */
            flags |= TUN_TUN_DEV;
            name = "tun%d";
        } else if (ifr->ifr_flags & IFF_TAP) {
            /* TAP device */
            flags |= TUN_TAP_DEV;
            name = "tap%d";
        } else
            goto failed;

        if (*ifr->ifr_name)
            name = ifr->ifr_name;

        dev = alloc_netdev(sizeof(struct tun_struct), name,
                   tun_setup);这里开始分配网络设备。设备创建之后通过tun_setup函数来初始化该设备,该函数会设置我们比较关系的数据发送接口dev->hard_start_xmit 为un_net_xmit;
        if (!dev)
            return -ENOMEM;

        tun = netdev_priv(dev);
        tun->dev = dev;
        tun->flags = flags;
        /* Be promiscuous by default to maintain previous behaviour. */
        tun->if_flags = IFF_PROMISC;
        /* Generate random Ethernet address. */
        *(u16 *)tun->dev_addr = htons(0x00FF);
        get_random_bytes(tun->dev_addr + sizeof(u16), 4);
        memset(tun->chr_filter, 0, sizeof tun->chr_filter);

        tun_net_init(dev);

        if (strchr(dev->name, '%')) {
            err = dev_alloc_name(dev, dev->name);
            if (err < 0)
                goto err_free_dev;
        }

        err = register_netdevice(tun->dev);这里向系统注册该网络设备。
        if (err < 0)
            goto err_free_dev;

        list_add(&tun->list, &tun_dev_list);
    }

    DBG(KERN_INFO "%s: tun_set_iff\n", tun->dev->name);

    if (ifr->ifr_flags & IFF_NO_PI)
        tun->flags |= TUN_NO_PI;

    if (ifr->ifr_flags & IFF_ONE_QUEUE)
        tun->flags |= TUN_ONE_QUEUE;

    file->private_data = tun;
    tun->attached = 1;

    strcpy(ifr->ifr_name, tun->dev->name);
    return 0;

 err_free_dev:
    free_netdev(dev);
 failed:
    return err;
}
四、用户态read/write数据是否有MAC地址的内核实现
在上面执行的tun_net_init函数中,其中对于tun设备做了特殊处理。因为tun设备没有MAC地址,在IP路由完成之后,如果说系统选择这个设备来发送数据,要通过ARP协议来确定目的和本机的MAC地址。那么此时ARP协议将如何确定该tun设备的MAC地址将会成为一个问题?因为显然tun设备是没有mac地址。因为ARP是介于IP层和物理层之间的一层协议,所以设备本身是无法控制这个协议的。在arp_constructor函数中,
    if (dev->hard_header == NULL) {
        neigh->nud_state = NUD_NOARP;
        neigh->ops = &arp_direct_ops;
        neigh->output = neigh->ops->queue_xmit;
由于tun设备的tun_setup初始化函数中没有为这个设备填充hard_header函数指针,所以这里将会给neigh->ops初始化为arp_direct_ops,这个变量的定义为
static struct neigh_ops arp_direct_ops = {
    .family =        AF_INET,
    .output =        dev_queue_xmit,
    .connected_output =    dev_queue_xmit,
    .hh_output =        dev_queue_xmit,
    .queue_xmit =        dev_queue_xmit,
};
这里的方法简单而粗暴,就是将各种情况下的发送接口都初始化为直接的挂到网络设备上,
dev_queue_xmit--->>>dev_hard_start_xmit(skb, dev)--->>>dev->hard_start_xmit(skb, dev)
从而完成了向设备的直接发送。
对于一个skbuff结构的发送包,在执行ip_queue_xmit的时候,还没有为以太网数据头留出地址。例如,对于通常的物理网卡设备,这个skbuff结构中的以太网数据头空间的申请和填充是在neigh_resolve_output-->>dev->hard_headereth_header函数中完成的,其代码就在该函数的开始:
struct ethhdr *eth = (struct ethhdr *)skb_push(skb, ETH_HLEN);
这里的skb_push就是从skbuff的数据区中申请大小为ETH_HLEN长度的数据,之后的代码将会填充这个报文的起始和目的MAC地址。反过来说,由于tun跳过了这个构建header的过程,所以用户态通过read读取数据的时候也就没有以太网数据头的内容

而对于tap设备,在tun_set_iff--->>tun_net_init中,对于tap设备则真正的当做一个网络设备来初始化
    case TUN_TAP_DEV:
        /* Ethernet TAP Device */
        dev->set_multicast_list = tun_net_mclist;

        ether_setup(dev);这里主动调用了以太网的通用初始化接口,并且通过随机数为这个网卡分配了一个MAC地址。这样就相当于有一个真正的网络设备了。
        random_ether_addr(dev->dev_addr);
五、该设备的数据来源和目的
1、作为字符设备的写入
tun_chr_aio_write--->>tun_get_user--->>netif_rx_ni
也就是当向该设备写入数据的时候,此时相当于网卡接受到了数据,所以此时要经过本机的IP路由,或者说开始自底向上的来流过本机的网络协议栈。此时内核的网络协议栈将会根据该报文的目的地址决定是本地接受这个报文还是将这个报文通过另一个网口转发出去(如果主机打开了ip_forward功能)。这个操作和socket操作的最大不同在于此时的报文的IP层(以及之上的TCP/UDP)报文都已经填充好,而socket写入的数据则无法控制这些信息。
2、作为字符设备的读出
tun_chr_aio_read--->>>skb_dequeue
这个读出操作比较简单,也就是直接的从这个虚拟网卡已经接收的报文中提取一个出来。那么这些报文是从哪里来的呢?这些报文应该是由待发送的数据,也就是本机的路由判断出本机发送的数据需要经过这个网卡流出本机。如果VPN所在的设备是一个路由器或者网关的话,那么这些报文可能是主机接收到的、需要通过该虚拟网卡转发的报文。当然还有本机的应用程序通过socket来发送的数据,然后由本机的IP路由层确定需要由该网卡代劳发送。总之,是本机IP层确定的由该网口发送的以太网报文。
3、作为网络设备的写入
这些就是通常的socket接口操作了,在操作系统的API级别上看没有什么区别。由于tun/tap设备可以设置IP地址,也可以作为路由的发送端口。回想起之前,当给一个网络设备设置IP的时候,系统会自动的添加一个该网段的路由。对于tun/tap的设置同样符合这个条件,当给这个tun/tap设置一个IP之后,它就可以独当一面,它就可以发送所有目的IP和自己同一网段的报文,当然也可以配置作为网关。这样,用户态的APP通过send/wrtie这个socket fd的时候,就可能有数据被路由到该网络设备。当然,主机确定需要该网口转发的其它主机报文也是此处报文的来源。
4、作为网络设备的接收 
这个一般同样是IP路由可以发送,但是这个的处理者一般是各个应用中的socket,包括侦听在该设备IP上的listen socket,以及于该网段上其它socket通讯的socket等。这个一般是底层不太关心或者说可操作性不大的一个方面。
六、应用
内核中对该设备的实现代码非常简单,就在一个不到千行(2.6.21内核)的tun.c文件中,但是它是很多虚拟网络功能的基础。例如在OpenVPN的Linux设备中,这种tun/tap设备就会影响组网的拓扑结构和性能。
在linux下的qemu虚拟机实现中,同样使用了tap网络设备,这种实现方法非常简洁,是tun/tap应用的一个比较好的例子,同时也是理解这种设备的一个不错资料。
vmware在Linux下的实现并没有直接使用tun/tap设备,这一点比较意外,但是它的实现也很巧妙,并且vmware的需要和目的并不是模拟一个单独的网络设备,而是需要模拟一个局域网网络(因此设备要处于同一个冲突域),也就是一个能够有机组合多个网络设备的容器。这个容器需要能够方便的接入新的设备,并且要提供这些设备之间通讯的基础设施。
正如很多事物本身可能没有意义,它的意义只能体现在它如何被应用上。例如,同样是火药,如果只是用来放烟花,那就逊掉了,但是如果用来造枪炮,那就碉堡了。

posted on 2019-03-06 21:00  tsecer  阅读(867)  评论(0编辑  收藏  举报

导航