内核-用户空间鸿沟
构建自己的 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 对应代码
原文地址