Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3433066
  • 博文数量: 198
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 7246
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-23 18:56
个人简介

将晦涩难懂的技术讲的通俗易懂

文章分类

全部博文(198)

文章存档

2023年(9)

2022年(4)

2021年(12)

2020年(8)

2019年(18)

2018年(19)

2017年(9)

2016年(26)

2015年(18)

2014年(54)

2013年(20)

分类: LINUX

2021-10-24 18:14:46

linux tun/tap设备的实现(kennel 3.10

——lvyilong316

什么是tun/tap

     TUN/TAP虚拟网络设备为用户空间程序提供了网络数据包的发送和接收能力。他既可以当做点对点设备(TUN),也可以当做以太网设备(TAP)。实际上,不仅Linux支持TUN/TAP虚拟网络设备,其他UNIX也是支持的,他们之间只有少许差别。

     TUN/TAP虚拟网络设备的原理比较简单,他在Linux内核中添加了一个TUN/TAP虚拟网络设备的驱动程序和一个与之相关连的字符设备/dev/net/tun,字符设备tun作为用户空间和内核空间交换数据的接口。当内核将数据包发送到虚拟网络设备时,数据包被保存在设备相关的一个队列中,直到用户空间程序通过打开的字符设备tun的描述符读取时,它才会被拷贝到用户空间的缓冲区中,其效果就相当于,数据包直接发送到了用户空间。通过系统调用write发送数据包时其原理与此类似。

     值得注意的是:一次read系统调用,有且只有一个数据包被传送到用户空间,并且当用户空间的缓冲区比较小时,数据包将被截断,剩余部分将永久地消失,write系统调用与read类似,每次只发送一个数据包。所以在编写此类程序的时候,请用足够大的缓冲区,直接调用系统调用read/write,避免采用C语言的带缓存的IO函数。

      TUN/TAP是一类虚拟网卡的驱动。网卡驱动很好理解,就是netdev+driver,最后将数据包通过这些驱动发送出去,netdev可以参考内核或者OVS代码,基本使用的就是几个钩子函数。

     虚拟网卡就是没有物理设备的网卡,那么他的驱动就是需要开发人员自己编写。一般虚拟网卡用于实现物理网卡不愿意做的事情,例如tunnel封装(用于vpnopenvpn)和Vtun( )),多个物理网卡的聚合等。一般使用虚拟网卡的方式与使用物理网卡一样,在协议栈中通过回调函数call到虚拟网卡的API,经过虚拟网卡处理之后的数据包再由协议栈发送出去。

tun/tap的使用

     linux2.4内核之后代码默认编译tuntap驱动,使用的时候只需要将模块加载即可(modprobe tunmknod /dev/net/tun c 10 200)。运行tuntap设备之后,会在内核空间添加一个杂项设备(miscdevice,类比字符设备、块设备等)/dev/net/tun,实质上是主设备号10的字符设备。从功能上看,tun设备驱动主要应该包括两个部分,一是虚拟网卡驱动,其实就是虚拟网卡中对skb进行封装解封装等操作;二是字符设备驱动,用于内核空间与用户空间的交互。

     源代码在/drivers/net/tun.c中,与其他netdev类似,tun这个netdev也提供openclosereadwriteAPI在分析 TUN/TAP驱动实现前,我们先看下如何使用。使用tun/tap设备的示例程序(摘自openvpn开源项目 tun.c文件)

点击(此处)折叠或打开

  1. int open_tun (const char *dev, char *actual, int size)  
  2. {  
  3.   struct ifreq ifr;  
  4.   int fd;  
  5.   char *device = "/dev/net/tun";  
  6.   if ((fd = open (device, O_RDWR)) < 0) //创建描述符  
  7.     msg (M_ERR, "Cannot open TUN/TAP dev %s", device);  
  8.   memset (&ifr, 0, sizeof (ifr));  
  9.   ifr.ifr_flags = IFF_NO_PI;  
  10.   if (!strncmp (dev, "tun", 3)) {    
  11.       ifr.ifr_flags |= IFF_TUN;  
  12.    }  
  13.   else if (!strncmp (dev, "tap", 3)) {  
  14.       ifr.ifr_flags |= IFF_TAP;  
  15.     }  
  16.   else {  
  17.     msg (M_FATAL, "I don't recognize device %s as a TUN or TAP device",dev);  
  18.     }  
  19.   if (strlen (dev) > 3)      /* unit number specified? */  
  20.     strncpy (ifr.ifr_name, dev, IFNAMSIZ);  
  21.   if (ioctl (fd, TUNSETIFF, (void *) &ifr) < 0) //打开虚拟网卡  
  22.     msg (M_ERR, "Cannot ioctl TUNSETIFF %s", dev);  
  23.   set_nonblock (fd);  
  24.   msg (M_INFO, "TUN/TAP device %s opened", ifr.ifr_name);  
  25.   strncpynt (actual, ifr.ifr_name, size);  
  26.   return fd;  
  27. }  

      调用上述函数后,就可以在shell命令行下使用ifconfig 命令配置虚拟网卡了:

ifconfig devname 10.0.0.1 up

route add -net 10.0.0.2 netmask 255.255.255.255 dev devname

配置好虚拟网卡地址后,就可以通过生成的字符设备描述符,在程序中使用readwrite函数就可以读取或者发送给虚拟的网卡数据了。

tun/tap的实现

    tun/tap设备驱动的开始也是init函数,其中主要调用了misc_register注册了一个miscdev设备。

点击(此处)折叠或打开

  1. static int __init tun_init(void)
  2. {
  3. /*……*/
  4.     ret = misc_register(&tun_miscdev);
  5.     /*……*/
  6. }

tun_miscdev得定义如下:

点击(此处)折叠或打开

  1. static struct miscdevice tun_miscdev = {
  2.     .minor = TUN_MINOR,
  3.     .name = "tun",
  4.     .nodename = "net/tun",
  5.     .fops = &tun_fops,
  6. }

    注册完这个设备之后将在系统中生成一个/dev/net/tun”文件,同字符设备类似,当应用程序使用open系统调用打开这个文件时,将生成file文件对象,而其file_operations将指向tun_fops

点击(此处)折叠或打开

  1. static const struct file_operations tun_fops = {
  2.     .owner    = THIS_MODULE,
  3.     .llseek = no_llseek,
  4.     .read = do_sync_read,
  5.     .aio_read = tun_chr_aio_read,
  6.     .write = do_sync_write,
  7.     .aio_write = tun_chr_aio_write,
  8.     .poll    = tun_chr_poll,
  9.     .unlocked_ioctl    = tun_chr_ioctl,
  10. #ifdef CONFIG_COMPAT
  11.     .compat_ioctl = tun_chr_compat_ioctl,
  12. #endif
  13.     .open    = tun_chr_open,
  14.     .release = tun_chr_close,
  15.     .fasync = tun_chr_fasync
  16. };

下面我们以应用层使用的步骤来分析内核的对应实现。应用层首先调用open打开“/dev/net/tun”,这将最终调用tun_fopsopen函数,即tun_chr_open

点击(此处)折叠或打开

  1. static int tun_chr_open(struct inode *inode, struct file * file)
  2. {
  3.     struct tun_file *tfile;

  4.     DBG1(KERN_INFO, "tunX: tun_chr_open\n");

  5.     /*分配并初始化struct tun_file结构*/
  6.     tfile = (struct tun_file *)sk_alloc(&init_net, AF_UNSPEC, GFP_KERNEL,
  7.                     &tun_proto);
  8.     if (!tfile)
  9.         return -ENOMEM;
  10.     rcu_assign_pointer(tfile->tun, NULL);
  11.     tfile->net = get_net(current->nsproxy->net_ns);
  12.     tfile->flags = 0;

  13.     rcu_assign_pointer(tfile->socket.wq, &tfile->wq);
  14.     init_waitqueue_head(&tfile->wq.wait);

  15.     tfile->socket.file = file;
  16.     /*设置struct tun_file的socket成员ops*/
  17.     tfile->socket.ops = &tun_socket_ops;

  18.     sock_init_data(&tfile->socket, &tfile->sk);
  19.     sk_change_net(&tfile->sk, tfile->net);

  20.     tfile->sk.sk_write_space = tun_sock_write_space;
  21.     tfile->sk.sk_sndbuf = INT_MAX;
  22.     /*将struct tun_file作为file的私有字段,而file就是每次应用调用open打开/dev/net/tun生成的*/
  23.     file->private_data = tfile;
  24.     set_bit(SOCK_EXTERNALLY_ALLOCATED, &tfile->socket.flags);
  25.     INIT_LIST_HEAD(&tfile->next);

  26.     sock_set_flag(&tfile->sk, SOCK_ZEROCOPY);

  27.     return 0;
  28. }

经过这个函数后,整个数据结构的关系就如下图所示。注意这里的struct file结构就是每次应用调用open打开/dev/net/tun生成的。

    应用程序执行完open操作后,一般会执行ioctl (fd, TUNSETIFF, (void *) &ifr) 来真正创建tap/tun设备。这将最终调用tun_ops中的tun_chr_ioctl函数。tun_chr_ioctl中会调用__tun_chr_ioctl

点击(此处)折叠或打开

  1. static long __tun_chr_ioctl(struct file *file, unsigned int cmd,
  2.              unsigned long arg, int ifreq_len)
  3. {
  4.     struct tun_file *tfile = file->private_data;
  5.     struct tun_struct *tun;
  6.     void __user* argp = (void __user*)arg;
  7.     struct ifreq ifr;
  8.     kuid_t owner;
  9.     kgid_t group;
  10.     int sndbuf;
  11.     int vnet_hdr_sz;
  12.     int ret;

  13.     if (cmd == TUNSETIFF || cmd == TUNSETQUEUE || _IOC_TYPE(cmd) == 0x89) {
  14.         if (copy_from_user(&ifr, argp, ifreq_len))
  15.             return -EFAULT;
  16.     } else {
  17.         memset(&ifr, 0, sizeof(ifr));
  18.     }
  19.     if (cmd == TUNGETFEATURES) {
  20.         /* Currently this just means: "what IFF flags are valid?".
  21.          * This is needed because we never checked for invalid flags on
  22.          * TUNSETIFF. */
  23.         return put_user(IFF_TUN | IFF_TAP | IFF_NO_PI | IFF_ONE_QUEUE |
  24.                 IFF_VNET_HDR | IFF_MULTI_QUEUE,
  25.                 (unsigned int __user*)argp);
  26.     } else if (cmd == TUNSETQUEUE)
  27.         return tun_set_queue(file, &ifr);

  28.     ret = 0;
  29.     rtnl_lock();
  30. /*获取tun_struct结构,首次调用TUNSETIFF时为NULL*/
  31.     tun = __tun_get(tfile);
  32.     if (cmd == TUNSETIFF && !tun) {
  33.         ifr.ifr_name[IFNAMSIZ-1] = '\0';

  34.         ret = tun_set_iff(tfile->net, file, &ifr);

  35.         if (ret)
  36.             goto unlock;

  37.         if (copy_to_user(argp, &ifr, ifreq_len))
  38.             ret = -EFAULT;
  39.         goto unlock;
  40.     }

  41.     ret = -EBADFD;
  42.     if (!tun)
  43.         goto unlock;

  44.     ret = 0;
  45.     switch (cmd) {
  46.     case TUNGETIFF:
  47. /*……*/
  48. }
  49. unlock:
  50. rtnl_unlock();
  51. if (tun)
  52.     tun_put(tun);
  53. return ret;
  54. }

可以看出如果cmdTUNSETIFF,则会调用tun_set_iff函数。

点击(此处)折叠或打开

  1. static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
  2. {
  3.     struct tun_struct *tun;
  4.     struct tun_file *tfile = file->private_data;
  5.     struct net_device *dev;
  6.     int err;

  7.     if (tfile->detached)
  8.         return -EINVAL;

  9.     dev = __dev_get_by_name(net, ifr->ifr_name);
  10.     if (dev) { /*首次调用dev为NULL*/
  11.          /**/
  12.     }
  13.     else {
  14.         char *name;
  15.         unsigned long flags = 0;
  16.         int queues = ifr->ifr_flags & IFF_MULTI_QUEUE ?
  17.              MAX_TAP_QUEUES : 1;

  18.         if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
  19.             return -EPERM;

  20.         /* Set dev type */
  21.         if (ifr->ifr_flags & IFF_TUN) { /*tun设备*/
  22.             /* TUN device */
  23.             flags |= TUN_TUN_DEV;
  24.             name = "tun%d";
  25.         } else if (ifr->ifr_flags & IFF_TAP) { /*tap设备*/
  26.             /* TAP device */
  27.             flags |= TUN_TAP_DEV;
  28.             name = "tap%d";
  29.         } else
  30.             return -EINVAL;

  31.         if (*ifr->ifr_name)
  32.             name = ifr->ifr_name;
  33.         /*分配net_device结构,并将struct tun_struct作为其private结构*/
  34.         dev = alloc_netdev_mqs(sizeof(struct tun_struct), name,
  35.                  tun_setup, queues, queues);

  36.         if (!dev)
  37.             return -ENOMEM;

  38.         dev_net_set(dev, net);
  39.         dev->rtnl_link_ops = &tun_link_ops;

  40.         tun = netdev_priv(dev);
  41.         tun->dev = dev;
  42.         tun->flags = flags; /*标识设备的类型,tun或tap*/
  43.         tun->txflt.count = 0;
  44.         tun->vnet_hdr_sz = sizeof(struct virtio_net_hdr);

  45.         tun->filter_attached = false;
  46.         tun->sndbuf = tfile->socket.sk->sk_sndbuf;

  47.         spin_lock_init(&tun->lock);
  48.         /*根据设备类型是tap或tun初始化net_device结构,关键是其dev->netdev_ops 成员*/
  49.         tun_net_init(dev);

  50.         err = tun_flow_init(tun);
  51.         if (err < 0)
  52.             goto err_free_dev;

  53.         dev->hw_features = NETIF_F_SG | NETIF_F_FRAGLIST |
  54.             TUN_USER_FEATURES;
  55.         dev->features = dev->hw_features;
  56.         dev->vlan_features = dev->features;

  57.         INIT_LIST_HEAD(&tun->disabled);
  58.         /*将tun(tun_struct)和file的private,即tun_file关联*/
  59.         err = tun_attach(tun, file);
  60.         if (err < 0)
  61.             goto err_free_flow;
  62.         /*注册net_device,完成网络设备驱动程序的注册*/
  63.         err = register_netdevice(tun->dev);
  64.         if (err < 0)
  65.             goto err_detach;

  66.         if (device_create_file(&tun->dev->dev, &dev_attr_tun_flags) ||
  67.          device_create_file(&tun->dev->dev, &dev_attr_owner) ||
  68.          device_create_file(&tun->dev->dev, &dev_attr_group))
  69.             pr_err("Failed to create tun sysfs files\n");
  70.     }
  71.     /*……*/
  72.     if (netif_running(tun->dev)) /*运行网卡qdisc 队列*/
  73.         netif_tx_wake_all_queues(tun->dev);

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

  76. err_detach:
  77.     tun_detach_all(dev);
  78. err_free_flow:
  79.     tun_flow_uninit(tun);
  80.     security_tun_dev_free_security(tun->security);
  81. err_free_dev:
  82.     free_netdev(dev);
  83.     return err;
  84. }

经过这个函数之后tun/tap的相关数据结构组织就如下图所示了。

数据通道实现

下面我们看tun/tap设备是如何进行数据转发的,我们从两个方向分析,首先是用户态到内核态,然后是内核态到用户态。整个过程如下图所示。

用户态到内核态

用户态调用writetun/tap设备中写入数据,最终调用kernel中对应file结构中的tun_fopswrite函数。整个调用路径如下。

    

注意tun/tap设备的net_device并没有向bridge那样注册rx_handle接受函数,所以经过netif_receive_skb后就进入了上层协议栈,对于系统来说就像从物理网卡eth0接受上来的包一样。如果用户态想在接受,就要创建socket了。

内核态到用户态

     tun/tap设备发出的数据包(这里的发出是指例如将tap设备加入独立的容器或namespace中后通过容器网卡发出,用户态的write是不会导致数据包从tap设备发出,而是送给协议栈,就需要调用net_devicendo_start_xmit函数了。整个流程如下图(橙色的线)。这里要说明一点,有人可能会疑惑如果所有进程都打开/dev/net/tun读取数据不会混淆吗?答案是不会的,因为每个进程open后内核都有自己的file文件对象,同时TUNSETIFF后也会有不同的net_device设备对象。

说明,要区分把tap设备直通给容器/namespace和把veth pair的一端通给容器/namespace区分开,tap设备通给容器/namespace,则容器内的数据发送会走上述“内核到用户态”的逻辑,用户态需要有进程以读文件形象等待tap数据的接收;veth pair中的一端时其ndo_start_xmit函数和tap设备不同时直接发送另一端veth pair的。

阅读(2615) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~