Sirius
Sirius

目录

Linux下的虚拟网卡TUN TAP

通常的socket编程,面对的都是物理网卡,Linux下其实很容易创建虚拟网卡;本文简单介绍一下Linux虚拟网卡的概念,并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡,最终实现一个从客户端到服务器的简单的IP隧道,希望本文能对理解虚拟网卡和IP隧道有所帮助,本文将提供完整的源程序;

  • TUN和TAP是Linuxn内核的虚拟网络设备,不同于普通靠硬件网络适配器实现的设备,这些虚拟的网络设备全部用软件实现,并可以向运行于Linux上的应用软件提供与硬件的网络设备完全相同的功能;
  • TAP等同于一个以太网设备,它操作OSI模型的第二层(数据链路层)数据包,通常我们所使用的网络就是以太网数据帧,所以要使用TAP设备,就需要自己构建以太网报头、IP报头、TCP/UDP报头;
  • TUN模拟了网络层设备,操作第三层(网络层)数据包,通常我们使用的TCP/UDP报文在网络层使用的IP协议,所以使用TUN设备,需要自己构建IP报头和TCP/UDP报头,比TAP设备少构建一个以太网报头;
  • Linux通过TUN/TAP设备向绑定该设备的用户空间的应用程序发送数据;同样,用户空间的应用程序也可以像操作硬件网络设备那样,通过TUN/TAP设备发送数据;在后面这种情况下,TUN/TAP设备向Linux的网络协议栈提交数据包,从而模拟从外部接收数据的过程;
  • 上一节的描述显然过于枯燥,可能会对初次接触虚拟网卡的读者感到困惑,不知所云,本节将实际建立一个tun设备,帮助你走出困惑;

  • 构建一个基本的tun设备,只需要两个步骤

    1. 编写一个程序,至少完成三个任务

      • 以可读写模式打开设备文件 /dev/net/tun
      int fd; fd = open("/dev/net/tun", O_RDWR));
      • 向Linux内核注册一个tun设备名称,本例中为tun0

        (struct ifreq)定义在头文件<linux/if.h>中,

        struct ifreq ifr; 
        memset(&ifr, 0, sizeof(ifr)); 
        ifr.ifr_flags = IFF_TUN | IFF_NO_PI; 
        strcpy(ifr.ifr_name, "tun0"); 
        
        ioctl(fd, TUNSETIFF, (void *)&ifr);
      • 编写处理tun0接收/发送数据的程序

        char buffer[BUFSIZE]; 
        while (1) { 
        read(fd, buffer, BUFSIZE); // todo 
        }
    2. 为设备分配IP地址(本例中为tun0分配的IP为10.0.0.1) `bash
      sudo ifconfig tun0 10.0.0.1 netmask 255.255.255.0 up```

  • 把上面的代码片段组合在一起,就可以完成一个tun设备的建立,

  • 这段程序在进入循环前增加了 system("ifconfig tun0 10.0.0.1/24 up"),为tun0分配了IP地址10.0.0.1,所以运行完后就不需要再为这个设备分配IP了;

  • 编译:gcc -Wall tun-01.c -o tun-01

  • 该程序需要root权限运行,主要是因为其中使用了ioctl,运行:sudo ./tun-01

  • 运行该程序,会构建一个tun设备,打开一个新的终端,使用 ifconfig 将可以看到系统中多了一个虚拟网络接口tun0,使用 route -n 查看路由也会看到增加了一条关于tun0设备的路由

    screenshot of setting up tun0 device

    图1:构建一个tun设备后


  • 尽管建立起了虚拟网卡tun0,但因为程序过于简单,所以这样建立的设备什么事情都做不了,必须完善程序,才能让这个设备真正地发挥作用;

  • tun设备是一个第三层(网络层)的设备,在这个设备上只能收到IP报头,收不到以太网报头,所以Linux索性没有为tun设备分配MAC地址;

  • 后面将以本节的程序为基础,不断改进,最终写出一个简单的IP隧道的程序。

/*
 * File: tun-01.c

 * To compile: $ gcc -Wall tun-01.c -o tun-01
 * Usage: $ sudo ./tun-01
 *
 * Example source code for article 《使用tun虚拟网络接口建立IP隧道的实例》
 * 
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>

#include <linux/if.h>
#include <linux/if_tun.h>

#include <sys/types.h>
#include <sys/ioctl.h>

#include <arpa/inet.h> 

#define BUFSIZE         2048
#define TUN_DEV_NAME    "tun0"
#define TUN_DEV_FILE    "/dev/net/tun"

int tun_fd = 0;
unsigned long int tun_count = 0;

void sigint(int signum);

int main(int argc, char *argv[]) {
    uint16_t nread;
    char buffer[BUFSIZE];
    struct ifreq ifr;

    // Open device file /dev/net/tun
    tun_fd = open(TUN_DEV_FILE , O_RDWR);
    // Register device name into Linux kernel
    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
    strcpy(ifr.ifr_name, TUN_DEV_NAME);
    ioctl(tun_fd, TUNSETIFF, (void *)&ifr);
    printf("Successfully connected to interface %s\n", TUN_DEV_NAME);
    signal(SIGINT, sigint);
    // set up IP address for tun0
    system("ifconfig tun0 10.0.0.1/24 up");
    // Process the packet received from tun0
    while (1) {
        nread = read(tun_fd, buffer, BUFSIZE);
        printf("TUN %lu: Read %d bytes from the tun interface\n", ++tun_count, nread);
    }

    if (tun_fd) close(tun_fd);
    return 0;
}

void sigint(int signum) {
    // Clean up ......
    if (tun_fd > 0) close(tun_fd);    
    printf("Terminating....\n");
    printf("Totally packets from tun: %ld packets\n", tun_count);
    exit(EXIT_SUCCESS);
}
  • 设备建立起来以后,程序员关心的是我们如何从这个设备上收发报文,如何处理这些报文;

  • 对于一个物理网络接口而言,接口一端连接着网络协议栈,另一端连接着物理网络;而对于一个虚拟网络接口而言,接口的一端仍然连接着网络协议栈,但是另一端连接着一个应用程序,也就是我们前面的那个程序,我们把这个程序称为 application-tun

  • 可以和一个物理网络接口比较来说明虚拟网络接口的数据流向,在物理接口上要发送到物理网络上去的报文,相对于虚拟接口将被发送到应用程序 application-tun 上;

  • 当我们使用socket发送报文时,报文被提交给Linux的网络协议栈,协议栈为报文封装各个协议层的报头,并根据路由表将报文交给相应设备的驱动程序,比如enp0s3的驱动程序,然后由驱动程序将报文发送到物理网络上(物理设备),或者发送给应用程序 application-tun(虚拟设备);

  • 在上一节中,我们使用 route -n 已经看到了关于tun0设备的路由:

内核 IP 路由表
目标            网关            子网掩码        标志  跃点   引用  使用 接口
0.0.0.0        192.168.2.3     0.0.0.0        UG   100    0     0  enp0s3
10.0.0.0       0.0.0.0         255.255.255.0  U    0      0     0  tun0
169.254.0.0    0.0.0.0         255.255.0.0    U    1000   0     0  enp0s3
192.168.2.0    0.0.0.0         255.255.255.0  U    100    0     0  enp0s3
  • 路由表明,当目的IP地址为10.0.0.x时,报文将被送到虚拟设备tun0的驱动程序上去,该设备绑定的IP为10.0.0.1;

  • 还有一条路由,当目的IP地址为192.168.2.x时,报文将被送到物理设备enp0s3的驱动程序上去,该设备绑定的IP为192.168.2.114;

  • 这两条路由比较相似,区别是一个是物理设备enp0s3,另一个是虚拟设备tun0,我们拿这两条路由进行对比说明数据流向;

  • 发送报文到物理/虚拟接口绑定的IP地址上

    • 当我们发送一个UDP报文到 192.168.2.114:5678(也就是本机物理设备enp0s3的IP)时,根据路由,报文被送给enp0s3的驱动程序,驱动程序并不会把这个报文发送到物理网络上,因为enp0s3的驱动程序已经是这个报文最终的目的地,所以enp0s3的驱动程序会将这个报文发到一个正在监听192.168.2.114:5678的用户程序上,如果我们没有编写这个程序,报文将被丢弃,这样我们就收不到这个报文;
    • 当我们发送一个UDP报文到 10.0.0.1:5678(也就是本机虚拟设备tun0的IP)时,根据路由,报文被送给tun0的驱动程序,驱动程序并不会把这个报文发送到 application-tun 上,因为tun0的驱动程序已经是这个报文的最终目的地,所以tun0的驱动程序会将这个报文发到一个正在监听10.0.0.1:5678的用户程序上,和物理设备一样,如果我们没有编写这个程序,报文将被丢弃,我们收不到这个报文;

    send data to tun’s IP

    图2:发送报文到tun0的IP上


  • 发送报文到符合路由的其他IP地址上

    • 当我们发送一个UDP报文到 192.168.2.112:5678 时,根据路由报文会被送给enp0s3的驱动程序,驱动程序会把这个报文发送到物理网络上;
    • 当我们发送一个UDP报文到 10.0.0.2:5678 时,根据路由报文会被送给报文被送给tun0的驱动程序,驱动程序会把这个报文发送到应用程序 application-tun 上;

    send data to tun’s route

    图3:发送报文到符合tun0路由的其他IP上


  • 对上述说明可以做一个简单的测试

    • 打开终端,运行前面的程序:tun-01

      sudo ./tun-01

    • 打开另一个终端,使用下面命令分别向 10.0.0.1:5678 发送数据,在运行 tun-01 的终端上并不会显示收到数据; echo "hello" > /dev/udp/10.0.0.1/5678

    • 使用下面命令分别向 10.0.0.2:5678 发送数据,在运行 tun-01 的终端上会显示收到数据;

      echo "hello" > /dev/udp/10.0.0.2/5678

  • 源IP地址的选择

    • 当我们在电脑系统上运行 sudo ./tun-01 时,我们的系统就有了两个IP地址,一个是物理网卡的,IP为192.168.2.114,另一个是虚拟网卡的,IP为10.0.0.1;

    • 当我们在做上面的测试时,我们用 echo ...... 命令向 10.0.0.1 和 10.0.0.2 发送了UDP消息,发送时我们并没有指定源IP地址,那么发出的消息的源IP地址是什么呢?192.168.2.114 还是 10.0.0.1?

    • 我们把前面那个程序 tun_01.c 改一下,一是增加一些错误判断,使这个程序更加完善一些,另外我们增加一个显示IPv4报头的功能,这样我们就可以看到IP头中的源IP地址了;

    • 改好的程序在后面

    • 编译:gcc -Wall tun-02.c -o tun-02

    • 下面我们做个测试,向 10.0.0.2:5678 发送一条UDP消息,我们看看源IP地址是什么?

      • 打开一个终端,运行tun-02

        sudo ./tun-02

      • 打开另一个终端,向10.0.0.2发送消息

        echo "hello" > /dev/udp/10.0.0.2/5678

      • 在运行tun-02的终端上显示出源IP地址为10.0.0.1

        screenshot for source IP

        图4:Linux在多网卡环境下选择源IP


    • 当使用sendmsg()发送数据时,是可以显式地指定源IP地址的;

    • 路由表中有一个src字段,当没有指定源IP地址时,将使用选定路由的src字段作为源IP地址,使用 ip route 可以看到src字段

      screenshot of ‘ip route’

      图5:ip route命令显示路由表中src字段


    • 如果选定的路由没有src字段,Linux会搜寻选定路由的网络接口上所有绑定的IP,对IPv6将选择第一个搜寻到的地址,对IPv4则尽量选择与目标IP在同一网段的IP地址;
/*
 * File: tun-02.c

 * To compile: $ gcc -Wall tun-02.c -o tun-02
 * Usage: $ sudo ./tun-02
 *
 * Example source code for article 《使用tun虚拟网络接口建立IP隧道的实例》
 * 
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>

#include <linux/if.h>
#include <linux/if_tun.h>
#include <linux/ip.h>

#include <sys/types.h>
#include <sys/ioctl.h>

#include <arpa/inet.h> 

#define BUFSIZE         2048
#define TUN_DEV_NAME    "tun0"
#define TUN_DEV_FILE    "/dev/net/tun"

int tun_fd = 0;
unsigned long int tun_count = 0;

void sigint(int signum);

void print_iphdr(struct iphdr *iph) {
    uint8_t *p1, *p2;

    printf("===============================================\n");
    if (iph->version == 4) {
        printf("Version: %d\tInternet Header Length: %d bytes\n", iph->version, iph->ihl * 4);
        printf("TOS: %d\tTotal Length: %d bytes\n", iph->tos, ntohs(iph->tot_len));
        printf("ID: %d\tFragment Offset: %d\n", ntohs(iph->id), ntohs(iph->frag_off));
        printf("TTL: %d\tProtocol: %d\tChecksum: %d\n", iph->ttl, iph->protocol, ntohs(iph->check));
        p1 = (uint8_t *)&iph->saddr;
        p2 = (uint8_t *)&iph->daddr;
        printf("Source IP: %d.%d.%d.%d\tDestination IP: %d.%d.%d.%d\n",
                p1[0], p1[1], p1[2], p1[3], p2[0], p2[1], p2[2], p2[3]);
    } else {
        printf("The packet is not IPv4.\n");
    }
    printf("\n");

}
int main(int argc, char *argv[]) {
    uint16_t nread;
    char buffer[BUFSIZE];
    struct ifreq ifr;
    int err, ret_value = EXIT_SUCCESS;

    // Open device file /dev/net/tun
    if( (tun_fd = open(TUN_DEV_FILE , O_RDWR)) < 0 ) {
        perror("Opening /dev/net/tun");
        exit(EXIT_FAILURE);
    }
    // Register device name into Linux kernel
    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
    strcpy(ifr.ifr_name, TUN_DEV_NAME);
    if( (err = ioctl(tun_fd, TUNSETIFF, (void *)&ifr)) < 0 ) {
        perror("ioctl(TUNSETIFF)");
        ret_value = EXIT_FAILURE;
        goto quit;
    }
    printf("Successfully connected to interface %s\n", TUN_DEV_NAME);
    signal(SIGINT, sigint);
    // set up IP address for tun0
    system("ifconfig tun0 10.0.0.1/24 up");
    // Process the packet received from tun0
    while (1) {
        if ((nread = read(tun_fd, buffer, BUFSIZE)) < 0) {
            perror("Read from tun");
            ret_value = EXIT_FAILURE;
            goto quit;
        } else if (nread == 0) {
            // Do nothing.
        } else {
            printf("TUN %lu: Read %d bytes from the tun interface\n", ++tun_count, nread);
            print_iphdr((struct iphdr *)buffer);
        }
    }

quit:
    if (tun_fd) close(tun_fd);
    return ret_value;
}

void sigint(int signum) {
    // Clean up ......
    if (tun_fd > 0) close(tun_fd);    
    printf("Terminating....\n");
    printf("Totally packets from tun: %ld packets\n", tun_count);
    exit(EXIT_SUCCESS);
}
  • tun实际上是tunnel的前面三个字母,tun设备注定和隧道是有关系的,tun设备也的确常用来构建一个IP隧道;

  • IP报文其实是指:IP报头 + TCP/UDP报头 + 数据

  • 所谓IP隧道是指把一个IP报文作为数据再封装一个TCP头和IP头,所以整个报文变成:IP报头 + TCP报头 + (IP报头 + TCP/UDP报头 + 数据)

  • 至于IP隧道的意义、应用场景之类的,本文不予讨论,可以自己去百个度或者谷个歌查一下,本文将致力于做一个简单的IP隧道;

  • 先看一张示意图

    Diagram Simple IP tunnel

    图6:简单的IP隧道示意图

  • 有两台电脑,Computer A和Computer B

    • Computer A:
      1. 物理网卡为enp0s3,绑定IP:192.168.2.112
      2. 虚拟网卡为tun0,绑定IP:10.0.0.2
    • Computer B:
      1. 物理网卡为enp0s3,绑定IP:192.168.2.114
      2. 虚拟网卡为tun0,绑定IP:10.0.0.1
  • Computer A和Computer B的路由表一样,如下:

内核 IP 路由表
目标            网关            子网掩码        标志  跃点   引用  使用 接口
0.0.0.0        192.168.2.3     0.0.0.0        UG   100    0     0  enp0s3
10.0.0.0       0.0.0.0         255.255.255.0  U    0      0     0  tun0
169.254.0.0    0.0.0.0         255.255.0.0    U    1000   0     0  enp0s3
192.168.2.0    0.0.0.0         255.255.255.0  U    100    0     0  enp0s3
  • Computer A的应用程序app A向10.0.0.1:1234发送报文,Computer B的应用程序app D侦听在10.0.0.1:1234上;

  • 目标很简单,computer A的app A直接向10.0.0.1:1234发送报文,computer B的app D能够正常收到收到,就像在一个局域网上一样;

  • 首先要明确的,物理局域网的网段是192.168.2.x,所以向10.0.0.1发送报文并不会被送到物理局域网上,按照路由,这条报文会被送到tun0的驱动程序上去,因为10.0.0.1并不是computer A的虚拟网卡tun0绑定的IP,所以驱动程序会把这个报文送到application-tun上,所以如果我们不做处理,这个报文根本无法到达目的地;

  • 如何处理这个报文使其发送到computer B的app D上去呢?通常的方法就是在computer A和computer B的物理网卡之间建立一条IP隧道;

  • 当computer A启动applition-tun时,主动发起向computer B的连接,端口号定为5678,computer B在启动applition-tun时,主动侦听在端口5678上,并等待computer A的连接请求,一旦连接建立,这个隧道就建好了;

  • computer A的application-tun收到发往10.0.0.1的报文时,要在整个IP报文上再包装上一个IP报头+TCP报头,TCP报头中指定目的端口号为5678,IP报头中指定目的IP为192.168.2.114,源IP为192.168.2.112,然后把这个新报文从建立的隧道中发出;

  • computer B上侦听在5678端口上的应用程序app C会收到这个报文,app C去掉IP报头和TCP报头,把数据部分作为一个完整的报文重新从socket发出,这个报文的内容正是computer A发出的原始报文,computer B的内核协议栈根据路由会将该报文发给tun0的驱动程序,驱动程序会将这个报文送到正在侦听1234端口的app D上;

  • 在客户端(computer A)需要编写一个程序,程序文件名:app-client.c,这个程序应遵循以下处理流程:

    1. 打开 /dev/net/tun 文件,返回tun_fd,在内核注册虚拟设备 tun0;
    2. 创建socket,sock_fd,在这个 sock_fd 上连接服务器端(computer B)的5678端口,建立IP隧道;
    3. 使用select检查tun_fd和sock_fd,并分别处理在这两个 fd 上收到的数据;
    4. 在tun_fd上收到数据的处理流程
      • 将收到的包括IP报头在内的报文作为数据从sock_fd上发出
    5. 在sock_fd上收到数据的处理流程
      • 把收到的数据作为一个IP报文显示报头及内容
  • 在服务器端(computer B)编写一个程序,文件名为:app-server.c,这个程序应遵循以下处理流程:

    1. 打开 /dev/net/tun 文件,返回tun_fd,在内核注册虚拟设备 tun0;
    2. 创建socket,fd为sock_fd,在这个 sock_fd 上侦听5678端口,等待客户端连接以建立IP隧道;
    3. 接受客户端的连接请求,为新连接创建socket,fd为net_fd
    4. 使用select检查tun_fd和net_fd,并分别处理在这两个 fd 上收到的数据;
    5. 在tun_fd上收到数据的处理流程
      • 将收到的包括IP报头在内的报文作为数据从net_fd上发出
    6. 在net_fd上收到数据的处理流程
      • 把收到的数据(不包括IP报头和TCP报头)作为带有IP报头的报文发到tun_fd上
  • 客户端程序在后面

  • 客户端程序编译:gcc -Wall app-client.c -o app-client

  • 服务器端程序在后面

  • 服务器端程序编译:gcc -Wall app-server.c -o app-server

  • 为了运行方便,也可以将这两个程序写成守护进程,将程序中注释掉的 daemon(0, 0) 放开即可;

  • 请根据实际情况调整程序中的宏定义,SERVER_IP和TUN_IP;

  • 在服务器端注意防火墙设置,打开5678端口或者关闭防火墙;

  • 这两个程序的运行均需要root权限。

  • 客户端程序测试

    • 需要打开三个终端窗口;

    • 首先将程序中的SERVER_IP改为本机的IP地址,然后重新编译;

    • 打开第一个终端,运行 nc -l 5678,这个命令将监听本机的5678端口;

    • 打开第二个终端,运行客户端程序:sudo ./app-client,应该显示"Connected to server …"

    • 打开第三个终端,运行 echo "hello" > /dev/udp/10.0.0.1/1234,这个命令将向10.0.0.1的1234端口发送一个UDP报文,报文的数据部分为"hello"

    • 此时在第二个终端上应该显示"Received data from tun",在第一个终端上收到一些乱码,但其中有"hello"字符串,乱码是因为我们收到的数据包括IP报头和UDP报头,这两部分是二进制的数据;

    • 如果你看到的和上面的描述一致,那么你的客户端程序基本没有问题;

    • 下面是截屏

      screenshot of 1st terminal for client test

      图7:测试客户端程序时的第一个终端

      screenshot of 2nd terminal for client test

      图8:测试客户端程序时的第二个终端

  • 服务器端程序无需独立测试;

  • IP隧道测试

    • 需要两台机器,一台做客户端,另一台做服务器端

    • 再次强调,请根据实际情况调整程序中的宏定义,SERVER_IP和TUN_IP,并重新编译程序;

    • 在服务器端注意防火墙设置,打开5678端口或者关闭防火墙;

    • 在服务器端和客户端均需要打开两个终端,下面是测试方法示意图

      diagram for testing

      图9:测试示意图

    • 在服务器第一个终端上启动服务器端程序 sudo ./app-server

    • 在服务器第二个终端上执行命令 nc -luk 1234,这个命令将一直监听在UDP的1234端口上;

    • 在客户端第一个终端上启动客户端程序 sudo ./app-client

    • 在客户端第二个终端上执行命令 echo "hello" > /dev/udp/10.0.0.1/1234,这个命令将向10.0.0.1(服务器端的tun0绑定的IP)的UDP端口1234发送一条消息

    • 客户端第二个终端上向10.0.0.1:1234发送了一个UDP消息,内容是:hello

    • 最终在服务器端的第二个终端上收到了这个信息

    • 下面是运行截图

      screenshot server 1st terminal for testing

      图10:服务器端第一个终端

      screenshot client 1st terminal for testing

      图11:客户端第一个终端

      screenshot server 2nd terminal for testing

      图12:服务器端第二个终端

      screenshot client 2nd terminal for testing

      图13:客户端第二个终端

/*
 * File: app-client.c

 * To compile: $ gcc -Wall app-client.c -o app-client
 * Usage: $ sudo ./app-client
 *
 * Example source code for article 《使用tun虚拟网络接口建立IP隧道的实例》
 * 
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>

#include <linux/if.h>
#include <linux/if_tun.h>
#include <linux/ip.h>

#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/select.h>

#include <arpa/inet.h> 

#define BUFSIZE         2048
#define SERVER_IP       "192.168.2.114"
#define PORT            5678
#define TUN_DEV_NAME    "tun0"
#define TUN_DEV_FILE    "/dev/net/tun"
#define TUN_IP          "10.0.0.2"

int tun_fd = 0, sock_fd = 0;
unsigned long int tun_count = 0, nic_count = 0;

void sigint(int signum);

/******************************************************************
 * Function: void print_iphdr(struct iphdr *iph)
 * Description: Print IP header
 ******************************************************************/
void print_iphdr(struct iphdr *iph) {
    uint8_t *p1, *p2;

    if (iph->version == 4) {
        printf("===============================================\n");
        printf("Version: %d\tInternet Header Length: %d bytes\n", iph->version, iph->ihl * 4);
        printf("TOS: %d\tTotal Length: %d bytes\n", iph->tos, ntohs(iph->tot_len));
        printf("ID: %d\tFragment Offset: %d\n", ntohs(iph->id), ntohs(iph->frag_off));
        printf("TTL: %d\tProtocol: %d\tChecksum: %d\n", iph->ttl, iph->protocol, ntohs(iph->check));
        p1 = (uint8_t *)&iph->saddr;
        p2 = (uint8_t *)&iph->daddr;
        printf("Source IP: %d.%d.%d.%d\tDestination IP: %d.%d.%d.%d\n",
                p1[0], p1[1], p1[2], p1[3], p2[0], p2[1], p2[2], p2[3]);
        printf("\n");
    }

}

int main(int argc, char *argv[]) {
    uint16_t nread, nwrite;
    char buffer[BUFSIZE];
    struct sockaddr_in remote;

    struct ifreq ifr;
    int err, ret_value = EXIT_SUCCESS;

    int maxfd = 0;

    char cmd[128];

    // Step 01: set up tun_fd and register tun0 into linux kernel
    //============================================================
    // Open device file /dev/net/tun
    if( (tun_fd = open(TUN_DEV_FILE , O_RDWR)) < 0 ) {
        perror("Opening /dev/net/tun");
        exit(EXIT_FAILURE);
    }
    // Register device name into Linux kernel
    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
    strcpy(ifr.ifr_name, TUN_DEV_NAME);
    if( (err = ioctl(tun_fd, TUNSETIFF, (void *)&ifr)) < 0 ) {
        perror("ioctl(TUNSETIFF)");
        ret_value = EXIT_FAILURE;
        goto quit;
    }
    printf("Successfully connected to interface %s\n", TUN_DEV_NAME);

    // Step 02: set up sock_fd and connect to server
    //===============================================
    if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket()");
        exit(EXIT_FAILURE);
    }

    // assign the destination address
    memset(&remote, 0, sizeof(remote));
    remote.sin_family = AF_INET;
    remote.sin_addr.s_addr = inet_addr(SERVER_IP);
    remote.sin_port = htons(PORT);

    /* connection request */
    if (connect(sock_fd, (struct sockaddr*)&remote, sizeof(remote)) < 0) {
        perror("connect()");
        ret_value = EXIT_FAILURE;
        goto quit;
    }
    printf("CLIENT: Connected to server %s\n", inet_ntoa(remote.sin_addr));

    // Step 03: set up ctrl+c handler and allocate an IP for tun0
    //============================================================
    signal(SIGINT, sigint);
    // set up IP address for tun0
    sprintf(cmd, "ifconfig %s %s/24 up", TUN_DEV_NAME, TUN_IP);
    system(cmd);
    //daemon(0, 0);


    // Process the packet received from tun0
    maxfd = (tun_fd > sock_fd) ? tun_fd : sock_fd;
    while(1) {
        int ret;
        fd_set rd_set;

        FD_ZERO(&rd_set);
        FD_SET(sock_fd, &rd_set);
        FD_SET(tun_fd, &rd_set);

        ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL);
        if (ret < 0 && errno == EINTR){
            continue;
        }

        if (ret < 0) {
            perror("select()");
            ret_value = EXIT_FAILURE;
            goto quit;
        }

        // Step 04: Process data received from tun
        //=========================================
        if (FD_ISSET(tun_fd, &rd_set)) {
            // data from tun: just read it and write it to the network
            if ((nread = read(tun_fd, buffer, BUFSIZE)) < 0) {
                perror("Read from tun");
                ret_value = EXIT_FAILURE;
                goto quit;
            } else if (nread == 0) {
                // Do nothing.
            } else if (nread > sizeof(struct iphdr)) {
                struct iphdr *p = (struct iphdr *)buffer;
                if (p->version == 4 && p->protocol == IPPROTO_UDP && p->daddr == inet_addr("10.0.0.1")) {
                    // write the data to sock_fd
                    printf("Received data from tun.\n");
                    tun_count++;
                    nwrite = write(sock_fd, buffer, nread);
                    if (nwrite < 0) {
                        perror("write");
                        ret_value = EXIT_FAILURE;
                        goto quit;
                    }
                }
            }
        }

        // Step 05: Process data received from NIC
        //=========================================
        if (FD_ISSET(sock_fd, &rd_set)) {
            // data from physical nic, read it and display the IP header
            // read packet
            if ((nread = read(sock_fd, buffer, BUFSIZE)) < 0) {
                perror("Read from nic");
                ret_value = EXIT_FAILURE;
                goto quit;
            } else if (nread == 0) {
                // socket has been closed
                printf("The socket has been closed by server end.\n");
                ret_value = EXIT_FAILURE;
                goto quit;
            }
            // now buffer[] contains a full packet or frame
            buffer[nread] = 0;
            nic_count++;
            printf("Client received message from server.\n");
            if (nread >= sizeof(struct iphdr)) print_iphdr((struct iphdr *)buffer);
        }
    }

quit:
    if (sock_fd > 0) close(sock_fd);
    if (tun_fd > 0) close(tun_fd);
    return(ret_value);
}

/*********************************************
 * Function: void sigint(int signum)
 * Desxription: ctrl+c handler
 *********************************************/
void sigint(int signum) {
    // Clean up ......
    if (tun_fd > 0) close(tun_fd);
    if (sock_fd > 0) close(sock_fd);
    printf("Terminating....\n");
    printf("Totally packets from tun: %ld packets\n", tun_count);
    printf("Totally packets from server: %ld packets\n", nic_count);
    exit(EXIT_SUCCESS);
}
/*
 * File: app-server.c

 * To compile: $ gcc -Wall app-server.c -o app-server
 * Usage: $ sudo ./app-server
 *
 * Example source code for article 《使用tun虚拟网络接口建立IP隧道的实例》
 * 
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>

#include <linux/if.h>
#include <linux/if_tun.h>
#include <linux/ip.h>

#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/select.h>

#include <arpa/inet.h> 

#define BUFSIZE         2048
//#define SERVER_IP       "192.168.2.114"
#define PORT            5678
#define TUN_DEV_NAME    "tun0"
#define TUN_DEV_FILE    "/dev/net/tun"
#define TUN_IP          "10.0.0.1"

int tun_fd = 0, sock_fd = 0, net_fd = 0;
unsigned long int tun_count = 0, nic_count = 0;

void sigint(int signum);

/******************************************************************
 * Function: void print_iphdr(struct iphdr *iph)
 * Description: Print IP header
 ******************************************************************/
void print_iphdr(struct iphdr *iph) {
    uint8_t *p1, *p2;

    if (iph->version == 4) {
        printf("===============================================\n");
        printf("Version: %d\tInternet Header Length: %d bytes\n", iph->version, iph->ihl * 4);
        printf("TOS: %d\tTotal Length: %d bytes\n", iph->tos, ntohs(iph->tot_len));
        printf("ID: %d\tFragment Offset: %d\n", ntohs(iph->id), ntohs(iph->frag_off));
        printf("TTL: %d\tProtocol: %d\tChecksum: %d\n", iph->ttl, iph->protocol, ntohs(iph->check));
        p1 = (uint8_t *)&iph->saddr;
        p2 = (uint8_t *)&iph->daddr;
        printf("Source IP: %d.%d.%d.%d\tDestination IP: %d.%d.%d.%d\n",
                p1[0], p1[1], p1[2], p1[3], p2[0], p2[1], p2[2], p2[3]);
        printf("\n");
    }

}

int main(int argc, char *argv[]) {
    uint16_t nread, nwrite;
    char buffer[BUFSIZE];
    //struct sockaddr_in remote;

    struct ifreq ifr;
    int err, ret_value = EXIT_SUCCESS;

    int maxfd = 0;
    char cmd[128];

    struct sockaddr_in server_addr;

    // Step 01: set up tun_fd and register tun0 into linux kernel
    //============================================================
    // Open device file /dev/net/tun
    if( (tun_fd = open(TUN_DEV_FILE , O_RDWR)) < 0 ) {
        perror("Opening /dev/net/tun");
        exit(EXIT_FAILURE);
    }
    // Register device name into Linux kernel
    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
    strcpy(ifr.ifr_name, TUN_DEV_NAME);
    if( (err = ioctl(tun_fd, TUNSETIFF, (void *)&ifr)) < 0 ) {
        perror("ioctl(TUNSETIFF)");
        ret_value = EXIT_FAILURE;
        goto quit;
    }
    printf("Successfully connected to interface %s\n", TUN_DEV_NAME);

    // Step 02: set up sock_fd and listen on a port
    //==============================================
    if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket()");
        exit(EXIT_FAILURE);
    }

    bzero(&server_addr, sizeof(server_addr));
    // assign IP, PORT
    server_addr.sin_family      = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port        = htons(PORT);

    // Binding newly created socket to given IP and verification
    if ((bind(sock_fd, (const struct sockaddr *)&server_addr, sizeof(server_addr))) != 0) {
        perror("bind()");
        close(sock_fd);
        exit(EXIT_FAILURE);
    } else printf("Successfully bind an address on sock_fd.\n");

    if ((listen(sock_fd, 5)) != 0) {
        perror("listen()");
        ret_value = EXIT_FAILURE;
        goto quit;
    } else printf("Server listening..\n\n");
    
    // Step 03: Accept the connection request from the client
    //========================================================
    // Accept the data packet from client and verification
    //len = sizeof(client_addr);
    //connfd = accept(sockfd, (struct sockaddr *)&client_addr, &len);
    net_fd = accept(sock_fd, NULL, NULL);
    if (net_fd < 0) {
        perror("accept()");
        ret_value = EXIT_FAILURE;
        goto quit;
    } else printf("Accept the connection request from client.\n");

    // Step 04: set up ctrl+c handler and allocate an IP for tun0
    //============================================================
    signal(SIGINT, sigint);
    // set up IP address for tun0
    sprintf(cmd, "ifconfig %s %s/24 up", TUN_DEV_NAME, TUN_IP);
    system(cmd);
    //daemon(0, 0);

    // Process the packet received from tun0
    maxfd = (tun_fd > net_fd) ? tun_fd : net_fd;
    while(1) {
        int ret;
        fd_set rd_set;

        FD_ZERO(&rd_set);
        FD_SET(net_fd, &rd_set);
        FD_SET(tun_fd, &rd_set);

        ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL);
        if (ret < 0 && errno == EINTR){
            continue;
        }

        if (ret < 0) {
            perror("select()");
            ret_value = EXIT_FAILURE;
            goto quit;
        }

        // Step 05: Process data received from tun
        //=========================================
        if (FD_ISSET(tun_fd, &rd_set)) {
            // data from tun: just read it and write it to the network
            if ((nread = read(tun_fd, buffer, BUFSIZE)) < 0) {
                perror("Read from tun");
                ret_value = EXIT_FAILURE;
                goto quit;
            } else if (nread == 0) {
                // Do nothing.
            } else {
                struct iphdr *p = (struct iphdr *)buffer;
                if (p->version == 4 && p->protocol == IPPROTO_UDP) {
                    // write the data to sock_fd
                    printf("Received data from tun.\n");
                    tun_count++;
                    /*
                    nwrite = write(net_fd, buffer, nread);
                    if (nwrite < 0) {
                        perror("write");
                        ret_value = EXIT_FAILURE;
                        goto quit;
                    }
                    */
                }
            }
        }

        // Step 06: Process data received from NIC
        //=========================================
        if (FD_ISSET(net_fd, &rd_set)) {
            // data from physical nic, read it and send to tun
            // read packet
            if ((nread = read(net_fd, buffer, BUFSIZE)) < 0) {
                perror("Read from nic");
                ret_value = EXIT_FAILURE;
                goto quit;
            } else if (nread == 0) {
                // socket has been closed
                printf("The socket has been closed by client end.\n");
                ret_value = EXIT_FAILURE;
                goto quit;
            }
            // now buffer[] contains a full packet or frame
            buffer[nread] = 0;
            nic_count++;
            printf("Server received message from client.\n");
            //if (nread >= sizeof(struct iphdr)) print_iphdr((struct iphdr *)buffer);
            nwrite = write(tun_fd, buffer, nread);
            if (nwrite < 0) {
                perror("write to tun");
                ret_value = EXIT_FAILURE;
                goto quit;
            }
        }
    }

quit:
    if (sock_fd > 0) close(sock_fd);
    if (net_fd > 0) close(net_fd);
    if (tun_fd > 0) close(tun_fd);
    return(ret_value);
}

/*********************************************
 * Function: void sigint(int signum)
 * Desxription: ctrl+c handler
 *********************************************/
void sigint(int signum) {
    // Clean up ......
    if (tun_fd > 0) close(tun_fd);
    if (sock_fd > 0) close(sock_fd);
    if (net_fd > 0) close(net_fd);
    printf("Terminating....\n");
    printf("Totally packets from tun: %ld packets\n", tun_count);
    printf("Totally packets from server: %ld packets\n", nic_count);
    exit(EXIT_SUCCESS);
}
  • 我们实现了一个简单的IP隧道,在这个IP隧道,我们传送一个UDP报文,我们传了一个UDP报文而不是一个TCP报文是为了省去connect()的麻烦;
  • 这样一个IP隧道并不局限在局域网中,通过互联网一样可以建立一个IP隧道;
  • 我们的这个服务器端的程序仅处理了一个客户端的连接,如果我们允许多个客户端接入并建立多条IP隧道,如果连接的多个客户端的tun都绑定在同一个网段上,那么通过服务器显然是可以像局域网一样相互通信的,好像多个终端在一个局域网里一样,是不是有点像VPN,实际上很多vpn就是使用IP隧道实现的;
  • IP隧道还可以用于很多场合,如果你的防火墙不允许某些协议通过,那么你可以通过一个防火墙允许的端口与服务器建立一个IP隧道,然后在这个IP隧道里跑那个不被防火墙允许的协议,就像我们在IP隧道里跑UDP协议一样;
  • 建立隧道也不一定非得使用TCP/IP协议,比如可以使用ICMP协议建立一个ICMP隧道,当你的电脑只能ping通你的服务器,其它的所有协议都无法通过防火墙的情况下,使用ICMP协议建立一个ICMP隧道,然后可以在这个隧道里跑其它协议;
  • 虚拟网络接口的用途很多,现在的虚拟机、容器等大多使用了虚拟网络接口,希望这篇文章可以让你对虚拟网络接口有个初步的认识。

https://blog.whowin.net/post/blog/network/0018-tun-example-for-setting-up-ip-tunnel/