2026/9/23
不用 TUN 的进程级分流
Linux 上的透明代理大多是同一种做法:创建一个 TUN 设备,把默认路由指向它,再在用户态跑一个 TCP/IP 协议栈,把原始数据包重新组装成连接。这种做法到哪都能用,但在开发机上有不小的代价。 Specola 的 Linux 后端换了一条路:把 eBPF 程序挂到 cgroup v2 层级上,重定向的是 socket, 而不是数据包。
TUN 设备做了什么
使用 TUN 时,内核会把每一个被路由到设备的 IP 包交给用户态进程。这个进程需要:
- 用自己的网络协议栈重组 TCP 流、跟踪 UDP 会话;
- 事后推断每个包是哪个进程发出的,通常靠扫描 socket 表;
- 修改路由表和系统 DNS 让流量进入设备,退出时再恢复。
每条连接的每一个包都要进出用户态——包括那些你只想直连的流量。
Specola 的 eBPF 后端做了什么
Specola 把一组小程序挂到 cgroup socket hook 上。它们在程序创建 socket、发起连接或发送数据报 的那一刻于内核中运行:
| Hook | 作用 |
|---|---|
cgroup/sock_create、sock_release |
记录每个 socket 属于哪个进程 |
cgroup/connect4 |
逐条 TCP 连接决定是否重定向给 Core |
cgroup/sendmsg4、recvmsg4 |
UDP 的同样决策,并在回包时还原原始对端 |
cgroup/getpeername4 |
让被重定向的 socket 仍然报告原始目标地址 |
sockops |
标记已接受的连接,让 Core 能对应到它的元数据 |
connect4 里的判断按固定顺序进行:
- Core 自己的 socket 和 Specola UI 始终放行;
- DNS 查询交给 Core 的 DNS 模块,保证域名规则可用;
- 回环、本机地址、私有和链路本地网段、组播和广播直接放行;
- 其余连接与 Core 写入 BPF map 的候选快照比对:规则中的进程选择器、IP 和 GeoIP 规则的
IPv4 网段、DNS 为带规则的域名解析出的地址,以及当前的
FINAL出口。
不是候选的连接直接在内核中按 FINAL 处理。使用 FINAL,DIRECT 时,它的 socket 完全不受
影响:走内核自己的 TCP 协议栈和正常路由,从不进入用户态。候选连接会被重定向到 Core 的本地
监听;Core 查到原始目标、socket cookie 和进程后,执行完整的有序规则列表,再决定直连、拒绝,
或者连接所选的代理或代理组。
这对开发者意味着什么
- 连接发起时就知道进程。
PROCESS-NAME,cargo,work这类规则用的是 socket 的真实所有者, 而不是事后根据包头猜出来的结果。 - 直连流量留在内核。 没有规则接管的浏览器、视频会议和本地大文件传输,不需要多绕一次用户态。
- 没有默认路由改动,也没有 TUN 网卡。 Specola 不靠改写路由表来接管流量,停止服务即卸载程序。
- 一套规则。 本地 HTTP/SOCKS5 端口的流量和透明接管的流量使用同一个
[rule].list匹配。
当前限制
- 目前只接管 IPv4,IPv6 流量会绕过;
- 加载和挂载程序需要 root,并要求 cgroup v2 挂载在
/sys/fs/cgroup; - 已建立的 TCP 连接保持建立时的决策,规则变更只影响新连接;
- 使用
FINAL,DIRECT时,自己走加密 DNS 解析域名的应用不会让 Specola 看到域名,需要用进程或 IP 规则匹配它; - 仅支持 Linux。macOS 和 Windows 上 Specola 使用 TUN。
什么时候仍然用 TUN
TUN 是可移植的选择,不管进程在哪个 cgroup,都以同样的方式接管整机流量。如果现在就需要接管
IPv6,或者要路由整个网络命名空间而不是单个工具,type = "tun" 更合适。两种后端共用同一套
规则,切换只需要改 [enhanced-mode] 里的一行。
我们正在准备在同一台机器上对比两种后端的可复现 benchmark,届时会连同测试脚本一起发布。