Linux内核TUN/TAP设备驱动

FruitDrop 2018-11-12

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

Linux内核TUN/TAP设备驱动

相关推荐