Linux 虛擬網絡設備之 tun-tap

在現在的雲時代,到處都是虛擬機和容器,它們背後的網絡管理都離不開虛擬網絡設備,所以瞭解虛擬網絡設備有利於我們更好的理解雲時代的網絡結構。

虛擬設備和物理設備的區別

在 Linux 網絡數據包的接收過程和數據包的發送過程這兩篇文章中,介紹了數據包的收發流程,知道了 Linux 內核中有一個網絡設備管理層,處於網絡設備驅動和協議棧之間,負責銜接它們之間的數據交互。驅動不需要了解協議棧的細節,協議棧也不需要了解設備驅動的細節。

對於一個網絡設備來說,就像一個管道(pipe)一樣,有兩端,從其中任意一端收到的數據將從另一端發送出去。

比如一個物理網卡 eth0,它的兩端分別是內核協議棧(通過內核網絡設備管理模塊間接的通信)和外面的物理網絡,從物理網絡收到的數據,會轉發給內核協議棧,而應用程序從協議棧發過來的數據將會通過物理網絡發送出去。

那麼對於一個虛擬網絡設備呢?首先它也歸內核的網絡設備管理子系統管理,對於 Linux 內核網絡設備管理模塊來說,虛擬設備和物理設備沒有區別,都是網絡設備,都能配置 IP,從網絡設備來的數據,都會轉發給協議棧,協議棧過來的數據,也會交由網絡設備發送出去,至於是怎麼發送出去的,發到哪裏去,那是設備驅動的事情,跟 Linux 內核就沒關係了,所以說虛擬網絡設備的一端也是協議棧,而另一端是什麼取決於虛擬網絡設備的驅動實現。

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

上圖中有兩個應用程序 A 和 B,都在用戶層,而其它的 socket、協議棧(Newwork Protocol Stack)和網絡設備(eth0 和 tun0)部分都在內核層,其實 socket 是協議棧的一部分,這裏分開來的目的是爲了看的更直觀。

tun0 是一個 Tun/Tap 虛擬設備,從上圖中可以看出它和物理設備 eth0 的差別,它們的一端雖然都連着協議棧,但另一端不一樣,eth0 的另一端是物理網絡,這個物理網絡可能就是一個交換機,而 tun0 的另一端是一個用戶層的程序,協議棧發給 tun0 的數據包能被這個應用程序讀取到,並且應用程序能直接向 tun0 寫數據。

這裏假設 eth0 配置的 IP 是 10.32.0.11,而 tun0 配置的 IP 是 192.168.3.11.

這裏列舉的是一個典型的 tun/tap 設備的應用場景,發到 192.168.3.0/24 網絡的數據通過程序 B 這個隧道,利用 10.32.0.11 發到遠端網絡的 10.33.0.1,再由 10.33.0.1 轉發給相應的設備,從而實現 VPN。

下面來看看數據包的流程:

  1. 應用程序 A 是一個普通的程序,通過 socket A 發送了一個數據包,假設這個數據包的目的 IP 地址是 192.168.3.1

  2. socket 將這個數據包丟給協議棧

  3. 協議棧根據數據包的目的 IP 地址,匹配本地路由規則,知道這個數據包應該由 tun0 出去,於是將數據包交給 tun0

  4. tun0 收到數據包之後,發現另一端被進程 B 打開了,於是將數據包丟給了進程 B

  5. 進程 B 收到數據包之後,做一些跟業務相關的處理,然後構造一個新的數據包,將原來的數據包嵌入在新的數據包中,最後通過 socket B 將數據包轉發出去,這時候新數據包的源地址變成了 eth0 的地址,而目的 IP 地址變成了一個其它的地址,比如是 10.33.0.1.

  6. socket B 將數據包丟給協議棧

  7. 協議棧根據本地路由,發現這個數據包應該要通過 eth0 發送出去,於是將數據包交給 eth0

  8. eth0 通過物理網絡將數據包發送出去

10.33.0.1 收到數據包之後,會打開數據包,讀取裏面的原始數據包,並轉發給本地的 192.168.3.1,然後等收到 192.168.3.1 的應答後,再構造新的應答包,並將原始應答包封裝在裏面,再由原路徑返回給應用程序 B,應用程序 B 取出裏面的原始應答包,最後返回給應用程序 A

這裏不討論 Tun/Tap 設備 tun0 是怎麼和用戶層的進程 B 進行通信的,對於 Linux 內核來說,有很多種辦法來讓內核空間和用戶空間的進程交換數據。

從上面的流程中可以看出,數據包選擇走哪個網絡設備完全由路由表控制,所以如果我們想讓某些網絡流量走應用程序 B 的轉發流程,就需要配置路由表讓這部分數據走 tun0。

tun/tap 設備有什麼用?

從上面介紹過的流程可以看出來,tun/tap 設備的用處是將協議棧中的部分數據包轉發給用戶空間的應用程序,給用戶空間的程序一個處理數據包的機會。於是比較常用的數據壓縮,加密等功能就可以在應用程序 B 裏面做進去,tun/tap 設備最常用的場景是 VPN,包括 tunnel 以及應用層的 IPSec 等,比較有名的項目是 VTun,有興趣可以去了解一下。

tun 和 tap 的區別

用戶層程序通過 tun 設備只能讀寫 IP 數據包,而通過 tap 設備能讀寫鏈路層數據包,類似於普通 socket 和 raw socket 的差別一樣,處理數據包的格式不一樣。

示例

示例程序

這裏寫了一個程序,它收到 tun 設備的數據包之後,只打印出收到了多少字節的數據包,其它的什麼都不做,如何編程請參考後面的參考鏈接。

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

演示

#--------------------------第一個shell窗口----------------------
#將上面的程序保存成tun.c,然後編譯
dev@debian:~$ gcc tun.c -o tun

#啓動tun程序,程序會創建一個新的tun設備,
#程序會阻塞在這裏,等着數據包過來
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

#--------------------------第二個shell窗口----------------------
#啓動抓包程序,抓經過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

#--------------------------第三個shell窗口----------------------
#./tun啓動之後,通過ip link命令就會發現系統多了一個tun設備,
#在我的測試環境中,多出來的設備名稱叫tun1,在你的環境中可能叫tun0
#新的設備沒有ip,我們先給tun1配上IP地址
dev@debian:~$ sudo ip addr add 192.168.3.11/24 dev tun1

#默認情況下,tun1沒有起來,用下面的命令將tun1啓動起來
dev@debian:~$ sudo ip link set tun1 up

#嘗試ping一下192.168.3.0/24網段的IP,
#根據默認路由,該數據包會走tun1設備,
#由於我們的程序中收到數據包後,啥都沒幹,相當於把數據包丟棄了,
#所以這裏的ping根本收不到返回包,
#但在前兩個窗口中可以看到這裏發出去的四個icmp echo請求包,
#說明數據包正確的發送到了應用程序裏面,只是應用程序沒有處理該包
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

結束語

平時我們用到 tun/tap 設備的機會不多,不過由於其結構比較簡單,拿它來了解一下虛擬網絡設備還不錯,爲後續理解 Linux 下更復雜的虛擬網絡設備(比如網橋)做個鋪墊。

原文鏈接:https://segmentfault.com/a/1190000009249039

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/VgC7DiZEyCRamFs_Fix1lw