硬貨 | 如何用 tap/tun 設備編寫一個 ICMP 程序

2021-02-25 Linux雲計算網絡

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

01 準備工作

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

[root@by ~]
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 驅動是默認編譯進內核中的。

如果你的系統不支持,請先選擇手動編譯內核或者升級內核。編譯時開啟下面的選項即可:

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

tap/tun 也支持編譯成模塊,如果編譯成模塊,需要手動加載它:

[root@localhost ~]
[root@localhost ~]
tun                    31665  0

關於以上的詳細步驟,網上有很多教程,這裡就不再贅述了。

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

mknod /dev/net/tun 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;
    }

    
    
    strcpy(dev, ifr.ifr_name);

    return fd;
}

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

還是有一個屬性是 ifr_name,表示設備的名字,它可以由用戶自己指定,也可以由系統自動分配,比如 tapX、tunX,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';

    
    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]
[root@localhost coding]
Open tun/tap device: tun0 for reading...

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

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

給 tun0 接口配置 IP 並啟用,比如 10.1.1.2/24。

[root@localhost ~]
[root@localhost ~]

再開一個終端,用 tcpdump 抓 tun0 的包。

[root@localhost ~]

然後在第二個終端 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


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>,以下是完整代碼。



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

    
    
    strcpy(dev, ifr.ifr_name);

    return fd;
}

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

    tun_name[0] = '\0';

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

5T 技術資源大放送!包括但不限於:雲計算、虛擬化、微服務、大數據、網絡、Docker容器、Kubernetes、Linux、Python、Go、C/C++、Shell、等等。在公眾號內回復「1024」,即可免費獲取!!

相關焦點

  • 所謂設備驅動即驅使硬體設備行動,帶你深入理解linux的設備驅動...
    191 = /dev/vcsa63 tty63 的文本/屬性內容代碼: 7 block 迴環設備(用一個普通的磁碟文件來模擬一個塊設備)對迴環設備的綁定由 mount(8) 或 losetup(8) 處理0 = /dev/loop0 第1個迴環設備1 = /dev/loop1 第2個迴環設備...
  • 內網滲透系列:內網隧道之ICMP隧道
    其中type和code是關鍵:2、鸚鵡學舌一樣返回回送回答計算機送出的回送請求到達目標伺服器後,伺服器回答這一請求,向送信方發送回送請求(類型是0,代碼是0)(同3)。這個ICMP 回送回答報文在IP 層來看,與被送來的回送請求報文基本上一樣。不同的只是,源和目標IP 地址欄位被交換了,類型欄位裡填入了表示回送回答的0。
  • 如何用 systemtap 排查問題
    有兩篇文章現在還記得,《Linux下如何知道文件被哪個進程寫》[1]和《巧用Systemtap注入延遲模擬IO設備抖動》[2],周末突然想起來,發現能看懂了:)本文雖然說是小技巧,可是難度一點也不低 ^_^什麼是 systemtapSystemtap is a tool that allows
  • Linux驅動實踐:如何編寫【 GPIO 】設備的驅動程序?
    在前幾篇文章中,我們一塊討論了:在 Linux 系統中,編寫字符設備驅動程序的基本框架,主要是從代碼流程和 API 函數這兩方面觸發。這篇文章,我們就以此為基礎,寫一個有實際應用功能的驅動程序:在驅動程序中,初始化 GPIO 設備,自動創建設備節點;在應用程式中,打開 GPIO 設備,並發送控制指令設置 GPIO 口的狀態;示例程序目標 編寫一個驅動程序模塊:mygpio.ko。
  • 戰術單位如何編寫自己的標準作業程序(SOP)
    如何才能讓寶貴的經驗在一個單位得到較好的傳承?SOP無疑是一個重要的法寶!本文重點介紹了將標準作業程序開發成使用指南的一般過程,並簡要討論了如何運用軍隊寫作標準來編寫有效的書面資料。下文將重點介紹美陸軍將標準作業程序開發成使用指南的一般過程,並將簡要討論如何運用軍隊寫作標準來編寫有效的書面資料。一個作業程序是指一個已被批准的過程,這個過程是用來完成一個複雜的、重複性任務的。程序由一系列詳細的步驟(或子任務)組成,執行這些步驟可以確保實現預期的結果。一套標準作業程序(SOP)指南可以幫助使用者更好地執行作業程序。
  • 內核探測工具systemtap簡介
    這種做法對於內核開發人員簡直是夢魘,因為一遍做下來至少得需要1個多小時,不僅破壞了原有內核代碼,而且如果換了一個需求又得重新做一遍上面的工作。所以,這種調試內核的方法效率是極其底下的。之後內核引入了一種Kprobe機制,可以用來動態地收集調試和性能信息的工具,是一種非破壞性的工具,用戶可以用它跟蹤運行中內核任何函數或執行的指令等。
  • 內核調試神器SystemTap — 簡介與使用(一)
    (1) stap通常直接使用stap執行用SystemTap語法編寫的腳本即可。stap - systemtap script translator/driverstap test.stp // .stp後綴的文件是用SystemTap語法編寫的腳本腳本主要元素:probe point + probe handlerstap [options] FILE // Run script in
  • QEMU安裝與磊科路由器igdmptd後門程序復現教程
    作為機器模擬器,它能夠在某一架構之上運行為另一架構而編寫的程序,比如在PC上分析一些嵌入式設備的固件。作為虛擬器,它能夠直接在宿主機CPU上執行代碼。磊科路由器後門事件的話可以看看這篇文章:http://www.freebuf.com/articles/terminal/55300.html。雖然有點久了,但還算經典,值得一試。
  • 如何編寫完美的 Python 命令行程序?
    這篇文章將教你如何編寫完美的 Python 命令行程序,提高團隊的生產力
  • 如何用C編寫一個打怪小遊戲(二)
    如果要戰鬥的話,我們要知道敵我雙方的屬性,如血量,攻擊,防禦等,這裡可以先聲明一個結構體:----Struct 屬性{Int 血量;Int 血量上限;Int 攻擊;Int 防禦;Int 攻擊距離;Int 移速;Int 閃避率;Int
  • 如何編寫完美的 Python 命令行程序?
    這篇文章將教你如何編寫完美的 Python 命令行程序,提高團隊的生產力,讓大家的工作更舒適。作為 Python 開發者,我們經常要編寫命令行程序。比如在我的數據科學項目中,我要從命令行運行腳本來訓練模型,以及計算算法的準確率等。
  • 詳細解讀用C語言編寫的 「掃雷」程序
    用C語言編寫的掃雷程序編寫前首先得有大致的思路吧,就是第一步幹啥第二部幹啥
  • 內核調試技巧 - systemtap定位丟包原因
    在內核裡面,數據包對應一個叫做skb(sk_buff結構)。當發生如上等原因丟包時,內核會調用***kfree_skb***把這個包釋放(丟掉)。kfree_skb函數中已經埋下了trace點,並且通過__builtin_return_address(0)記錄下了調用kfree_skb的函數地址並傳給location參數,因此可以利用systemtap kernel.trace來跟蹤kfree_skb獲取丟包函數。考慮到該丟包函數可能調用了子函數,子函數繼續調用子子函數,如此遞歸。
  • 用Systemtap探索MySQL
    於是裝好systemtap-sdt-dev包,再進行cmake就可以了. 不需要額外的配置選項,也不需要開啟WITH_DEBUG。(說明:按照手冊,systemtap-sdt-dev中的dtrace用於將file.d文件轉成頭文件參與編譯,並非真正的dtrace)> apt-get install -sdt-dev> cmake -DBUILD_CONFIG=mysql_release -DDOWNLOAD_BOOST=1 -DDOWNLOAD_BOOST_TIMEOUT=3600
  • 用 Webhook+Python+Shell 編寫一套 Unix 類系統監控工具
    ,一個進程是程序運行時的內存空間和設置。輸入輸出重定向鍵盤稱為標準輸入設備,顯示器稱為標準輸出設備在 Shell 中,不使用系統的標準輸入、輸出設備而重新指定其輸入輸出的方法稱為輸入輸出重定向。什麼時候需要使用重定向?
  • 工埔教育|實例講解,如何規範編寫PLC程序
    DP從站必須做故障診斷,故障報警,用FB125即可。一、控制模式1、系統設置遠程/本地/手動按鈕1.1、遠程:只能通過上位機對系統進行自動啟/停控制,單臺設備就地控制優先,在程控時,可以通過上位機對設備進行軟手操/自動切換,軟手操啟/停;1.2、本地:只能通過觸控螢幕對系統進行自動啟/停控制,單臺設備就地控制優先,在程控時,可以通過觸控螢幕對設備進行軟手操/
  • 網絡 攻 擊 肆虐-給你的網絡設備來套防彈衣吧!
    有本事咱就給它套上一個金鐘罩鐵布衫,刀槍不入。網絡設備安全加固怎麼做?常見的做法:我相信很多朋友腦海裡面首先浮現的是採用類似ACL訪問控制列表的方法,只允許信任的管理員訪問管理設備,限制特定的協議等。路由器或交換機接口配置ACL允許特定流量。限制特定網段使用SSH、HTTPS等訪問設備。這就夠了麼?1.如何限制網絡設備的帶內帶外安全?