前面兩篇文章已經介紹過 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」,即可免費獲取!!