Linux 四大透明代理技术解析
坑边闲话:许多读者可能用了好几年 sing-box 和 Clash,却一直没搞清楚 TUN 模式和 TPROXY 模式到底差在哪里。本文结合笔者的个人理解,尝试把这几套机制从头捋了一遍。
1. 背景:代理软件为什么要分这么多模式·
使用过 sing-box、Clash 或 shadowsocks 的人都知道,这类工具往往提供多种工作模式:SOCKS 代理、TUN 模式、透明代理(REDIRECT / TPROXY)。这几种模式在文档里往往被并列列出,却鲜有人讲清楚它们的本质差异。
表面上,这些模式都是"让流量经过代理程序",但它们拦截流量的位置、使用的内核机制、对原始目标地址的处理方式,以及适用场景,都有根本性的不同。
本文的目标是建立一个统一的系统级 mental model,把这四种机制:SOCKS、REDIRECT、TUN、TPROXY,放到同一个框架下,讲清楚每种机制的工作原理、设计取舍,以及为什么现代代理软件在不同场景下选择不同方案。
2. 四种机制的本质抽象·
在深入每种机制之前,先从最高层看:所有代理机制要解决的核心问题都是一样的:如何把本不属于自己的流量,交给用户态程序处理?
四种机制的抽象差异,本质上是"在哪个层次拦截流量"的差异:
1 | +------------------+----------+--------------------+ |
2.1 SOCKS / HTTP Proxy:应用层显式代理·
这是最古老也最直接的方案。应用程序在建立连接时,不直接连接目标服务器,而是主动连接代理服务器,并通过协议协商(SOCKS5 握手或 HTTP CONNECT)告知代理程序真实的目标地址。
1 | +-------+ SOCKS5 handshake +-------+ TCP connect +--------+ |
关键特征:
- 应用程序感知代理的存在,必须主动配置
- 内核不做任何流量劫持,代理地址是连接的真实目标
- 原始目标地址由应用层协议显式传递,不存在"恢复原始目标"的问题
这种方案的局限是显而易见的:只有支持 SOCKS/HTTP 代理协议的应用才能使用。系统级的透明代理,需要更底层的拦截机制。
2.2 REDIRECT:NAT 层劫持·
REDIRECT 是 iptables/netfilter 的 NAT 功能。它在内核的 NAT 表中修改数据包的目标地址,将发往任意目标的流量"重定向"到本地的一个端口,由监听在该端口的代理程序接收。
1 | Original packet: src=192.168.1.2:54321 dst=1.2.3.4:443 |
从代理程序的视角来看,它接收到的是一个普通的 TCP 连接,目标是 127.0.0.1:12345. 此时一个问题出现了:原始目标地址 1.2.3.4:443 去哪儿了?
代理程序必须知道原始目标才能建立出站连接,这就引出了 SO_ORIGINAL_DST,我们在第 3 节详细讨论。
2.3 TUN:虚拟网卡导流·
TUN 是一种虚拟的三层网络设备(L3 net_device)。它的工作原理是:通过路由规则(routing / policy routing)将流量导入 TUN 设备,而 TUN 设备的另一端连接着用户态程序通过 /dev/net/tun 打开的文件描述符。
1 | +--------+ IP packets +----------+ read(fd) +-----------+ |
TUN 的关键特征是:它工作在 IP 层,用户态程序拿到的是完整的 IP 数据包(而非 TCP 流)。代理程序自己解析 IP/TCP/UDP 头,从中提取目标地址,完全不依赖任何 NAT 或 conntrack 机制。
2.4 TPROXY:透明 socket 接管·
TPROXY 是 netfilter 提供的一种机制,它与 REDIRECT 最大的区别在于:不修改数据包的目标地址。流量到达本地 socket 时,目标地址仍然是原始的 1.2.3.4:443.
1 | +--------+ dst=1.2.3.4:443 +-----------+ dst=1.2.3.4:443 +--------+ |
这要求用户态程序的 socket 设置了 IP_TRANSPARENT 选项,允许其绑定并接收目标不是本机地址的数据包。这是 TPROXY 机制的核心——通过一个特殊的 socket 选项,绕过内核对"目标地址必须是本机地址"的检查。
3. SO_ORIGINAL_DST: REDIRECT 的历史包袱·
SO_ORIGINAL_DST 是理解 REDIRECT 机制时绕不开的一个细节,它的存在本身就说明了 REDIRECT 方案设计上的一个缺陷。
3.1 问题的来源·
REDIRECT 在 netfilter 的 NAT 表中修改了数据包的目标地址。当 TCP 三次握手完成,代理程序 accept() 到这个连接时,getpeername() 能拿到客户端地址,但 getsockname() 只能拿到本地监听地址 127.0.0.1:12345,原始目标 1.2.3.4:443 已经被 NAT 改写,无从得知。
1 | proxy calls getsockname() -> 127.0.0.1:12345 (local listen addr) |
3.2 解决方案:从 conntrack 取回·
内核在做 NAT 的时候,会在 connection tracking(conntrack)中记录一条 NAT 映射。conntrack 保存了连接的 original tuple(原始五元组),包括修改前的目标地址。
SO_ORIGINAL_DST 是一个 socket option,通过 getsockopt() 调用,让内核从 conntrack 查询当前连接的原始目标地址,返回给用户态程序:
1 | struct sockaddr_in orig_dst; |
这个调用的内核实现路径大致是:
1 | getsockopt(SO_ORIGINAL_DST) |
3.3 conntrack 的角色·
conntrack 是 netfilter 维护的连接跟踪表。每条 TCP/UDP 连接在首个数据包经过 netfilter 时被记录,包含两个方向的 tuple:
1 | ORIGINAL direction: 192.168.1.2:54321 -> 1.2.3.4:443 (TCP) |
NAT 修改数据包时,同时在 conntrack 条目中记录 NAT 变换。后续的 SO_ORIGINAL_DST 查询就是在读取这个 ORIGINAL tuple。
3.4 SO_ORIGINAL_DST 的限制·
这个机制有几个根本性的限制:
只适用于 REDIRECT/NAT:TUN 和 TPROXY 机制本就不修改目标地址,根本不需要这个调用。SOCKS 则是应用层协议,目标地址由应用显式传递。
依赖 conntrack:如果 conntrack 表项已过期或被清除,查询会失败。在高并发场景下,conntrack 本身也是性能瓶颈。
UDP 处理复杂:UDP 没有连接概念,conntrack 对 UDP 的跟踪是基于 5 元组的超时机制,在多路复用场景下行为不直观。
| 机制 | 是否需要 SO_ORIGINAL_DST |
原因 |
|---|---|---|
| SOCKS | 否 | 目标地址由应用层协议显式传递 |
| REDIRECT | 是 | NAT 修改了 dst, 需从 conntrack 恢复 |
| TUN | 否 | 用户态直接解析 IP 包头,dst 始终可见 |
| TPROXY | 否 | dst 未被修改,socket 直接看到原始目标地址 |
4. TPROXY 是一个更干净的方案·
4.1 REDIRECT 的根本问题·
REDIRECT 方案的核心矛盾在于:它主动破坏了数据包的目标地址,然后又需要通过额外的机制 (SO_ORIGINAL_DST + conntrack) 把这个信息找回来。这不仅增加了复杂性,还引入了对 conntrack 的强依赖。
对于运行在路由器上的透明网关,这个问题更加突出:
- 路由器要处理大量并发连接,
conntrack表是内存瓶颈 - 转发的流量(非本机发起)用 REDIRECT 处理更加繁琐
- 每个连接都要额外查询一次
conntrack, 有性能开销
4.2 TPROXY 的设计思路·
TPROXY(Transparent Proxy)的核心思想是:既然最终还是要把原始目标地址告诉代理程序,为什么不一开始就不修改它?
TPROXY 让数据包保持原始的目标地址不变,同时通过特殊的 socket 机制,让用户态程序的 socket 能够接收这个"目标不是自己"的数据包。代理程序直接从 getsockname() 就能拿到原始目标地址,不需要任何额外查询。
4.3 核心机制:五个关键组件·
TPROXY 的工作需要五个组件协同:
① netfilter mangle 表
TPROXY 规则设置在 mangle 表的 PREROUTING 链,而不是 nat 表。mangle 表不做地址转换,只做标记和流量引导。
1 | iptables -t mangle -A PREROUTING -p tcp --dport 443 \ |
② TPROXY target
当数据包命中 TPROXY 规则时,netfilter 做两件事:
- 在数据包上打
fwmark - 将数据包"关联"到监听在指定端口的本地 socket(通过内核的 socket lookup)
③ fwmark + ip rule
fwmark 是数据包上携带的一个整数标记,不会出现在网络上,只在本机内核内部使用。ip rule 根据 fwmark 决定使用哪张路由表:
1 | ip rule add fwmark 0x1 lookup 100 |
这条路由规则的效果是:带有 fwmark 0x1 的数据包,走 table 100, 而 table 100 里有一条 local 路由把所有地址都指向 lo。这样内核就会把这个包交给本地 socket 处理,即使目标地址不是本机 IP.
④ IP_TRANSPARENT socket option
代理程序在创建监听 socket 时,必须设置 IP_TRANSPARENT:
1 | int val = 1; |
这个选项做两件事:
- 允许 socket 绑定非本机地址(用于出站连接伪装源地址,TPROXY 的出站侧)
- 允许 socket 接收目标地址不是本机地址的数据包(用于入站侧接管)
没有这个选项,内核在做 socket lookup 时,发现目标地址不是本机地址,会拒绝将数据包交给该 socket。
⑤ 完整数据流
1 | Client (192.168.1.2:54321) sends TCP SYN to 1.2.3.4:443 |
4.4 为什么 IP_TRANSPARENT 能打破地址限制·
内核在做本地 socket delivery 时,正常流程是:查找目标地址是否是本机地址(在 local routing table 中),如果不是则转发或丢弃。
IP_TRANSPARENT 的引入,在 socket lookup 阶段增加了一个例外:如果找不到匹配的普通 socket,还会搜索带有 IP_TRANSPARENT 标记的 socket。这类 socket 被内核视为「可以接收任意目标地址」的特权 socket。
这个选项需要 CAP_NET_ADMIN 权限,因为它打破了正常的网络地址归属规则。
5. TUN 设备的内核机制·
5.1 TUN 是什么·
TUN(network TUNnel)是 Linux 内核提供的一种虚拟网络设备,实现在 drivers/net/tun.c. 它是一个标准的 net_device,从内核网络栈的角度来看,与物理网卡没有任何区别——可以配置 IP 地址、添加路由、被 netfilter 处理。
TUN 和 TAP 的区别:
- TUN:工作在 L3,收发 IP 数据包
- TAP:工作在 L2,收发以太网帧(带 Ethernet header)
代理场景通常使用 TUN,因为代理程序关心的是 IP/TCP/UDP,不需要处理以太网层。
5.2 /dev/net/tun 字符设备接口·
TUN 设备的用户态接口是字符设备 /dev/net/tun。用户态程序打开这个设备文件,通过 ioctl(TUNSETIFF) 创建或绑定到一个 TUN 网络接口,之后这个文件描述符就成为 TUN 设备的「另一端」:
1 | +-------------------+ +-------------------+ |
5.3 数据包的发送路径:tun_net_xmit·
当内核网络栈要通过 tun0 发送一个数据包时(例如路由决策把某个 TCP 连接的报文送到了 tun0),调用链如下:
1 | dev_queue_xmit() |
注意这里的关键:tun_net_xmit 把 skb(socket buffer,内核中表示数据包的结构)放入一个队列,然后唤醒等待在这个 TUN 设备上的用户态进程。
5.4 用户态读取路径:tun_chr_read_iter·
用户态程序调用 read(fd, buf, len) 读取 TUN 设备时,进入 tun_chr_read_iter:
1 | // simplified from drivers/net/tun.c |
没有 DMA,没有硬件中断——TUN 设备的"中断"是软件的 wake_up_interruptible(),对应 tun_net_xmit 中的 wakeup 调用。这是一个标准的 Linux wait queue 机制。
5.5 TUN 的通知机制·
1 | Packet arrives at tun0 (via kernel routing) |
用户态程序也可以用 poll()/epoll() 监听 TUN fd, 这是代理程序通常的做法:用一个事件循环同时监听 TUN fd 和上游连接的 fd,实现非阻塞的全双工转发。
5.6 TUN 架构全图·
1 | +----------------------------------------------------------+ |
注意防环路的必要性:如果代理程序自身发出的流量也被路由到 tun0,会形成无限循环。通常用 policy routing 或 cgroup 标记解决:代理程序的流量走默认路由,其他进程的流量走 tun0。
6. 四种机制完整对比·
6.1 对比表格·
| 机制 | 拦截层 | 是否修改 dst | 应用是否感知代理 | 原始目标获取方式 | 技术核心 |
|---|---|---|---|---|---|
| SOCKS | 应用层 | 否 | 是 | 应用层协议显式传递 | socket |
| REDIRECT | NAT 表 | 是 | 否 | SO_ORIGINAL_DST | conntrack |
| TUN | routing | 否 | 否 | 解析 IP 包头 | net_device |
| TPROXY | mangle 表 | 否 | 否 | getsockname() | transparent socket |
6.2 数据流对比·
SOCKS:
1 | App ----[SOCKS5 handshake: "connect to 1.2.3.4:443"]----> Proxy ----> 1.2.3.4:443 |
REDIRECT:
1 | App ---[TCP SYN to 1.2.3.4:443]---> netfilter NAT ---[dst=127.0.0.1:12345]---> Proxy |
TUN:
1 | App ---[TCP SYN to 1.2.3.4:443]---> routing (default via tun0) ---> tun0 device |
TPROXY:
1 | App ---[TCP SYN to 1.2.3.4:443]---> netfilter mangle (TPROXY) ---> [fwmark=1] |
6.3 关键差异分析·
TUN vs TPROXY 的本质区别
这是最容易混淆的一对。虽然两者都不修改 dst,但工作层次不同:
- TUN 工作在 routing 层,拿到的是 IP 数据包,代理程序必须自己实现 TCP/UDP 协议栈(或使用 gVisor 等用户态网络栈)
- TPROXY 工作在 socket 层,内核完成了 TCP 三次握手,代理程序拿到的是 已建立的 TCP 连接
这个差异决定了实现复杂度:TUN 模式的代理需要在用户态处理完整的 TCP/IP 协议,TPROXY 模式只需处理应用层数据。
REDIRECT vs TPROXY 的根本区别
- REDIRECT 在 nat 表操作,修改数据包,依赖
conntrack恢复信息 - TPROXY 在 mangle 表操作,不修改数据包,
dst信息始终保留
7. 选型分析·
7.1 什么场景用什么方案·
7.1.1 应用级代理:SOCKS·
适用场景:开发调试、单个应用代理、不需要系统级透明代理
SOCKS 是最简单的方案,没有内核配置,没有权限要求,浏览器和大多数开发工具原生支持。当你只需要"让这个程序走代理"时,SOCKS 是开销最低的选择。
7.1.2 简单透明代理:REDIRECT·
适用场景:单机、仅 TCP、不关心高性能
REDIRECT 配置简单(一条 iptables 规则),适合快速搭建。局限在于:UDP 支持复杂,conntrack 有内存和性能开销,不适合高并发的网关场景。
7.1.3 单机代理 / 桌面系统:TUN·
适用场景:桌面 Linux/macOS/Windows、单机代理、需要同时处理 TCP 和 UDP
TUN 模式在桌面系统上有几个优势:
- 跨平台:macOS 和 Windows 没有 TPROXY,但都有 TUN
- 全流量拦截:routing 层面的拦截,TCP 和 UDP 一视同仁
- 防环路容易:用 cgroup 或 uid 标记区分代理进程和普通进程流量
sing-box 和 Clash 的 TUN 模式正是如此:创建一个 tun0 接口,通过 policy routing 把非代理进程的流量路由进去,在用户态运行一个简化的 TCP/UDP 协议栈。
7.1.4 路由器 / 网关 / OpenWrt:TPROXY·
适用场景:透明网关、OpenWrt、需要转发其他主机流量
TPROXY 在网关场景有明显优势:
- 无需修改数据包,减少
conntrack压力 - 直接获取原始目标地址,无额外查询开销
- 对转发流量(非本机发起)支持更好
- OpenWrt 等嵌入式 Linux 通常内核已包含 TPROXY 支持
7.2 为什么现代代理软件这样选择·
7.2.1 sing-box / Clash 在桌面用 TUN·
根本原因是跨平台。TPROXY 是 Linux 独有的 netfilter 功能,macOS 没有 iptables,Windows 更不用说。TUN 设备在三大平台都有支持(Linux 的 /dev/net/tun,macOS 的 /dev/utun*,Windows 的 WinTun),实现一套代码逻辑,通过不同平台的 TUN 接口即可运行。
另一个原因是 DNS 劫持。TUN 模式下,代理程序拿到的是 IP 数据包,可以在 DNS 请求到达目标服务器之前就截获并伪造响应,实现 Fake IP 等技术,解决 DNS 泄露问题。
7.2.2 OpenClash / 路由器固件用 TPROXY·
网关场景的特点是:转发大量其他主机发起的连接,对性能和资源消耗敏感。TPROXY 方案:
- 不涉及用户态 TCP 协议栈(相比 TUN),代理程序直接接管 TCP 连接
conntrack压力比 REDIRECT 小(不需要 NAT 改写)- iptables/nftables 规则更简洁,调试更直观
7.2.3 为什么 SOCKS 仍然存在·
SOCKS 看似过时,但它有两个不可替代的场景:
浏览器和开发工具:Firefox, curl, git 等工具原生支持 SOCKS5,配置简单,不需要任何系统权限。
代理链(proxy chaining):多级代理场景下,SOCKS 协议可以嵌套,实现代理穿越代理。这是 TUN/TPROXY 在协议层面无法直接实现的。
8. 设计哲学:流量委托的四种抽象·
回顾这四种机制,它们解决的是同一个问题,但借助了不同层次的抽象:
1 | +------------------+------------------+-------------------------------+ |
一个有趣的观察:
- SOCKS 是"应用主动合作"。应用自己把目标地址告诉代理
- REDIRECT 是"内核强制改写,事后修复"。代理被动接受,再通过 conntrack 查询被改写的信息
- TUN 是"内核交出原始数据"。代理拿到最原始的 IP 包,自己处理一切
- TPROXY 是"内核做最小介入"。内核只做流量引导,不修改数据,代理直接拿到完整信息
从系统设计的角度,TPROXY 体现了一种"最小修改原则":只做必要的事,不引入额外的状态(conntrack NAT 映射),不破坏已有信息(原始 dst 地址)。它的复杂性在于配置(mangle + fwmark + ip rule + IP_TRANSPARENT),而不在于运行时。
TUN 则是另一种极端:把所有协议处理权都交给用户态,内核只提供一个数据通道。这种方案最灵活(用户态可以完全自定义协议处理),也最重(需要在用户态实现 TCP/UDP 协议栈),但跨平台能力最强。
这四种机制的存在,不是因为某一种"更好",而是因为不同的约束(平台、性能、权限、协议支持)在不同场景下有不同的最优解。理解这些取舍,是选对工具、排查问题的基础。









