基本概念
tun/tap为用户空间的程序提供了收发数据包的能力,不同于物理网口从内核层接受ip数据包,tun/tap从用户层接受,同理,tun/tap向用户程序发送数据而不是向内核发送。
tun/tap本质是一个虚拟设备,一端是内核网络接口,另一端是用户空间的文件描述符。它有两种工作模式:
- Tap mode, 工作于L2数据链路层,主要用于vm
- Tun mode, 工作于L3网络层,主要用于vpn一类的应用
TUN/TAP provides packet reception and transmission for user space programs. It can be > seen as a simple Point-to-Point or Ethernet device
上面是内核文档中描述tun/tap的一段,其中提到了tun设备其实是一种Point-to-Point
网络设备,那怎么理解呢?
POINT2POINT表示该网络接口上不存在L2的选址功能:
- 没有ARP请求(ipv4)
- 没有NDP请求(ipv6)
- 不涉及内核层的邻居子系统(neighbour layer)
- 不支持路由表中
via
指令 - 该接口上的数据包只能送到the same(only) next-hop
和tap或者其他正常的网口(Ethernet device)对比来说,tun网口的一端只会连接一个设备,而Ethernet设备往往都连接很多设备,需要通过neighbour layer去处理不同的连接。
网络模型
tun设备在应用层暴露的是fd,提供应用读写数据的接口,在内核层和物理网卡一样对接的是网络栈,另外从图中看出很重要的一点就是,tun设备在内核层是没有ring buffer
的,这就导致如果tun设备的TX queue
满了之后就会丢失数据包。
下面通过一个具体的测试,来更加直观的了解网络数据是如何在tun设备中流动的。
假设我们已经通过/dev/net/tun
内核文件对象创建了tun0
, 然后执行:
ip addr add 192.168.1.1/24 dev tun0
ip route add 10.1.1.0/24 via 192.168.1.2
ping 10.1.1.2 -I 10.1.1.1
上面的app A是ping应用程序,app B可以是openvpn进程。
数据包的发送流程为:
- ping应用程序将数据包通过系统调用发送到内核网络协议栈
- 协议栈根据数据包的目的IP地址,匹配本地路由规则,知道这个数据包应该由tun0发出,于是将数据包送到tun0队列
- tun0在内核层收到数据后,发现有进程挂在自己的等待队列上,于是唤醒挂起的进程,进程(openvpn)完成数据包的读取
- openvpn收到数据包后进行类似于应用层的网络协议栈处理,使用tcp或者udp打包这个数据包,构造一个ip地址可以从物理网卡(eth0)出去的新数据包
- 这个新数据包进入内核协议栈后,最终顺利通过
eth0
发送出去
响应数据包的接收流程:
- eth0收到一个
10.33.0.1 -> 10.33.0.11
的数据包,送人内核协议栈 - 内核将数据送到openvpn进程
- openvpn进程处理后将数据包写入tun0在应用层的fd
- tun0在内核协议中将数据包回送给ping进程
内核代码实现
tun内核态到用户态(read from /dev/tun)
__dev_queue_xmit() -> dev_hard_start_xmit() -> netdev_start_xmit() -> ops->ndo_start_xmit(skb, dev)
ndo_start_xmit是tun驱动程序注册的函数,实现将内核数据传输到用户层。在内核驱动/drivers/net/tun.c
中能看到ndo_start_xmit注册为tun_net_xmit.
linux-V6.1.7
static const struct net_device_ops tun_netdev_ops = {
.ndo_init = tun_net_init,
.ndo_uninit = tun_net_uninit,
.ndo_open = tun_net_open,
.ndo_stop = tun_net_close,
.ndo_start_xmit = tun_net_xmit,
.ndo_fix_features = tun_net_fix_features,
.ndo_select_queue = tun_select_queue,
.ndo_set_rx_headroom = tun_set_headroom,
.ndo_get_stats64 = tun_net_get_stats64,
.ndo_change_carrier = tun_net_change_carrier,
};
static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct tun_struct *tun = netdev_priv(dev);
int txq = skb->queue_mapping;
...
/* NETIF_F_LLTX requires to do our own update of trans_start */
queue = netdev_get_tx_queue(dev, txq);
txq_trans_cond_update(queue);
/* Notify and wake up reader process */
if (tfile->flags & TUN_FASYNC)
kill_fasync(&tfile->fasync, SIGIO, POLL_IN);
tfile->socket.sk->sk_data_ready(tfile->socket.sk);
...
}
用户态到内核(write to /dev/tun)
通过syscall -> tun_fops.write -> … -> tun_chr_write_iter -> tun_get_user
/* Get packet from user space buffer */
static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
void *msg_control, struct iov_iter *from,
int noblock, bool more)
{
...
struct sk_buff *skb;
bool frags = tun_napi_frags_enabled(tfile);
...
if ((tun->flags & TUN_TYPE_MASK) == IFF_TAP) {
align += NET_IP_ALIGN;
if (unlikely(len < ETH_HLEN ||
(gso.hdr_len && tun16_to_cpu(tun, gso.hdr_len) < ETH_HLEN)))
return -EINVAL;
}
...
switch (tun->flags & TUN_TYPE_MASK) {
case IFF_TUN:
...
skb_reset_mac_header(skb); //tun只工作在L3
skb->protocol = pi.proto;
skb->dev = tun->dev;
break;
case IFF_TAP:
if (frags && !pskb_may_pull(skb, ETH_HLEN)) {
err = -ENOMEM;
drop_reason = SKB_DROP_REASON_HDR_TRUNC;
goto drop;
}
skb->protocol = eth_type_trans(skb, tun->dev);
break;
}
...
skb_reset_network_header(skb);
skb_probe_transport_header(skb);
skb_record_rx_queue(skb, tfile->queue_index);
}