2026/9/23

不用 TUN 的进程级分流

Linux 上的透明代理大多是同一种做法:创建一个 TUN 设备,把默认路由指向它,再在用户态跑一个 TCP/IP 协议栈,把原始数据包重新组装成连接。这种做法到哪都能用,但在开发机上有不小的代价。 Specola 的 Linux 后端换了一条路:把 eBPF 程序挂到 cgroup v2 层级上,重定向的是 socket, 而不是数据包。

TUN 设备做了什么

使用 TUN 时,内核会把每一个被路由到设备的 IP 包交给用户态进程。这个进程需要:

  1. 用自己的网络协议栈重组 TCP 流、跟踪 UDP 会话;
  2. 事后推断每个包是哪个进程发出的,通常靠扫描 socket 表;
  3. 修改路由表和系统 DNS 让流量进入设备,退出时再恢复。

每条连接的每一个包都要进出用户态——包括那些你只想直连的流量。

Specola 的 eBPF 后端做了什么

Specola 把一组小程序挂到 cgroup socket hook 上。它们在程序创建 socket、发起连接或发送数据报 的那一刻于内核中运行:

Hook 作用
cgroup/sock_createsock_release 记录每个 socket 属于哪个进程
cgroup/connect4 逐条 TCP 连接决定是否重定向给 Core
cgroup/sendmsg4recvmsg4 UDP 的同样决策,并在回包时还原原始对端
cgroup/getpeername4 让被重定向的 socket 仍然报告原始目标地址
sockops 标记已接受的连接,让 Core 能对应到它的元数据

connect4 里的判断按固定顺序进行:

  1. Core 自己的 socket 和 Specola UI 始终放行;
  2. DNS 查询交给 Core 的 DNS 模块,保证域名规则可用;
  3. 回环、本机地址、私有和链路本地网段、组播和广播直接放行;
  4. 其余连接与 Core 写入 BPF map 的候选快照比对:规则中的进程选择器、IP 和 GeoIP 规则的 IPv4 网段、DNS 为带规则的域名解析出的地址,以及当前的 FINAL 出口。

不是候选的连接直接在内核中按 FINAL 处理。使用 FINAL,DIRECT 时,它的 socket 完全不受 影响:走内核自己的 TCP 协议栈和正常路由,从不进入用户态。候选连接会被重定向到 Core 的本地 监听;Core 查到原始目标、socket cookie 和进程后,执行完整的有序规则列表,再决定直连、拒绝, 或者连接所选的代理或代理组。

这对开发者意味着什么

当前限制

什么时候仍然用 TUN

TUN 是可移植的选择,不管进程在哪个 cgroup,都以同样的方式接管整机流量。如果现在就需要接管 IPv6,或者要路由整个网络命名空间而不是单个工具,type = "tun" 更合适。两种后端共用同一套 规则,切换只需要改 [enhanced-mode] 里的一行。

我们正在准备在同一台机器上对比两种后端的可复现 benchmark,届时会连同测试脚本一起发布。