利用 Linux tap/tun 虛擬設備寫一個 ICMP echo 程序

本文首發於個人公衆號 Linux雲計算網絡(id: cloud_dev),專一於乾貨分享,號內有 10T 書籍和視頻資源,後臺回覆「1024」便可領取,歡迎你們關注,二維碼文末能夠掃。linux

前面兩篇文章已經介紹過 tap/tun 的原理和配置工具。這篇文章經過一個編程示例來深刻了解 tap/tun 的程序結構。編程

01 準備工做

首先經過 modinfo tun 查看系統內核是否支持 tap/tun 設備驅動。網絡

[root@by ~]# modinfo tun
filename:       /lib/modules/3.10.0-862.14.4.el7.x86_64/kernel/drivers/net/tun.ko.xz
alias:          devname:net/tun
alias:          char-major-10-200
license:        GPL
author:         (C) 1999-2004 Max Krasnyansky <maxk@qualcomm.com>
description:    Universal TUN/TAP device driver
retpoline:      Y
rhelversion:    7.5
srcversion:     50878D5D5A0138445B25AA8
depends:
intree:         Y
vermagic:       3.10.0-862.14.4.el7.x86_64 SMP mod_unload modversions
signer:         CentOS Linux kernel signing key
sig_key:        E4:A1:B6:8F:46:8A:CA:5C:22:84:50:53:18:FD:9D:AD:72:4B:13:03
sig_hashalgo:   sha256

在 linux 2.4 及以後的內核版本中,tun/tap 驅動是默認編譯進內核中的。tcp

若是你的系統不支持,請先選擇手動編譯內核或者升級內核。編譯時開啓下面的選項便可:ide

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

tap/tun 也支持編譯成模塊,若是編譯成模塊,須要手動加載它:函數

[root@localhost ~]# modprobe tun
[root@localhost ~]# lsmod | grep tun
tun                    31665  0

關於以上的詳細步驟,網上有不少教程,這裏就再也不贅述了。工具

https://blog.csdn.net/lishuhuakai/article/details/70305543oop

上面只是加載了 tap/tun 模塊,要完成 tap/tun 的編碼,還須要有設備文件,運行命令:測試

mknod /dev/net/tun c 10 200 # c表示爲字符設備,10和200分別是主設備號和次設備號

這樣在 /dev/net 下就建立了一個名爲 tun 的文件。編碼

02 編程示例

2.1 啓動設備

使用 tap/tun 設備,須要先進行一些初始化工做,以下代碼所示:

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;
}

首先打開字符設備文件 /dev/net/tun,而後用 ioctl 註冊設備的工做模式,是 tap 仍是 tun。這個模式由結構體 struct ifreq 的屬性 ifr_flags 來定義,它有如下表示:

  • IFF_TUN: 表示建立一個 tun 設備。
  • IFF_TAP: 表示建立一個 tap 設備。
  • IFF_NO_PI: 表示不包含包頭信息,默認的,每一個數據包傳到用戶空間時,都會包含一個附加的包頭來保存包信息,這個表示不加包頭。
  • IFF_ONE_QUEUE:表示採用單一隊列模式。

仍是有一個屬性是 ifr_name,表示設備的名字,它能夠由用戶本身指定,也能夠由系統自動分配,好比 tapXtunX,X 從 0 開始編號。

ioctl 完了以後,文件描述符 fd 就和設備創建起了關聯,以後就能夠根據 fd 進行 read 和 write 操做了。

2.2 寫一個 ICMP 的調用函數

爲了測試上面的程序,咱們寫一個簡單的 ICMP echo 程序。咱們會使用 tun 設備,而後給 tunX 接口發送一個 ping 包,程序簡單響應這個包,完成 ICMP 的 request 和 reply 的功能。

以下代碼所示:

int main()
{
    int tun_fd, nread;
    char buffer[4096];
    char tun_name[IFNAMSIZ];

    tun_name[0] = '\0';

    /* 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 for reading...\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, that's %s\n", nread, buffer);
    }
    return 0;
}

下面測試一下。

2.3 給 tap/tun 設備配置 IP 地址

編譯:

[root@localhost coding]# gcc -o taptun taptun.c
[root@localhost coding]# ./taptun
Open tun/tap device: tun0 for reading...

開另外一個終端,查看生成了 tun0 接口:

[root@localhost coding]# ip a
6: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN qlen 500
    link/none

tun0 接口配置 IP 並啓用,好比 10.1.1.2/24

[root@localhost ~]# ip a a 10.1.1.2/24 dev tun0
[root@localhost ~]# ip l s tun0 up

再開一個終端,用 tcpdumptun0 的包。

[root@localhost ~]# tcpdump -nnt -i tun0

而後在第二個終端 ping 一下 10.1.1.0/24 網段的 IP,好比 10.1.1.3,看到:

[root@localhost ~]# ping -c 4 10.1.1.3
PING 10.1.1.3 (10.1.1.3) 56(84) bytes of data.
64 bytes from 10.1.1.3: icmp_seq=1 ttl=64 time=0.133 ms
64 bytes from 10.1.1.3: icmp_seq=2 ttl=64 time=0.188 ms
64 bytes from 10.1.1.3: icmp_seq=3 ttl=64 time=0.092 ms
64 bytes from 10.1.1.3: icmp_seq=4 ttl=64 time=0.110 ms

--- 10.1.1.3 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3290ms
rtt min/avg/max/mdev = 0.092/0.130/0.188/0.038 ms

因爲 tun0 接口建好以後,會生成一條到本網段 10.1.1.0/24 的默認路由,根據默認路由,數據包會走 tun0 口,因此能 ping 通,能夠用 route -n 查看。

再看 tcpdump 抓包終端,成功顯示 ICMP 的 request 包和 reply 包。

[root@localhost ~]# tcpdump -nnt -i tun0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
IP 10.1.1.2 > 10.1.1.3: ICMP echo request, id 3250, seq 1, length 64
IP 10.1.1.3 > 10.1.1.2: ICMP echo reply, id 3250, seq 1, length 64
IP 10.1.1.2 > 10.1.1.3: ICMP echo request, id 3250, seq 2, length 64
IP 10.1.1.3 > 10.1.1.2: ICMP echo reply, id 3250, seq 2, length 64

再看程序 taptun.c 的輸出:

[root@localhost coding]# ./taptun
Open tun/tap device: tun0 for reading...
Read 48 bytes from tun/tap device
Write 48 bytes to tun/tap device
Read 48 bytes from tun/tap device
Write 48 bytes to tun/tap device

ok,以上便驗證了程序的正確性。

03 總結

經過這個小例子,讓咱們知道了基於 tap/tun 編程的流程,對 tap/tun 又加深了一層理解。

使用 tap/tun 設備須要包含頭文件 #include <linux/if_tun.h>,如下是完整代碼。

/******************************************************************************
 *  File Name: taptun.c
 *  Author: 公衆號: CloudDeveloper
 *  Created Time: 2019年02月23日 星期六 21時28分24秒
 *****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#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>

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];

    tun_name[0] = '\0';

    /* 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 for reading...\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, that's %s\n", nread, buffer);
    }
    return 0;
}

個人公衆號 「Linux雲計算網絡」(id: cloud_dev) ,號內有 10T 書籍和視頻資源,後臺回覆 「1024」 便可領取,分享的內容包括但不限於 Linux、網絡、雲計算虛擬化、容器Docker、OpenStack、Kubernetes、工具、SDN、OVS、DPDK、Go、Python、C/C++編程技術等內容,歡迎你們關注。

相關文章
相關標籤/搜索