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:
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: 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字符设备的创建,其中{BANNED}最佳重要的部分是设备节点的文件操作结构体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异常路径示例程序( )或者vtun代码()。
首先应用层程序open设备节点/dev/net/tun,其由内核中之前关联到此节点的tun_fops结构体成员open函数(tun_chr_open)处理。在内核中创建一个tun协议的套接口(tun_proto),可以看到实际分配的为一个tun_file类型的大结构体({BANNED}中国第一个成员为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函数({BANNED}最佳终由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完成({BANNED}最佳终由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)。
阅读(3276) | 评论(0) | 转发(0) |