TPROXY 在 iptables 中的使用位置
-j TPROXY这个 target(目标动作)只能在 mangle表的 PREROUTING链(以及从 PREROUTING链跳转到的自定义链)中使用。
这是由 Linux 内核的 Netfilter 框架和TPROXY的设计目的所决定的。
原因分析
- 设计初衷:
TPROXY最初的设计目标是为了实现 透明代理 ,主要处理 转发的流量 。例如,将一台设备作为网关,拦截并代理局域网内其他设备的所有流量。PREROUTING链是数据包进入网卡后、进行路由决策(判断这个包是发给本机还是需要转发出去) 之前 的钩子点。这是拦截一个“未知命运”的数据包的最早时机,非常适合用于透明代理。
- 技术限制:
- 在
OUTPUT链中,数据包已经被确定是由本机进程产生的,其路由路径(出口设备、下一跳等)已经基本确定。TPROXY的核心功能——在不修改数据包目标 IP 的情况下将其重定向到一个本地 socket——在此路径上难以实现或不被支持。 - 内核代码层面,
TPROXYtarget 没有被注册到OUTPUT链可用的 target 列表中。您如果尝试在OUTPUT链中使用它,iptables会直接报错TPROXY: No such file or directory或类似信息,因为它根本找不到这个选项。
- 在
- 功能分工:
-
PREROUTING+TPROXY: 处理“过路”的流量(转发)和指向本机的入站连接。这是TPROXY的“主场”。 -
OUTPUT+MARK+ 策略路由: 处理“本机产生”的流量。这是为本地进程流量实现透明代理的 替代方案 。
-
如何处理本机产生的流量?
既然OUTPUT链不能用TPROXY,那么如何让本机浏览器、终端等程序发出的流量也能走透明代理呢?这就需要用到之前提到的 “标记 + 策略路由” 的组合拳:
-
在
mangle表的OUTPUT链中打标记 (MARK):Terminal window iptables -t mangle -A OUTPUT -p tcp -m owner ! --uid-owner clash -j MARK --set-mark 1- 这条规则为 非 Clash 用户 产生的 TCP 流量设置一个标记(例如
1)。
- 这条规则为 非 Clash 用户 产生的 TCP 流量设置一个标记(例如
-
配置策略路由,让带标记的流量回流:
Terminal window # 创建一条策略规则:所有带有标记 1 的数据包,查询编号为 100 的路由表ip rule add fwmark 1 lookup 100# 在 100 号路由表中添加一条规则:将所有流量(0.0.0.0/0)指向本地环回接口(lo)# 关键选项 'local' 使得流量被重新交给本地的上层应用程序处理ip route add local default dev lo table 100- 这组命令的效果是:被
iptables标记为1的本地流量,在即将发出时,会被路由规则“劫持”,并强制发送到lo接口。 - 因为数据包被发往了
lo接口,它就会被送回用户空间,从而可以被正在监听某个端口的 Clash 进程接收并处理。 这巧妙地模拟了TPROXY的效果。
- 这组命令的效果是:被
总结
| 流量类型 | 处理链 | 核心操作 | 原因 |
|---|---|---|---|
| 转发/入站流量 | mangle PREROUTING | 直接使用 -j TPROXY | 这是 TPROXY的设计用途,内核支持在此链进行重定向。 |
| 本机进程发出的流量 | mangle OUTPUT | 使用 -j MARK打标 + ip rule/route引流 | 内核限制,不能在 OUTPUT链使用 TPROXY,需用策略路由实现同样目的。 |
一个完整的TPROXY模式配置是两者结合的产物:PREROUTING链使用真正的TPROXY功能,而OUTPUT链使用MARK和策略路由来达到相同的最终目的。
为什么要使用 lo 设备作为路由策略的默认设备
必须使用 lo(环回接口)**。使用任何其他物理接口(如 eth0, wlan0)都是错误且无法工作的。
原因在于,dev lo加上 local关键字的设计目的,是为了实现 “将数据包注入到本机网络栈” 这个特定动作,这与 TPROXY 的目标完全一致。
详细解释
为了理解为什么必须是lo,我们需要分解这条命令的目的和含义:
ip route add local 0.0.0.0/0 dev lo table 100这条命令在编号为 100的自定义路由表中创建了一条规则。我们来解析它的每个部分:
-
local (关键字)- 这是最核心的部分。
local是一个 路由类型 ,而不是一个简单的选项。 - 它的含义是: 目标地址是本机 。内核会将匹配此规则的数据包视为 发往本机某个应用程序的数据包 ,而不是需要转发到其他主机的数据包。
- 它告诉内核:“这个数据包的目的地是我们自己,请将它上传给传输层(TCP/UDP),然后交给正在监听对应端口的应用程序处理。”
- 这是最核心的部分。
-
0.0.0.0/0 (前缀)- 这是 默认路由 ,匹配所有目标地址。
-
dev lo (出口设备)lo是 环回接口 。它是一个虚拟设备,所有发往lo的数据包都不会离开主机,而是在内部网络栈中循环。- 指定
dev lo是为了与local类型保持一致和完整。既然目的地是本机,最“自然”的出口设备就是环回接口。
-
table 100- 这条规则只存在于我们自定义的、用于处理代理流量的路由表中,不会影响系统的默认路由。
为什么不能是其他设备(如 eth0)?
假设你把命令错误地写成:
# 错误示例!这将导致代理完全失效。ip route add 0.0.0.0/0 dev eth0 table 100- 缺少
local关键字 :这条规则变成了一个普通的默认路由,意思是“把所有不知道如何走的流量都从eth0接口发出去,让网关去处理”。 - 结果 :被你打了标记
1的本地进程流量,在OUTPUT链处理完后,会根据策略路由查询table 100。这条错误的路由规则会指示内核:“把这些流量从物理网卡eth0直接发送到网络上。” - 最终后果 :数据包根本不会送给监听在本地端口的 Clash 进程,而是直接被发送到了互联网, 透明代理功能完全失效 。Clash 完全看不到这些流量。
整个流程的串联
现在我们把所有步骤串联起来,看看 dev lo如何发挥作用:
-
本地进程 (如浏览器)发起一个到
google.com:443的 TCP 连接。 -
数据包进入网络栈,经过
mangle 的 OUTPUT 链。 -
iptables规则匹配到这个数据包(非 Clash 进程,非局域网目标),并给它 打上标记1。iptables -t mangle -A OUTPUT -p tcp -j MARK --set-mark 1 -
内核准备路由这个数据包。 策略路由规则 生效:“所有带标记
1的包,不查主路由表,去查table 100。”ip rule add fwmark 1 table 100 -
内核查询
table 100,找到了这条规则:ip route add local 0.0.0.0/0 dev lo table 100 -
这条规则告诉内核:“ 把这个包当作是发往本机的 (
local),并从环回接口lo发送。” -
因为目的地被指定为“本机”,内核于是将这个原本目标是
google.com:443的数据包 上交给了传输层(TCP/UDP)。 -
传输层发现本机有一个进程(Clash)正在监听
*:7893(TPROXY 端口),于是将数据包 交付给 Clash。 -
Clash 接收到这个连接,然后代表浏览器去向真正的目标发起代理请求。
总结
| 组件 | 作用 | 为什么是 lo |
|---|---|---|
ip rule ... table 100 | 引流 :将特定标记的流量导向自定义路由表。 | - |
ip route add local ... dev lo table 100 | 注入 :在自定义路由表中, 强制将流量目的地变更为本机 ,并使其进入本机网络栈的上层。 | local关键字决定了流量目的地是本机,而 dev lo是与“发往本机”这个动作最逻辑匹配的出口设备。这是实现流量回流的唯一正确方法。 |
-j TPROXY | 拦截 :在 PREROUTING链直接重定向转发流量。 | (TPROXY 本身已包含重定向到本机的逻辑) |
因此,dev lo并不是一个任意选择,而是 local路由类型的标准配套。它们的组合是实现“将外出流量重新注入回本机应用层”这一魔法的关键步骤 ,从而使得本地进程的流量也能被透明代理处理。使用任何其他设备都无法实现这个目的。
为什么处理 Clash 流量需要在 OUTPUT 和 PREROUTING 链上同时操作?
这涉及到 Linux 网络栈中一个至关重要的机制: 连接跟踪(Conntrack)。
核心原理:连接跟踪(Conntrack)的角色
Linux 内核有一个叫做 conntrack的子系统,它会跟踪所有经过网络栈的连接的状态。每个连接(如一个 TCP 会话)都会被标记为 NEW(新连接)、ESTABLISHED(已建立)、RELATED(相关连接)等状态。
这对于网络地址转换(NAT)、状态防火墙和 我们的 TPROXY 模式 都至关重要。
场景分析:一个本机 curl 请求的完整生命周期
让我们跟踪一个 curl http://ipinfo.io请求的完整路径,这能清楚地解释为什么需要 PREROUTING:
-
第 1 步:出站请求 (SYN 包)
- 终端进程发起连接,生成一个 SYN 包(
SRC:192.168.33.101:12345, DST:34.117.59.81:80)。 - 此包经过
mangle OUTPUT链,被打上mark 10。 - 策略路由规则生效,查询
table 100,路由规则local ... dev lo将此包 送回本机网络栈的“入口”。 - 此时,这个数据包仿佛变成了一个“从网络进入本机”的包 ,其目标地址仍然是
34.117.59.81:80。
- 终端进程发起连接,生成一个 SYN 包(
-
第 2 步:PREROUTING 拦截
- 这个被送回的包现在会 再次经过
mangle PREROUTING链 。 - 该链中配置的
TPROXY规则开始起作用:Terminal window iptables -t mangle -A PREROUTING -p tcp -j TPROXY --on-port 17893 --tproxy-mark 0x1 - 这条规则告诉内核:“所有 TCP 包,都请用 TPROXY 方式重定向到本地的 17893 端口,并给这些包设置 mark 1(用于后续的策略路由识别)。”
- 这是最关键的一步!
--on-port 17893参数在这里明确指定了重定向的目标端口。
- 这个被送回的包现在会 再次经过
-
第 3 步:内核交付
- 内核的 TPROXY 模块接收到指令,开始寻找一个 正在监听 17893 端口 并且 设置了
IP_TRANSPARENT标志 的 socket。 - Clash 进程正是创建了这样一个 socket。因此,内核将这个目标地址为
34.117.59.81:80的 SYN 包 交付给了 Clash,尽管目标端口根本不是 17893。
- 内核的 TPROXY 模块接收到指令,开始寻找一个 正在监听 17893 端口 并且 设置了
-
第 4 步:Clash 处理
- Clash 接收到这个原始的 SYN 包,从中提取出原始目标地址 (
ipinfo.io:80)。 - Clash 根据自身的规则决定如何代理这个请求(直连、代理、拒绝等)。
- 如果决定代理,Clash 会代表您的终端程序,发起一个新的连接到目标服务器或上游代理。
- Clash 接收到这个原始的 SYN 包,从中提取出原始目标地址 (
-
第 5 步:后续流量
- 此后,这个连接的所有后续包(ACK、数据包等)都会因为 conntrack 系统的存在,被自动关联到第 3 步中建立的“Clash socket -> 目标服务器”的链接上,从而持续被 Clash 处理。
为什么必须加上 PREROUTING规则?
- 处理非对称路径(主要原因): 网络流量并不总是对称的。请求路径和响应路径可能不同。
PREROUTING规则确保 所有从外部进入的流量 (无论是响应包,还是其他形式的入站连接)都有一个机会被检查并重定向到 Clash,从而保证连接的完整性和稳定性。没有它,在某些复杂的网络场景或连接状态异常时,响应包可能无法正确送达。 - 建立完整的连接跟踪上下文:
TPROXY在PREROUTING链中对数据包的处理,帮助内核更好地维护和识别哪些连接应该由 Clash 来处理。这为整个代理过程提供了更可靠的上下文环境。 - 覆盖所有入口:
PREROUTING是网络数据包进入本机后的 第一个钩子点 。在这里设置规则相当于设置了一个“总阀门”,确保没有任何需要代理的流入流量被遗漏。结合OUTPUT的“总阀门”,就构成了一个完整的闭环。
结论
一个生产环境可用的、健壮的 TPROXY 透明代理配置,必须同时包含 PREROUTING和 OUTPUT两部分的规则。
-
OUTPUT链 + 策略路由: 负责 捕获 本机进程发出的 初始请求 。 -
PREROUTING链 +TPROXY: 负责 接收 和 引导 所有从外部返回的 响应数据包 ,确保它们能到达 Clash 进程,从而完成整个通信回路。
两者相辅相成,缺一不可。