Linux 虛擬網(wǎng)絡(luò)設(shè)備之 tun/tap
在現(xiàn)在的云時(shí)代,到處都是虛擬機(jī)和容器,它們背后的網(wǎng)絡(luò)管理都離不開虛擬網(wǎng)絡(luò)設(shè)備,所以了解虛擬網(wǎng)絡(luò)設(shè)備有利于我們更好的理解云時(shí)代的網(wǎng)絡(luò)結(jié)構(gòu)。
虛擬設(shè)備和物理設(shè)備的區(qū)別
在Linux網(wǎng)絡(luò)數(shù)據(jù)包的接收過程和數(shù)據(jù)包的發(fā)送過程這兩篇文章中,介紹了數(shù)據(jù)包的收發(fā)流程,知道了Linux內(nèi)核中有一個(gè)網(wǎng)絡(luò)設(shè)備管理層,處于網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)和協(xié)議棧之間,負(fù)責(zé)銜接它們之間的數(shù)據(jù)交互。驅(qū)動(dòng)不需要了解協(xié)議棧的細(xì)節(jié),協(xié)議棧也不需要了解設(shè)備驅(qū)動(dòng)的細(xì)節(jié)。
對(duì)于一個(gè)網(wǎng)絡(luò)設(shè)備來說,就像一個(gè)管道(pipe)一樣,有兩端,從其中任意一端收到的數(shù)據(jù)將從另一端發(fā)送出去。
比如一個(gè)物理網(wǎng)卡eth0,它的兩端分別是內(nèi)核協(xié)議棧(通過內(nèi)核網(wǎng)絡(luò)設(shè)備管理模塊間接的通信)和外面的物理網(wǎng)絡(luò),從物理網(wǎng)絡(luò)收到的數(shù)據(jù),會(huì)轉(zhuǎn)發(fā)給內(nèi)核協(xié)議棧,而應(yīng)用程序從協(xié)議棧發(fā)過來的數(shù)據(jù)將會(huì)通過物理網(wǎng)絡(luò)發(fā)送出去。
那么對(duì)于一個(gè)虛擬網(wǎng)絡(luò)設(shè)備呢?首先它也歸內(nèi)核的網(wǎng)絡(luò)設(shè)備管理子系統(tǒng)管理,對(duì)于Linux內(nèi)核網(wǎng)絡(luò)設(shè)備管理模塊來說,虛擬設(shè)備和物理設(shè)備沒有區(qū)別,都是網(wǎng)絡(luò)設(shè)備,都能配置IP,從網(wǎng)絡(luò)設(shè)備來的數(shù)據(jù),都會(huì)轉(zhuǎn)發(fā)給協(xié)議棧,協(xié)議棧過來的數(shù)據(jù),也會(huì)交由網(wǎng)絡(luò)設(shè)備發(fā)送出去,至于是怎么發(fā)送出去的,發(fā)到哪里去,那是設(shè)備驅(qū)動(dòng)的事情,跟Linux內(nèi)核就沒關(guān)系了,所以說虛擬網(wǎng)絡(luò)設(shè)備的一端也是協(xié)議棧,而另一端是什么取決于虛擬網(wǎng)絡(luò)設(shè)備的驅(qū)動(dòng)實(shí)現(xiàn)。
tun/tap的另一端是什么?
先看圖再說話:
+----------------------------------------------------------------+
| |
| +--------------------+ +--------------------+ |
| | User Application A | | User Application B |<-----+ |
| +--------------------+ +--------------------+ | |
| | 1 | 5 | |
|...............|......................|...................|.....|
| ↓ ↓ | |
| +----------+ +----------+ | |
| | socket A | | socket B | | |
| +----------+ +----------+ | |
| | 2 | 6 | |
|.................|.................|......................|.....|
| ↓ ↓ | |
| +------------------------+ 4 | |
| | Newwork Protocol Stack | | |
| +------------------------+ | |
| | 7 | 3 | |
|................|...................|.....................|.....|
| ↓ ↓ | |
| +----------------+ +----------------+ | |
| | eth0 | | tun0 | | |
| +----------------+ +----------------+ | |
| 10.32.0.11 | | 192.168.3.11 | |
| | 8 +---------------------+ |
| | |
+----------------|-----------------------------------------------+
↓
Physical Network
上圖中有兩個(gè)應(yīng)用程序A和B,都在用戶層,而其它的socket、協(xié)議棧(Newwork Protocol Stack)和網(wǎng)絡(luò)設(shè)備(eth0和tun0)部分都在內(nèi)核層,其實(shí)socket是協(xié)議棧的一部分,這里分開來的目的是為了看的更直觀。
tun0是一個(gè)Tun/Tap虛擬設(shè)備,從上圖中可以看出它和物理設(shè)備eth0的差別,它們的一端雖然都連著協(xié)議棧,但另一端不一樣,eth0的另一端是物理網(wǎng)絡(luò),這個(gè)物理網(wǎng)絡(luò)可能就是一個(gè)交換機(jī),而tun0的另一端是一個(gè)用戶層的程序,協(xié)議棧發(fā)給tun0的數(shù)據(jù)包能被這個(gè)應(yīng)用程序讀取到,并且應(yīng)用程序能直接向tun0寫數(shù)據(jù)。
這里假設(shè)eth0配置的IP是10.32.0.11,而tun0配置的IP是192.168.3.11.
這里列舉的是一個(gè)典型的tun/tap設(shè)備的應(yīng)用場景,發(fā)到192.168.3.0/24網(wǎng)絡(luò)的數(shù)據(jù)通過程序B這個(gè)隧道,利用10.32.0.11發(fā)到遠(yuǎn)端網(wǎng)絡(luò)的10.33.0.1,再由10.33.0.1轉(zhuǎn)發(fā)給相應(yīng)的設(shè)備,從而實(shí)現(xiàn)VPN。
下面來看看數(shù)據(jù)包的流程:
應(yīng)用程序A是一個(gè)普通的程序,通過socket A發(fā)送了一個(gè)數(shù)據(jù)包,假設(shè)這個(gè)數(shù)據(jù)包的目的IP地址是192.168.3.1 socket將這個(gè)數(shù)據(jù)包丟給協(xié)議棧 協(xié)議棧根據(jù)數(shù)據(jù)包的目的IP地址,匹配本地路由規(guī)則,知道這個(gè)數(shù)據(jù)包應(yīng)該由tun0出去,于是將數(shù)據(jù)包交給tun0 tun0收到數(shù)據(jù)包之后,發(fā)現(xiàn)另一端被進(jìn)程B打開了,于是將數(shù)據(jù)包丟給了進(jìn)程B 進(jìn)程B收到數(shù)據(jù)包之后,做一些跟業(yè)務(wù)相關(guān)的處理,然后構(gòu)造一個(gè)新的數(shù)據(jù)包,將原來的數(shù)據(jù)包嵌入在新的數(shù)據(jù)包中,最后通過socket B將數(shù)據(jù)包轉(zhuǎn)發(fā)出去,這時(shí)候新數(shù)據(jù)包的源地址變成了eth0的地址,而目的IP地址變成了一個(gè)其它的地址,比如是10.33.0.1. socket B將數(shù)據(jù)包丟給協(xié)議棧 協(xié)議棧根據(jù)本地路由,發(fā)現(xiàn)這個(gè)數(shù)據(jù)包應(yīng)該要通過eth0發(fā)送出去,于是將數(shù)據(jù)包交給eth0 eth0通過物理網(wǎng)絡(luò)將數(shù)據(jù)包發(fā)送出去
10.33.0.1收到數(shù)據(jù)包之后,會(huì)打開數(shù)據(jù)包,讀取里面的原始數(shù)據(jù)包,并轉(zhuǎn)發(fā)給本地的192.168.3.1,然后等收到192.168.3.1的應(yīng)答后,再構(gòu)造新的應(yīng)答包,并將原始應(yīng)答包封裝在里面,再由原路徑返回給應(yīng)用程序B,應(yīng)用程序B取出里面的原始應(yīng)答包,最后返回給應(yīng)用程序A
這里不討論Tun/Tap設(shè)備tun0是怎么和用戶層的進(jìn)程B進(jìn)行通信的,對(duì)于Linux內(nèi)核來說,有很多種辦法來讓內(nèi)核空間和用戶空間的進(jìn)程交換數(shù)據(jù)。
從上面的流程中可以看出,數(shù)據(jù)包選擇走哪個(gè)網(wǎng)絡(luò)設(shè)備完全由路由表控制,所以如果我們想讓某些網(wǎng)絡(luò)流量走應(yīng)用程序B的轉(zhuǎn)發(fā)流程,就需要配置路由表讓這部分?jǐn)?shù)據(jù)走tun0。
tun/tap設(shè)備有什么用?
從上面介紹過的流程可以看出來,tun/tap設(shè)備的用處是將協(xié)議棧中的部分?jǐn)?shù)據(jù)包轉(zhuǎn)發(fā)給用戶空間的應(yīng)用程序,給用戶空間的程序一個(gè)處理數(shù)據(jù)包的機(jī)會(huì)。于是比較常用的數(shù)據(jù)壓縮,加密等功能就可以在應(yīng)用程序B里面做進(jìn)去,tun/tap設(shè)備最常用的場景是VPN,包括tunnel以及應(yīng)用層的IPSec等,比較有名的項(xiàng)目是VTun,有興趣可以去了解一下。
tun和tap的區(qū)別
用戶層程序通過tun設(shè)備只能讀寫IP數(shù)據(jù)包,而通過tap設(shè)備能讀寫鏈路層數(shù)據(jù)包,類似于普通socket和raw socket的差別一樣,處理數(shù)據(jù)包的格式不一樣。
示例
示例程序
這里寫了一個(gè)程序,它收到tun設(shè)備的數(shù)據(jù)包之后,只打印出收到了多少字節(jié)的數(shù)據(jù)包,其它的什么都不做,如何編程請(qǐng)參考后面的參考鏈接。
#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>
#include<stdlib.h>
#include<stdio.h>
int tun_alloc(int flags)
{
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 ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) {
close(fd);
return err;
}
printf("Open tun/tap device: %s for reading...\n", ifr.ifr_name);
return fd;
}
int main()
{
int tun_fd, nread;
char buffer[1500];
/* Flags: IFF_TUN - TUN device (no Ethernet headers)
* IFF_TAP - TAP device
* IFF_NO_PI - Do not provide packet information
*/
tun_fd = tun_alloc(IFF_TUN | IFF_NO_PI);
if (tun_fd < 0) {
perror("Allocating interface");
exit(1);
}
while (1) {
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);
}
return 0;
}
演示
#--------------------------第一個(gè)shell窗口----------------------
#將上面的程序保存成tun.c,然后編譯
dev@debian:~$ gcc tun.c -o tun
#啟動(dòng)tun程序,程序會(huì)創(chuàng)建一個(gè)新的tun設(shè)備,
#程序會(huì)阻塞在這里,等著數(shù)據(jù)包過來
dev@debian:~$ sudo ./tun
Open tun/tap device tun1 for reading...
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
Read 84 bytes from tun/tap device
#--------------------------第二個(gè)shell窗口----------------------
#啟動(dòng)抓包程序,抓經(jīng)過tun1的包
# tcpdump -i tun1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun1, link-type RAW (Raw IP), capture size 262144 bytes
19:57:13.473101 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 1, length 64
19:57:14.480362 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 2, length 64
19:57:15.488246 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 3, length 64
19:57:16.496241 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 4, length 64
#--------------------------第三個(gè)shell窗口----------------------
#./tun啟動(dòng)之后,通過ip link命令就會(huì)發(fā)現(xiàn)系統(tǒng)多了一個(gè)tun設(shè)備,
#在我的測試環(huán)境中,多出來的設(shè)備名稱叫tun1,在你的環(huán)境中可能叫tun0
#新的設(shè)備沒有ip,我們先給tun1配上IP地址
dev@debian:~$ sudo ip addr add 192.168.3.11/24 dev tun1
#默認(rèn)情況下,tun1沒有起來,用下面的命令將tun1啟動(dòng)起來
dev@debian:~$ sudo ip link set tun1 up
#嘗試ping一下192.168.3.0/24網(wǎng)段的IP,
#根據(jù)默認(rèn)路由,該數(shù)據(jù)包會(huì)走tun1設(shè)備,
#由于我們的程序中收到數(shù)據(jù)包后,啥都沒干,相當(dāng)于把數(shù)據(jù)包丟棄了,
#所以這里的ping根本收不到返回包,
#但在前兩個(gè)窗口中可以看到這里發(fā)出去的四個(gè)icmp echo請(qǐng)求包,
#說明數(shù)據(jù)包正確的發(fā)送到了應(yīng)用程序里面,只是應(yīng)用程序沒有處理該包
dev@debian:~$ ping -c 4 192.168.3.12
PING 192.168.3.12 (192.168.3.12) 56(84) bytes of data.
--- 192.168.3.12 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3023ms
結(jié)束語
平時(shí)我們用到tun/tap設(shè)備的機(jī)會(huì)不多,不過由于其結(jié)構(gòu)比較簡單,拿它來了解一下虛擬網(wǎng)絡(luò)設(shè)備還不錯(cuò),為后續(xù)理解Linux下更復(fù)雜的虛擬網(wǎng)絡(luò)設(shè)備(比如網(wǎng)橋)做個(gè)鋪墊。
原文鏈接:https://segmentfault.com/a/1190000009249039
