深入理解 TUN 设备工作机制
坑边闲话:笔者用了好几年 sing-box / Clash,但真正理解 tun0 的 .1/.2 是什么、内核怎么把包"塞"给用户态进程,其实并不容易。这篇文章试图补上这块认知。
1. 为什么需要 TUN·
1.1 SOCKS / HTTP proxy 的边界·
SOCKS5 / HTTP proxy 是应用层协议。它们要求应用程序主动感知代理的存在,通过特定协议与代理协商连接目标。这带来两个根本限制:
- 应用必须支持代理协议。大多数命令行工具(ping、nslookup、某些 CLI)不支持,或者支持方式各异。
- 无法拦截不经过 socket API 的流量。虽然理论上大多数流量都走 socket,但你无法在内核层面统一接管所有出站连接。
更关键的是:SOCKS proxy 工作在 L4(TCP/UDP),它看到的是流(stream),不是 IP 包。它没有办法看到完整的 L3 数据包头,也就无法做基于 IP 头的策略路由。
1.2 透明代理需要内核配合·
"透明代理"的语义是:应用程序不需要知道代理的存在,流量自动被拦截和转发。要实现这一点,必须在内核网络栈层面介入。
Linux 的传统方案是 iptables/nftables REDIRECT/TPROXY,可以把 TCP/UDP 流量重定向到本地监听端口。但这依然有局限:只能处理 TCP/UDP,对 ICMP、原始 IP 协议号支持有限;而且 TPROXY 配置复杂。
TUN 的方案更彻底:在 L3 层面创建一个虚拟网卡,让内核把需要代理的流量路由到这张虚拟网卡,然后用户态程序从这张网卡读取原始 IP 数据包。
1.3 两层路由的分工·
这是理解整个系统的关键结论:
1 | Linux routing 决定「哪些流量交给 sing-box」 |
Linux 内核路由(ip route / ip rule)负责流量分类和导流:把需要代理的流量打上标记,通过策略路由送入 tun0.
sing-box 用户态路由负责决策:这个目标地址应该走 vless?走 direct?走 shadowsocks?它拿到的是原始 IP 包,可以自由决定如何处理。
2. TUN 是什么·
2.1 一个被广泛误解的设备·
常见的误解是把 TUN 理解为一个 pipe 或 socket——一端是内核,另一端是用户态程序,数据从一端流向另一端。这个模型在直觉上没错,但它遗漏了最重要的部分:
TUN 不是简单的 pipe,而是一个 L3 虚拟网卡(net_device)+ 用户态数据通道。
2.2 tun0 是 net_device·
tun0 在内核里是一个标准的 struct net_device,和 eth0、lo 是同等地位的网络设备。它:
- 有 IP 地址(可以是 /30 的点对点地址对,也可以是 /24 等)
- 注册在内核网络子系统中
- 可以参与
ip route、ip rule策略路由 - 可以被 iptables/nftables 的
-i tun0匹配到
1 | ip link show tun0 |
注意 link/none——这是 L3 接口的标志。
2.3 没有二层·
TUN 是纯 L3 接口:
- 没有 MAC 地址
- 没有 ARP
- 不参与以太网帧的封装/解封
link/none而不是link/ether
这与 TAP 设备的本质区别在于:TAP 模拟的是完整的以太网卡(L2),TUN 只模拟 IP 层设备(L3)。
从内核网络栈的角度看,发往 tun0 的流量在 L3 被「发出」,不经过 L2 帧封装,直接进入 TUN driver 的发送队列。
3. tun0 和「对端」的工作机制·
3.1 point-to-point 地址对的含义·
sing-box 启动时会创建 tun0 并配置类似这样的地址:
1 | tun0: inet 172.18.0.1 peer 172.18.0.2/32 |
这是一个 point-to-point(点对点)虚拟链路的标准配置方式:
1 | +---------------------------+ +-----------------------------+ |
.1 是内核侧地址,.2 是"对端"地址。这个对端不是网络中真实存在的机器,它只是一个逻辑标签,用于告诉内核"通过这条链路可以到达 .2"。
3.2 为什么路由要写 via 172.18.0.2·
sing-box 创建 tun0 后,会添加类似这样的路由:
1 | ip route add default via 172.18.0.2 dev tun0 table 2022 |
或者:
1 | ip route add 0.0.0.0/0 dev tun0 |
内核在查找 “如何到达 8.8.8.8” 时,命中这条路由:下一跳是 172.18.0.2,出口设备是 tun0。
由于 tun0 是 point-to-point 接口,172.18.0.2 就是链路对端,内核不需要 ARP,直接把 IP 包送入 TUN driver 的发送路径。
1 | 内核路由决策: |
这里 172.18.0.2 本质是一个"路由锚点",让内核知道该把包交给哪个接口。真正的传输靠的是 TUN driver,不是二层可达性。
4. 数据流全过程·
4.1 出站方向:应用 → sing-box·
1 | 用户应用程序 (curl, browser, ...) |
4.2 入站方向:远端响应 → 应用·
1 | 远端服务器响应包 |
4.3 策略路由的角色·
sing-box 通常配置如下策略路由(以 fwmark 方案为例):
1 | # 所有出站包打 fwmark 0x1 |
这里的关键设计是避免路由环路:sing-box 自己通过真实网卡发出的加密流量,不能再被拦截进 tun0, 否则会无限循环。通常通过 fwmark 豁免来解决。
5. TUN 如何"通知"用户态·
5.1 没有中断,靠什么唤醒·
常见问题:TUN 是软件虚拟设备,没有硬件中断,没有 DMA. 内核把包放进队列后,sing-box 怎么知道有数据了?
答案是:文件描述符 + wait queue(等待队列)机制。
1 | 没有硬件中断(IRQ) |
5.2 /dev/net/tun 是字符设备·
TUN 的用户态接口是 /dev/net/tun,一个字符设备(character device)。sing-box 通过以下步骤使用它:
1 | // 1. 打开字符设备 |
read() 会阻塞,直到 tun_queue 里有数据。
5.3 内核侧:wait queue 机制·
在内核 drivers/net/tun.c 中,当包进入 tun_queue 时:
1 | // 简化版流程(tun_net_xmit 内部) |
wake_up_interruptible_poll() 会唤醒所有在这个 wait queue 上睡眠的进程。sing-box 的 read() 或 epoll_wait() 因此返回。
1 | +------------------+ skb_queue_tail() +-------------+ |
5.4 epoll 模型·
实际上 sing-box 使用 epoll(Go runtime 底层用 epoll)而不是简单的阻塞 read:
1 | sing-box goroutine |
epoll 的好处是一个线程可以同时等待 tun_fd 和其他 fd (代理连接的 socket),实现高效多路复用。
6. TUN 的双向性·
6.1 kernel to userspace 方向·
1 | 路由命中 tun0 |
这个方向:内核「发送」到 tun0 = 用户态「接收」到数据。
6.2 userspace to kernel 方向·
1 | sing-box write(fd, ip_pkt, len) |
这个方向:用户态"发送" = 内核"从 tun0 接收到了数据"。
6.3 对称性的意义·
1 | +----------------------------+ +-------------------------+ |
TUN 的双向性使 sing-box 成为一个完整的 L3 网络协议栈代理:它既能拦截出站流量,又能把响应重新注入内核,对应用程序完全透明。
7. 内核实现细节·
7.1 关键数据结构·
核心代码在 drivers/net/tun.c,主要结构:
1 | struct tun_struct { |
tun_struct 持有 net_device 的引用,以及若干 tun_file(每个 tun_file 对应一个打开 /dev/net/tun 的 fd)。
7.2 net_device 注册·
TUN 通过 alloc_netdev / register_netdev 把自己注册为标准 net_device:
1 | // tun_set_iff() 内部(简化) |
一旦注册,tun0 就对内核路由子系统可见,可以作为路由出口接口。
7.3 tun_net_xmit: 包进入队列·
当内核路由决定把 skb 发往 tun0 时,调用 tun_net_xmit:
1 | static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev) |
注意:如果队列满了,包会被直接丢弃(tx_dropped++),不会阻塞内核。
7.4 tun_chr_read_iter: 用户态读取·
sing-box 调用 read(fd) 时,进入 tun_chr_read_iter:
1 | static ssize_t tun_chr_read_iter(struct kiocb *iocb, struct iov_iter *to) |
整个路径:read() → wait_event_interruptible() 睡眠 → tun_net_xmit() 中 wake_up() → read() 返回,拿到 IP 包。
8. TUN vs 其他机制·
8.1 对比表·
| 机制 | 层级 | 用户态接口 | 适用场景 | 限制 |
|---|---|---|---|---|
| SOCKS5 | L4 | 代理协议 | 应用主动配置 | 需应用支持 |
| TUN | L3 | read/write fd | 透明代理/VPN | 需内核配合路由 |
| TAP | L2 | read/write fd | 虚拟以太网 | 含以太网帧头 |
| veth | L2 | net_device 对 | 容器网络 | 两端都在内核 |
8.2 TUN vs SOCKS·
SOCKS 工作在应用层,要求应用主动感知;TUN 工作在 L3,对应用完全透明。
1 | SOCKS 代理路径: |
8.3 TUN vs TAP·
TAP 模拟完整以太网网卡(L2),用户态读到的是以太网帧(含 MAC 头);TUN 只有 L3,读到的是原始 IP 包。
WireGuard 在内核态实现,不走 /dev/net/tun;OpenVPN 早期使用 TUN/TAP 字符设备(用户态实现)。
8.4 TUN vs veth·
veth 是内核内部的虚拟以太网对(L2),两端都在内核,主要用于容器/namespace 隔离。veth 没有直接的用户态数据通道;TUN 的核心价值就是为用户态程序提供 L3 数据平面的直接访问。
9. 总结:mental model·
9.1 完整架构图·
1 | +----------------------------------------------+ |
9.2 五个核心问题的答案·
为什么 sing-box 要用 TUN?
SOCKS/HTTP proxy 要求应用感知代理,无法透明拦截所有流量。TUN 在 L3 层面接管流量,对应用完全透明,是实现"全局代理"的唯一内核级方案。
tun0 的 .1/.2 是什么?
point-to-point 虚拟链路的两端地址:.1 是内核侧地址,.2 是逻辑对端(sing-box 侧),不是真实机器,只是路由系统的寻址锚点。
数据包如何从内核进入用户态?
路由命中 tun0, 随后 tun_net_xmit() 把 skb 放入 tun_queue → wake_up() 唤醒 sing-box 的 read()/epoll, sing-box 拿到原始 IP 包。
为什么没有中断还能工作?
靠软件 wakeup + wait queue:内核在入队时调用 wake_up_interruptible_poll(),等待的 read()/epoll 进程被唤醒。这是纯软件机制,与硬件中断无关。
TUN 在 Linux 网络栈中的位置?
TUN 是注册在内核中的 L3 net_device,处于路由决策之后、物理网卡之前。它把内核路由子系统与用户态程序打通,是 VPN/代理工具实现"用户态网络栈"的基础设施。









