tun/tap

Feb 15, 2022



基本概念


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_tap_0

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

tun_tap_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);


        

    }

应用实例

tun2sockes

tun2http