tun/tap 设备
{Back to Index}

Table of Contents

虚拟网络设备也归内核的网络设备管理子系统管理,对于 Linux 内核网络设备管理模块来说,虚拟设备和物理设备没有区别,都是网络设备,都能配置 IP , 从网络设备来的数据,都会转发给协议栈,协议栈过来的数据,也会交由网络设备发送出去,至于是怎么发送出去的,发到哪里去,那是设备驱动的事情。

所以说,和物理设备一样, 虚拟网络设备的一端也是协议栈,而另一端是什么取决于虚拟网络设备的驱动实现。

tun/tap 设备 1 一端连着协议层, 另一端连接用户层的应用程序 ,协议栈发给 tun/tap 的数据包能被这个应用程序读取到,并且应用程序能直接向 tun/tap 写数据。

用户层程序通过 tun 设备只能读写 IP 数据包,而通过 tap 设备能读写链路层数据包,处理数据包的格式不一样。

tun/tap 设备可以将协议栈中的部分数据包转发给用户空间的应用程序,给用户空间的程序一个处理数据包的机会。 于是比较常用的数据压缩,加密等功能就可以在应用程序里面做进去,常用的场景是 VPN ,包括 tunnel 以及应用层的 IPSec 等。

1 使用 ip tuntap 命令创建

ip tuntap add name tap0 mode tap
ip tuntap del name tap0 mode tap # 删除时必须同时指定 mode ,因为可以创建同名的 tun 或 tap 设备

这里给出通过 C 程序对 tun 设备进行监听的代码,如果需要监听 tap 设备,将 flag 设为 IFF_TAP 即可:

// tun.c

#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <linux/if_tun.h>
#include<stdlib.h>
#include<stdio.h>

int tun_alloc(char *dev, int flags) {

    struct ifreq ifr;
    int fd, err;
    char *clonedev = "/dev/net/tun";

    if ((fd = open(clonedev, O_RDWR)) < 0) {
        return fd;
    }

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = flags;

    if (*dev) {
        /* if a device name was specified, put it in the structure; otherwise,
         * the kernel will try to allocate the "next" device of the
         * specified type */
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);
    }

    if ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) {
        close(fd);
        return err;
    }

    strcpy(dev, ifr.ifr_name);
    printf("Open tun device: %s for reading...\n", ifr.ifr_name);

    return fd;
}

int main(int argc, char *argv[]) {
    char tun_name[IFNAMSIZ];
    if( argc == 2 ) {
        strcpy(tun_name, argv[1]);
    }
    else if( argc > 2 ) {
        printf("Too many arguments supplied.\n");
        exit(1);
    }
    else {
        printf("Tun device name expected.\n");
        exit(1);
    }
    int tun_fd, nread;
    char buffer[1500];

    /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
     *        IFF_TAP   - TAP device
     *        IFF_NO_PI - Do not provide packet information
     */

    tun_fd = tun_alloc(tun_name, IFF_TUN | IFF_NO_PI);

    if (tun_fd < 0) {
        perror("Allocating interface");
        exit(1);
    }

    while (1) {
        nread = read(tun_fd, buffer, sizeof(buffer));
        if (nread < 0) {
            perror("Reading from interface");
            close(tun_fd);
            exit(1);
        }

        printf("Read %d bytes from tun device %s\n", nread, tun_name);
    }
    return 0;
}
ip tuntap add name mytun mode tun
ip l set mytun up
gcc tun.c -o tun
ping -I mytun localhost &       # 放到后台
./tun mytun                     # 开始监听
# 可以观察到
# Open tun device: mytun for reading...
# Read 84 bytes from tun device mytun

2 使用 socat 创建

socat -u TUN:10.0.6.1/24,tun-type=tap,up EXEC:'hexdump -C' # 监听 tap 设备

socat -u TUN:10.0.7.1/24,iff-no-pi,up EXEC:'hexdump -C' # 监听 tun 设备

使用 socat 可以搭建简易的隧道。

2.1 基于 tap 的隧道 2

# server
socat tcp-l:40839,reuseaddr TUN:172.0.0.1/24,iff-no-pi,up,tun-type=tap
# socat 默认将添加路由,否则,需手工添加 172.0.0.0/24 网段路由,经由生成的 tap 设备

# client
socat tcp:10.74.68.100:40839 TUN:172.0.0.2/24,iff-no-pi,up,tun-type=tap

tap-tunnel.png

Figure 1: 使用 tap 设备搭建隧道

从下图的抓包中可见,底层 TCP 连接上承载了二层数据,即 TCP 的 payload 为以太网帧 (Ethernet over IP) :

tap-tcpdump.png

Figure 2: socat + tap

2.2 基于 tun 的隧道 3

原理和使用 tap 类似,底层 TCP 连接上承载了以太网数据,即 TCP 的 payload 为 IP 数据报文 (IP over IP) 。

3 使用 pytun 创建

from pytun import TunTapDevice

tun = TunTapDevice()

tun.addr = '10.8.0.1'
tun.netmask = '255.255.255.0'
tun.mtu = 1500
tun.up()

print("tun device (%s) created" % tun.name)

while True:
    try:
        buf = tun.read(tun.mtu)
        print ("read from %s: %s" % (tun.name, buf))
    except Exception as e:
        print(e)
        tun.close()
        exit()

Footnotes:

Author: Hao Ruan (ruanhao1116@gmail.com)

Created: 2021-01-18 Mon 23:17

Updated: 2021-08-17 Tue 11:23

Emacs 27.1 (Org mode 9.3)