內核-用戶空間鴻溝
構建自己的 TCP棧是一項極具挑戰的任務。通常,當用戶空間應用程序需要互聯網連接時,它們會調用操作系統內核提供的高級 API。這些 API 幫助應用程序 連接網絡創建、發送和接收數據,從而消除了直接處理原始數據包的復雜性。這是開發標準應用程序的絕佳選擇。
然而,當您打算構建自定義 TCP 棧時,事情就會變得棘手。為了實現自定義TCP 棧,您不僅僅是網絡服務的消費者,還必須是管理者、處理者和調度者。這意味著需要直接與原始網絡數據包交互,并處理它們,然后將它們發送到各自的目的地。本質上,您必須繞過操作系統的內置 TCP 棧,才能在用戶空間 TCP 棧中直接接收和處理來自網絡的原始數據包。
為了能夠實現[在用戶空間]處理原始網絡數據包,需要設置一個虛擬網絡接口。虛擬網絡接口將“欺騙”內核將傳入數據包直接傳遞給它,就像物理 NIC(網絡接口卡)一樣,但內核不會干預原始數據包處理。對于這個小技巧,我們將使用 Linux TUN/TAP 設備驅動程序,專注于 TUN(網絡)來啟動我們的虛擬網絡接口。
從本質上講,TUN 設備是一個存在于操作系統內核中的基于軟件的[虛擬]網絡接口。該虛擬網絡接口的行為與物理網絡接口非常相似,但它不依賴于物理硬件。 TUN 設備在 OSI 模型的第 3 層運行,并向任何需要發送或接收數據包的應用程序公開文件描述符。
一旦啟動并運行了 TUN 設備,任何針對其 關聯 IP 地址的數據包都將被內核重定向(內核不過問任何問題,不處理任何數據包),直接進入已將自身綁定到的用戶空間應用程序的懷抱中的TUN 設備。這種設置為我們提供了全權委托,我們可以隨心所欲地處理原始數據包。
linux Tun/Tap 原理 ,注意tun0 直接把數據包轉發給了 User Application B,這樣User Application B 就會接收到原始數據
數據包處理工作流程:TUN 設備與標準網絡堆棧
Step | With TUN Device TUN | Without TUN Device |
---|---|---|
1 | Packet arrives at physical NIC. 數據包到達物理網卡。 | Packet arrives at physical NIC. 數據包到達物理網卡。 |
2 | Kernel’s routing sends packet to TUN. 內核的路由將數據包發送到TUN。 | Kernel’s network stack processes packet. 內核的網絡堆棧處理數據包。 |
3 | Packet forwarded to TUN device. 數據包轉發到 TUN 設備。 | Packet may be filtered, NAT’d, etc. 數據包可能會被過濾、NAT 等。 |
4 | User-space app reads packet from TUN. 用戶空間應用程序從 TUN 讀取數據包。 | OS passes packet to appropriate socket 操作系統將數據包傳遞到適當的套接字 |
5 | User-space stack processes packet. 用戶空間堆棧處理數據包。 | Application reads packet from socket. 應用程序從套接字讀取數據包。 |
6 | Optional: User-space modifies packet. 可選:用戶空間修改數據包。 | N/A 不適用 |
7 | Optional: Packet sent out via TUN. 可選:通過 TUN 發送的數據包。 | N/A 不適用 |
8 | Kernel routes the outgoing packet. 內核路由傳出數據包。 | Kernel routes the outgoing packet. 內核路由傳出數據包。 |
-
With a TUN Device:
TUN 設備:內核執行的工作最少。它將數據包轉發到 TUN 設備,允許用戶空間應用程序(我們的 TCP 應用程序)處理大部分數據包處理,包括可選的修改和潛在的重傳。 -
Without a TUN Device:
非TUN 設備:內核自己的網絡堆棧完全處理數據包,包括任何路由、過濾和 NAT 操作。應用程序只是從套接字讀取數據包,從底層細節中抽象出來。
現在理解了為什么需要使用 TUN 設備,可以開始編寫一些代碼了。可以通過運行創建一個全新的 Rust 項目。
cargo new blah blaj
我們將使用 TunTap
crate ,它是 Tun/Tap 驅動程序的 Rust 包裝。要將其添加到項目中,只需將以下行添加到 Cargo.Toml 文件中。
tun-tap = "0.1.4"
use std::io;fn main() -> io::Result<()> {// Create a new TUN interface named "tun0" in TUN mode.let nic = tun_tap::Iface::new("tune", tun_tap::Mode::Tun)?;// Define a buffer of size 1504 bytes (maximum Ethernet frame size without CRC) to store received data.let mut buf = [0u8; 1504];// Main loop to continuously receive data from the interface.loop {// Receive data from the TUN interface and store the number of bytes received in `nbytes`.let nbytes = nic.recv(&mut buf[..])?;eprintln!("read {} bytes: {:x?}", nbytes, &buf[..nbytes]);}Ok(())
}
使用 cargo b --release
構建二進制文件后,需要提升編譯后的二進制文件的權限。通過運行 sudo setcap cap_net_admin=eip ./target/release/tcp
來實現這一點。這授予二進制文件操作網絡接口和路由表所需的權限。
一旦執行了二進制文件,就會創建一個名為“tun0”的新虛擬網絡接口。為了驗證它的存在,可以運行 ip addr
,它應該顯示所在機器上所有網絡接口的列表,包括新創建的“tun0”,它通常位于列表的底部,如下所示。
4: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 500
link/none
但要注意,此時它沒有 IP 地址。所以不能向它發送任何數據包。為了解決這個問題,可以執行 sudo ip addr add 192.168.0.1/24 dev tun0
,給 創建的名為 tun0
的網絡接口 分配 IP 地址 192.168.0.1
和子網掩碼 255.255.255.0
(由 /24
表示) )。然后可以再次運行 ip addr
命令進行確認,將看到如下所示的輸出,確認虛擬網絡接口現在已附加一個 IP 地址,現在可以 ping 并向其發送數據包。
4: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500
qdisc noop state DOWN group default qlen 500
link/none
inet 192.168.0.1/24 scope global tun0
valid_lft forever preferred_lft forever
接下來,通過執行 sudo ip link set up dev tun0
激活網絡接口。
現在我們已經準備好進行測試了。如果您還記得,之前我們說過將在內核發送給用戶空間程序中處理原始網絡數據包,那么現在看看它的實際情況。繼續運行以下命令來 ping 虛擬網絡接口或其中的任何子網。 [同時二進制文件仍在執行]
ping - I tun0 192.168.0.2
您會注意到應用程序收到了一些原始字節。像下面這樣的東西。這挺令人興奮。
[0, 0, 86, dd, 60, 0, 0, 0, 0, 8, 3a, ff, fe, 80, 0, 0, 0, 0, 0, 0, 15, 62, d0, a2, 5c, 4e, c2, 45, ff, 2, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 2, 85, 0, 78, 9e, 0, 0, 0, 0]]
輔助腳本
為了方便操作,可以編寫整個過程的腳本:
#!/bin/bash
cargo b --release
sudo setcap cap_net_admin=eip ./target/release/tcp
./target/release/tcp &
pid=$1
sudo ip addr add 192.168.0.1/24 dev tun0
trap "kill $pid" INT TERM
wait $pid
參考
- Virtual networking 101: Bridging the gap to understanding TAP
虛擬網絡 101:彌合理解 TAP 的差距 - Corresponding Code 對應代碼
原文地址