Linux 中的虚拟网络

TUN、TAP 和 bridge

Posted by Jerry Chen on September 15, 2020

TUN/TAP 虚拟网络

介绍

TUN/TAP 是 Linux 内核虚拟网络设备。其中 TUN 是点对点设备,TAP 是以太网设备;tun 设备不需要 mac 地址,同一广播域只能有两个设备,并且传输信息无以太网帧头;

用户空间存在一个字符设备 /dev/net/tun,可以通过对它进行 ioctl 配置生成虚拟网络设备,如 tun0、tap0 等;

准备

内核默认支持,如果没有就按照如下路径打开:

Device Drivers => Network device support => Universal TUN/TAP device driver support

确保支持后,如果没有发现 /dev/net/tun 设备节点,那就手动创建一个:

1
2
# c 示为字符设备,10 200 别是主设备号和次设备号
mknod /dev/net/tun c 10 200

开始编程

编写 c 文件

编写 test_tun.c 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <assert.h>
#include <fcntl.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <unistd.h>

int tun_alloc(char *dev, int flags)
{
    assert(dev != NULL);

    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 != '\0'){
        strncpy(ifr.ifr_name, dev, IFNAMSIZ);
    }

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

    // 一旦设备开启成功,系统会给设备分配一个名称,对于tun设备,一般为tunX,X为从0开始的编号;
    // 对于tap设备,一般为tapX
    strcpy(dev, ifr.ifr_name);

    return fd;
}

int main()
{
    int tun_fd, nread;
    char buffer[4096];
    char tun_name[IFNAMSIZ] = "mynet";
    //tun_name[0] = '\0'; //截断后将自动命名 tun/tap 设备

    /* 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);
    }

    printf("Open tun/tap device: %s\n", tun_name);

    while (1){
        unsigned char ip[4];
        // 收包
        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/tap device\n", nread);

        // 简单对收到的包调换一下顺序
        memcpy(ip, &buffer[12], 4);
        memcpy(&buffer[12], &buffer[16], 4);
        memcpy(&buffer[16], ip, 4);

        buffer[20] = 0;
        *((unsigned short *)&buffer[22]) += 8;

        // 发包
        nread = write(tun_fd, buffer, nread);

        printf("Write %d bytes to tun/tap device\n", nread);
    }
    return 0;
}
进行测试
  1. 编译生成 test_tun;

    1
    
    gcc test_tun.c -o test_tun
    
  2. 运行 ./test_tun 会打印 TUN name is mynet,创建了虚拟网络设备 mynet;

    可以使用 ip addr show 查看所有的网络设备;

  3. 设置虚拟网络设备的 IP

    1
    
    sudo ip addr add 192.168.2.2/24 dev mynet
    

    默认创建虚拟网络设备后,其状态为 down,需要手动 up:

    1
    
    sudo ip link set dev mynet up
    
  4. ping 一下虚拟设备

    需要网络状态为 up

    1
    
    ping -c3 192.168.2.2 -I mynet
    

使用软件包管理虚拟网络设备

tunctl

安装:

1
apt-get install uml-utilities

用法:

1
2
3
4
5
6
7
# -p 创建 tap 设备,默认
# -n 创建 tun 设备
# -t 指定创建的 tap/tun 设备名称
# -d 删除指定接口
tunctl -t name
tunctl -n -t name
tunctl -d name
[推荐] ip tuntap

安装:

属于 iproute2 工具箱,一般自带;

创建 tap/tun 设备:

1
2
Copyip tuntap add dev tap0 mod tap # 创建 tap 
ip tuntap add dev tun0 mod tun # 创建 tun

删除 tap/tun 设备:

1
2
Copyip tuntap del dev tap0 mod tap # 删除 tap 
ip tuntap del dev tun0 mod tun # 删除 tun

PS: usergroup 参数和 tunctl 的 -u、 -g 参数是一样的;

bridge-utils 网桥

安装

1
sudo apt install bridge-utils

使用

1
2
3
4
5
6
7
brctl 常规用法:
    addbr  #添加一个虚拟网桥
    addif  #为虚拟网桥添加物理端口
    delbr  #删除虚拟网桥,但是,在删除网桥的时候保证网桥是down的
    delif  #删除虚拟网桥上的一个物理接口
    show   #查看网桥的相关配置
    stp    #开启或关闭虚拟网桥的生成树功能

环境

PC 机虚拟网卡 vEthernet(WSL) 的 IP 为 172.20.240.1/20

WSL 中有一网卡 eth0 的 IP 为 172.20.245.58/20

实验

在 WSL 中虚拟一个网桥 br0,并将 WSL 中的网卡 eth0 加入到网桥:

1
2
3
4
5
6
7
sudo brctl addbr br0
sudo brctl addif br0 eth0

# 展示全部网桥
brctl show
# 展示 br0 网桥
brctl showstp br0

接着给 br0 添加 IP 和网关,注意网关需要填写 vEthernet 的 IP:

1
2
3
4
sudo ip addr add 172.20.240.100/20 brd 172.20.240.1 dev br0

# 别忘了激活网桥设备
sudo ip link set dev br0 up

成功后,在 WSL 中可以 ping 通网关 vEthernet,PC 也可以 ping 通 br0;

扩展

在 WSL 中新建一个虚拟 TAP 网络设备 tap0,把它挂载到 br0 网桥,在 PC 机上访问它;

1
2
3
4
5
6
sudo tunctl -t tap0
sudo brctl addif br0 tap0

# 配置 IP,其中网关 IP 需要为 br0 的 IP
sudo ip addr add 172.20.240.101/20 brd 172.20.240.100 dev tap0
sudo ip link set dev tap0 up