TUN 虚拟网卡:IP 包的进出口
专栏:VNT 2.0 组网源码 · 第 4 / 11 篇:::info 学习目标 完成本篇后你能够:解释 TUN 设备的工作原理与读写循环设计;说出 TAP 模式的以太网帧处理与 ARP 代答;理解 TaskGroup 的”任一退出即全停”语义。 前置:第 3 篇完成。预计时长:55 分钟。 :::
:::note 本章术语速查(新手建议先读)
- fd(文件描述符):Linux 里“一切皆文件”——网卡、socket 都是数字编号的 fd。Android 的 VpnService 会给一个 tunFd。
- 零拷贝:数据不加复制地在内存里“挪指针”,省 CPU。
- MTU:一个包的最大字节数。VNT 默认 1380(给封装头部留余量)。
- ARP:局域网里“谁有这个 IP?把你的 MAC 给我”的广播协议。
- VLAN:在一根网线里跑多个隔离网络的标记技术。 :::
TUN 是内核提供的一种虚拟网络设备:用户态程序从它读出的是完整的 IP 包(TAP 则是以太网帧)。VNT 用 tun-rs crate 的异步接口管理它(vnt-core/src/tun/)。
设备创建:平台差异全在这里
// vnt-core/src/tun/general.rs:140-204(节选)
DeviceBuilder::new()
.device_name(...) // 可自定义网卡名
.mtu(mtu)
.layer(if tap { Layer::L2 } else { Layer::L3 }) // TAP=二层 / TUN=三层
...
.offload(true) // Linux: 开启 offload 提性能
.metric(1) // Windows: 路由度量
平台细节都在这里消化:Windows 用 wintun 驱动(启动时解压 DLL)、Linux 开 offload 并设 tx 队列 1000、Android/iOS 传入 VpnService 的 fd(AsyncDevice::from_fd)——这就是 vnt-jni 能在手机上跑的原因。TAP 模式还会设置 MAC:02:00:IP前4字节——MAC 与虚拟 IP 可以互算(第 6 篇的 ARP 代答靠它)。
读写循环:两个方向、零拷贝加头
// vnt-core/src/tun/general.rs:254-287
async fn in_device_loop(
mut receiver: Receiver<TransmissionBytes>,
mut device_framed_write: DeviceFramedWrite<BytesCodec, Arc<AsyncDevice>>,
) -> anyhow::Result<()> {
while let Some(data) = receiver.recv().await {
device_framed_write.send(data).await?; // 写向网卡:发给内核
}
Ok(())
}
async fn out_device_loop(
mut device_framed_read: DeviceFramedRead<BytesCodec, Arc<AsyncDevice>>,
...
) -> anyhow::Result<()> {
while let Some(rs) = device_framed_read.next().await {
let bytes_mut = rs?;
if device_mode == DeviceMode::Tap {
if let Some(reply) = enhanced_outbound.ethernet_outbound(bytes_mut).await {
device.send(reply.as_ref()).await?; // TAP: ARP 代答回包直接写回
}
} else {
enhanced_outbound.ipv4_outbound(bytes_mut).await; // TUN: 交给数据面
}
}
Ok(())
}
一个精妙的性能细节:读出缓冲区时预留 16 字节头部空间(自定义 BytesCodec),后续出站流水线加 NetPacket 头时用 retreat_head 原地退指针——零拷贝加头。两个方向任一退出都会 spawn_stop_all 停掉整个 TaskGroup——网卡写不进去的进程不该继续假装活着。
读写循环全景
三种入站出口
从 TUN 读出的包去向由设备模式决定:
| 模式 | 出站处理 | 说明 |
|---|---|---|
| tun | ipv4_outbound | 交给数据面流水线(第 7 篇) |
| tap | ethernet_outbound | 先解析以太网帧;网关的 ARP 请求直接本地代答(回应网关 MAC=02:00);IPv4 部分再走通用出站 |
| no | internal_nat | 不建网卡;内置用户态 ICMP/TCP/UDP NAT 把指定端口的流量转进虚拟网 |
TAP 的 ARP 代答回答了一个经典问题:“ping 网关为什么能通?“——网关是虚拟的,VNT 亲自冒充它回答 ARP。
回程过滤:哪些包配写进 TUN
入站方向的 TunDataInbound.send_ip 有白名单:目的地址必须属于虚拟网段、广播、组播、或 input 参数允许的子网,否则丢弃。这是安全边界——没有它,任何对端都能把任意包注入你的内核。TAP 模式写回前还要 wrap_ipv4 补以太网头。
与其他模块的联系
- TaskGroup(utils/task_control.rs):TUN 读写、打洞、心跳同属一组——任一关键任务退出即全停,配合优雅退出;
- 数据面(第 7 篇):
out_device_loop交出的每个包走EnhancedOutbound → HybridOutbound流水线; - 子网路由(第 6 篇的 input/output):决定”哪些目的地址的包能进 TUN、哪些虚拟外地址可转发”。
常见踩坑
- Windows 提示无权限——建网卡需要管理员;
no模式则不需要; - MTU 不匹配——默认 1380(为隧道封装留余量),某些网络下需要手动调;
- TAP 模式下设备不通——检查虚拟 MAC 是否符合
02:00:IP规则(有的系统会改 MAC)。
随堂练习(带验收标准)
- 组网成功后
ip link show(Linux)找到 VNT 网卡,确认 IP 与 MTU=1380; - 在 TUN 模式抓 VNT 网卡的包(tcpdump -i <网卡名>),对比物理网卡的包——物理网卡上你应该只能看到加密后的 UDP;
- 进阶:读
tun/general.rs的 BytesCodec,解释预留 16 字节如何实现零拷贝加头。