Linux内核TUN/TAP设备驱动

Linux内核的TUN/TAP虚拟设备,不同于内核的其它设备,其发送和接收数据包都在网络协议栈内部完成,发送的数据包并不会离开协议栈进入到物理网络中,同样,也不会接收到从物理网络中进入协议栈的数据包。

用户空间的设备节点/dev/net/tun用于读写TUN/TAP设备,内核中TUN/TAP设备在发送数据包时,将数据包发送到与/dev/net/tun文件描述符相关联的套接口,用户空间就可从设备节点读取数据。用户空间程序向/dev/net/tun文件描述符写入数据时,TUN/TAP驱动调用内核的数据包接收函数(如netif_rx)将接收到的数据包送入网络协议栈,就像数据包是从物理网络中接收的一样。

使用TUN/TAP设备,可实现各种各样的隧道,如下示意图:

    |-----------|   |--------------------------|    |    |--------------------------|    |-----------| 
    |           |   |                          |    |    |                          |    |           | 
    |   apps    |   |     |---- tunnel ----|   |    |    |     |---- tunnel ----|   |    |   apps    | 
    |           |   |     |                |   |    |    |     |                |   |    |           | 
    |-----------|   |------------|    |--------|    |    |--------|    |------------|    |-----------| 
    192.168.1.0/24  |/dev/net/tun|    | socket |    |    | socket |    |/dev/net/tun|    192.168.1.0/24
                    |------------|----|--------|    |    |--------|----|------------|
                                           |                  |
                                           |----19.1.1.0/24---|   

    ---------------------------------------------------------------------------------------------------									   
    
    Linux内核网络协议栈 (ip route add 0.0.0.0/0 dev tun0)

图中左侧隧道处理程序从/dev/net/tun文件描述符读取网络应用发出的数据包,经过处理,比如加密,之后通过套接口发往远端(19.1.1.0网络)。图中右侧隧道处理程序通过套接口接收到数据包之后,经过处理,比如解密,之后写入/dev/net/tun文件描述符中,网络应用程序将从内核中接收到原始的数据包。当右侧网络应用回复数据包时,又会沿着原路返回左侧应用。

使用IP命令创建TUN/TAP设备。

$ ip tuntap add name tap0 mode tap
$ ip tuntap add name tun0 mode tun
$ 
$ ip link show type tun

4: tap0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 16:cf:09:a3:4d:89 brd ff:ff:ff:ff:ff:ff
5: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 500
    link/none

TUN设备与TAP设备的不同之处在于TUN设备处理IP数据包,TAP设备要求以太网数据包,前一个工作在三层网络,后一个工作在二层网络。


TUNTAP系统初始化

内核函数tun_init初始化TUN/TAP设备驱动,其中注册rtnetlink链路处理结构,但目前IP命令并不是通过rtnetlink接口创建TUN/TAP设备,注册这个tun_link_ops貌似还没有用起来。另外,注册一个misc类型的字符设备,设备节点为net/tun,注意TUN/TAP设备共用此设备节点,IP命令通过此设备节点提供的ioctl创建TUN/TAP网络设备。

static int __init tun_init(void)
{
    ret = rtnl_link_register(&tun_link_ops);
    ret = misc_register(&tun_miscdev);
}

先来看一下注册的rtnetlink链路处理结构tun_link_ops,其中kind成员初始化为字符串”tun“,ip命令工具集iproute2虽然并为使用此接口创建接口,但是在使用此接口实现ip link show type tun命令。TUN/TAP驱动在创建新设备时将tun_link_ops赋值给了设备的rtnl_link_ops成员,在处理ip显示命令时将tun_link_ops的kind字段(tun字符串)赋值给你netlink的属性IFLA_INFO_KIND, 所以,ip显示命令可以与命令行的type的值比较,过滤出TUN/TAP设备来。

static struct rtnl_link_ops tun_link_ops __read_mostly = {
    .kind       = DRV_NAME, //"tun"
    .priv_size  = sizeof(struct tun_struct),
    .setup      = tun_setup,
};

static int rtnl_link_info_fill(struct sk_buff *skb, const struct net_device *dev)
{
    const struct rtnl_link_ops *ops = dev->rtnl_link_ops;
    nla_put_string(skb, IFLA_INFO_KIND, ops->kind);
}

函数tun_setup初始化了TUN/TAP设备私有结构体中的用户相关属性(owner/group),将发送队列大小设置为TUN_READQ_SIZE(500)。在创建设备时调用此函数。

static void tun_setup(struct net_device *dev)
{
    struct tun_struct *tun = netdev_priv(dev);
    tun->owner = INVALID_UID;
    tun->group = INVALID_GID;
    dev->tx_queue_len = TUN_READQ_SIZE;
}

再来看第二部分misc字符设备的创建,其中最重要的部分是设备节点的文件操作结构体tun_fops。

static struct miscdevice tun_miscdev = {
    .minor = TUN_MINOR,
    .name = "tun",
    .nodename = "net/tun",
    .fops = &tun_fops,
};

tun_fops定义了misc字符设备文件的读写以及ioctl系统调用处理函数。

static const struct file_operations tun_fops = {
    .read_iter  = tun_chr_read_iter,
    .write_iter = tun_chr_write_iter,
    .poll   = tun_chr_poll,
    .unlocked_ioctl = tun_chr_ioctl,
    .open   = tun_chr_open,
};

设备创建


TUNTAP设备的创建是通过操作设备文件/dev/net/tun来实现的。应用层实例程序可参看iproute2代码中的实现,或者DPDK异常路径示例程序( https://doc.dpdk.org/guides/sample_app_ug/exception_path.html)或者vtun代码(http://vtun.sourceforge.net/)。

首先应用层程序open设备节点/dev/net/tun,其由内核中之前关联到此节点的tun_fops结构体成员open函数(tun_chr_open)处理。在内核中创建一个tun协议的套接口(tun_proto),可以看到实际分配的为一个tun_file类型的大结构体(第一个成员为struct sock),之后将此套接口与打开的tun文件描述符做相互的关联。 tun_socket_ops为套接口的操作函数集,目前只有sendmsg和recvmsg两个。这两个函数分别对应设备节点的写操作和读操作。

    static const struct proto_ops tun_socket_ops = {
        .peek_len = tun_peek_len,
        .sendmsg = tun_sendmsg,
        .recvmsg = tun_recvmsg,
    };
    static struct proto tun_proto = {
        .name       = "tun",
        .obj_size   = sizeof(struct tun_file),
    };
    struct tun_file *tfile;
    tfile = (struct tun_file *)sk_alloc(net, AF_UNSPEC, GFP_KERNEL, &tun_proto, 0);
	
    tfile->socket.file = file;
	tfile->socket.ops = &tun_socket_ops;
    file->private_data = tfile;

接下来应用层程序使用ioctl系统调用的TUNSETIFF命令参数,控制上一步得到的tun设备节点的文件描述符来创建TUN/TAP网络设备,内核的tun_chr_ioctl函数处理ioctl调用,网络设备的创建具体由tun_set_iff函数实现,创建完成后,tun_struct的成员dev指向新创建的设备结构体(net_device)。

static long __tun_chr_ioctl(struct file *file, unsigned int cmd, unsigned long arg, int ifreq_len)
{
    if (cmd == TUNSETIFF)
        ret = tun_set_iff(sock_net(&tfile->sk), file, &ifr);
}

应用程序使用标志IFF_TUN/IFF_TAP来区分创建的设备类型,保存在内核中的tun_struct结构体成员flags中,在分配net_device结构体时分配出了这个tun_struct结构。tun_net_init函数具体处理TUN/TAP设备的差异化。

static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
{
    struct tun_struct *tun;
    struct tun_file *tfile = file->private_data;

    dev = alloc_netdev_mqs(sizeof(struct tun_struct), name, NET_NAME_UNKNOWN, tun_setup, queues, queues);

    dev->rtnl_link_ops = &tun_link_ops;
    tun = netdev_priv(dev);
    tun->dev = dev;
    tun->flags = flags;  // IFF_TUN或者IFF_TAP

    tun_net_init(dev);
    tun_flow_init(tun);

    err = tun_attach(tun, file, false, ifr->ifr_flags & IFF_NAPI);
    err = register_netdevice(tun->dev);
}

由TUN/TAP的初始化代码可见,TUN设备为一个Point-to-Point点到点设备,其二层网络头部长度为0,没有ARP;而TAP设备为虚拟以太网设备,有标准的以太网函数ether_setup建立,并且生成的随机的硬件MAC地址。前者设备操作函数集使用tun_netdev_ops,后者TAP设备使用tap_netdev_ops。

static void tun_net_init(struct net_device *dev)
{
    struct tun_struct *tun = netdev_priv(dev);

    switch (tun->flags & TUN_TYPE_MASK) {
    case IFF_TUN:
        dev->netdev_ops = &tun_netdev_ops;

        /* Point-to-Point TUN Device */
        dev->hard_header_len = 0;
        dev->addr_len = 0;
        dev->mtu = 1500;

        /* Zero header length */
        dev->type = ARPHRD_NONE;
        dev->flags = IFF_POINTOPOINT | IFF_NOARP | IFF_MULTICAST;

    case IFF_TAP:
        dev->netdev_ops = &tap_netdev_ops;
        /* Ethernet TAP Device */
        ether_setup(dev);

        eth_hw_addr_random(dev);
    }
}

至此,内核中表示TUN/TAP设备的结构体tun_struct与设备本身(net_device)基本初始化完成。接下来需要将tun_struct与打开的TUN/TAP设备描述符关联起来。即将tun_struct结构体指针赋予tun_file的tun成员,参见函数tun_attach。

static int tun_attach(struct tun_struct *tun, struct file *file, bool skip_filter, bool napi)
{
    struct tun_file *tfile = file->private_data;
    rcu_assign_pointer(tfile->tun, tun);
}

由TUN/TAP创建过程可知,设备节点/dev/net/tun为一个操作入口,可使用其文件描述符创建新的网络设备。

                                      file_operations tun_fops
                               |----------------------------------| open (tun_file & sock)
   tun file descriptor(file) | .open   = tun_chr_open           |-----|
    |--------------------|     | .read_iter  = tun_chr_read_iter  |     |
    |                    |---->| .write_iter = tun_chr_write_iter |     |  ioctl (tun_struct & device)
    | *f_ops = tun_fops  |     | .unlocked_ioctl = tun_chr_ioctl  |----------------|
    |--------------------|     |----------------------------------|     |          |
    |                    |                                              |          |
    | *private_data      |---->|------------------------------|<--------|          |
    |--------------------|     | struct sock sk               |                    |
    ^                       |--| struct socket socket         |                    \/
    |                       |  |                              |             struct tun_struct      
    |  |----------------|---|  | struct tun_struct __rcu *tun |-------->|-----------------------|          
    |--|   file *file   |      |------------------------------|         |   tun_file *tfiles[]  |
       |----------------|              struct tun_file              |---|   net_device *dev     |
         struct socket                                              |   |-----------------------|
                                                                    |
						    |------------------------------------|<-|             
                            | dev->netdev_ops = &tun_netdev_ops  |  | TUN device
                            |------------------------------------|  |
                                                                    |
                            |------------------------------------|<-|
                            | dev->netdev_ops = &tap_netdev_ops  |  | TAP device
                            |------------------------------------|

TUN/TAP文件写数据

写操作由函数tun_chr_write_iter函数(最终由tun_get_user函数实现)完成。分配skb,拷贝数据包,调用tun_rx_batched(内部调用netif_receive_skb(skb))接收数据到内核协议栈。

static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile, void *msg_control, struct iov_iter *from, ...)
{ 
    skb = tun_alloc_skb(tfile, align, copylen, linear, noblock);
    err = skb_copy_datagram_from_iter(skb, 0, from, len);
    tun_rx_batched(tun, tfile, skb, more);
}

如果是由sendmsg触发调用tun_get_user函数,并且msghdr结构成员struct iovec *msg_iov中的页面数量不超过MAX_SKB_FRAGS宏定义的值,tun_get_user函数可实现数据包的零拷贝,由函数zerocopy_sg_from_iter实现:

int zerocopy_sg_from_iter(struct sk_buff *skb, struct iov_iter *from)
{
    int copy = min_t(int, skb_headlen(skb), iov_iter_count(from));

    /* copy up to skb headlen */
    if (skb_copy_datagram_from_iter(skb, 0, from, copy))
        return -EFAULT;  
    return __zerocopy_sg_from_iter(NULL, skb, from, ~0U);
} 

如果用户在创建设备时ioctl设置了IFF_NAPI参数,TUN/TAP驱动将注册一个NAPI的poll处理函数tun_napi_poll,此时tun_get_user函数只需要将skb添加到文件描述符关联的sock的sk_write_queue队列即可,调用napi_schedule交由poll函数接收数据包。

static int tun_napi_receive(struct napi_struct *napi, int budget)
{
    struct tun_file *tfile = container_of(napi, struct tun_file, napi);
    struct sk_buff_head *queue = &tfile->sk.sk_write_queue;

    while (received < budget && (skb = __skb_dequeue(&process_queue)))
        napi_gro_receive(napi, skb);
}

TUN/TAP文件读数据

读操作由函数tun_chr_read_iter完成(最终由tun_do_read实现)。TUN/TAP接收到的数据保存在tun_file结构体成员的tx_array(struct skb_array)中,tx_array为一个环形结构,对TUN/TAP文件的读操作作为环的消费者,生产者为TUN/TAP网络设备的数据发送,内核协议栈将数据包路由到TUN/TAP设备。

static ssize_t tun_do_read(struct tun_struct *tun, struct tun_file *tfile, struct iov_iter *to, int noblock, struct sk_buff *skb)
{
    if (!skb) {
        /* Read frames from ring */
        skb = tun_ring_recv(tfile, noblock, &err);
    }
    ret = tun_put_user(tun, tfile, skb, to);
}

TUN/TAP网络设备发送

TUN/TAP设备提供给上层的操作函数集tun_netdev_ops,其中ndo_start_xmit回调用于上层发送数据的接口。对于TUN/TAP发送函数tun_net_xmit,处理比较简单,其将数据包放入tx_array环中,通知上层数据包已准备好。应用层select在TUN/TAP文件描述符上的进程将接收到消息。

static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct tun_struct *tun = netdev_priv(dev);

    if (skb_array_produce(&tfile->tx_array, skb))
        goto drop;

    tfile->socket.sk->sk_data_ready(tfile->socket.sk);
    return NETDEV_TX_OK;
}

两外,内核中有专门的tap驱动(drivers/net/tap.c)用于和macvtap、ipvtap一同使用,与TUN/TAP驱动类似(drivers/net/tun.c)。


内核版本

Linux-4.15

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页