3295 字
16 分钟
​TPROXY使用过程中遇到的问题

TPROXY 在 iptables 中的使用位置#

-j TPROXY这个 target(目标动作)只能在 mangle表的 PREROUTING链(以及从 PREROUTING链跳转到的自定义链)中使用。​​

这是由 Linux 内核的 Netfilter 框架和TPROXY的设计目的所决定的。

原因分析#

  1. ​ 设计初衷:​
    • TPROXY最初的设计目标是为了实现 ​​ 透明代理 ​​,主要处理 ​​ 转发的流量 ​​。例如,将一台设备作为网关,拦截并代理局域网内其他设备的所有流量。
    • PREROUTING链是数据包进入网卡后、进行路由决策(判断这个包是发给本机还是需要转发出去)​​ 之前 ​​ 的钩子点。这是拦截一个“未知命运”的数据包的最早时机,非常适合用于透明代理。
  2. ​ 技术限制:​
    • OUTPUT链中,数据包已经被确定是由本机进程产生的,其路由路径(出口设备、下一跳等)已经基本确定。TPROXY的核心功能——在不修改数据包目标 IP 的情况下将其重定向到一个本地 socket——在此路径上难以实现或不被支持。
    • 内核代码层面,TPROXYtarget 没有被注册到OUTPUT链可用的 target 列表中。您如果尝试在OUTPUT链中使用它,iptables会直接报错TPROXY: No such file or directory或类似信息,因为它根本找不到这个选项。
  3. ​ 功能分工:​
    • PREROUTING+ TPROXY:​​ 处理“过路”的流量(转发)和指向本机的入站连接。这是TPROXY的“主场”。
    • OUTPUT+ MARK+ 策略路由:​​ 处理“本机产生”的流量。这是为本地进程流量实现透明代理的 ​​ 替代方案 ​​。

如何处理本机产生的流量?#

既然OUTPUT链不能用TPROXY,那么如何让本机浏览器、终端等程序发出的流量也能走透明代理呢?这就需要用到之前提到的 ​​“标记 + 策略路由”​​ 的组合拳:

  1. ​ 在 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)。
  2. ​ 配置策略路由,让带标记的流量回流:​

    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,我们需要分解这条命令的目的和含义:

Terminal window
ip route add local 0.0.0.0/0 dev lo table 100

这条命令在编号为 100的自定义路由表中创建了一条规则。我们来解析它的每个部分:

  1. local​ (关键字)

    • 这是最核心的部分。local是一个 ​​ 路由类型 ​​,而不是一个简单的选项。
    • 它的含义是:​​ 目标地址是本机 ​​。内核会将匹配此规则的数据包视为 ​​ 发往本机某个应用程序的数据包 ​​,而不是需要转发到其他主机的数据包。
    • 它告诉内核:“这个数据包的目的地是我们自己,请将它上传给传输层(TCP/UDP),然后交给正在监听对应端口的应用程序处理。”
  2. 0.0.0.0/0​ (前缀)

    • 这是 ​​ 默认路由 ​​,匹配所有目标地址。
  3. dev lo​ (出口设备)

    • lo是 ​​ 环回接口 ​​。它是一个虚拟设备,所有发往 lo的数据包都不会离开主机,而是在内部网络栈中循环。
    • 指定 dev lo是为了与 local类型保持一致和完整。既然目的地是本机,最“自然”的出口设备就是环回接口。
  4. table 100

    • 这条规则只存在于我们自定义的、用于处理代理流量的路由表中,不会影响系统的默认路由。

为什么不能是其他设备(如 eth0)?#

假设你把命令错误地写成:

Terminal window
# 错误示例!这将导致代理完全失效。
ip route add 0.0.0.0/0 dev eth0 table 100
  • ​ 缺少 local关键字 ​​:这条规则变成了一个普通的默认路由,意思是“把所有不知道如何走的流量都从 eth0接口发出去,让网关去处理”。
  • ​ 结果 ​​:被你打了标记 1的本地进程流量,在 OUTPUT链处理完后,会根据策略路由查询 table 100。这条错误的路由规则会指示内核:“把这些流量从物理网卡 eth0直接发送到网络上。”
  • ​ 最终后果 ​​:数据包根本不会送给监听在本地端口的 Clash 进程,而是直接被发送到了互联网,​​ 透明代理功能完全失效 ​​。Clash 完全看不到这些流量。

整个流程的串联#

现在我们把所有步骤串联起来,看看 dev lo如何发挥作用:

  1. ​ 本地进程 ​​(如浏览器)发起一个到google.com:443的 TCP 连接。

  2. 数据包进入网络栈,经过 mangle​ 的 ​OUTPUT​​ 链。

  3. iptables规则匹配到这个数据包(非 Clash 进程,非局域网目标),并给它 ​​ 打上标记 1​。

    iptables -t mangle -A OUTPUT -p tcp -j MARK --set-mark 1

  4. 内核准备路由这个数据包。​​ 策略路由规则 ​​ 生效:“所有带标记 1的包,不查主路由表,去查 table 100。”

    ip rule add fwmark 1 table 100

  5. 内核查询 table 100,找到了这条规则:

    ip route add local 0.0.0.0/0 dev lo table 100

  6. 这条规则告诉内核:“​​ 把这个包当作是发往本机的 (local)​​,并从环回接口 lo发送。”

  7. 因为目的地被指定为“本机”,内核于是将这个原本目标是google.com:443的数据包 ​​ 上交给了传输层(TCP/UDP)​​。

  8. 传输层发现本机有一个进程(Clash)正在监听*:7893(TPROXY 端口),于是将数据包 ​​ 交付给 Clash​​。

  9. 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. ​ 第 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
  2. ​ 第 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. ​ 第 3 步:内核交付 ​

    • 内核的 TPROXY 模块接收到指令,开始寻找一个 ​​ 正在监听 17893 端口 ​​ 并且 ​​ 设置了 IP_TRANSPARENT标志 ​​ 的 socket。
    • Clash 进程正是创建了这样一个 socket。因此,内核将这个目标地址为 34.117.59.81:80的 SYN 包 ​​ 交付给了 Clash​​,尽管目标端口根本不是 17893。
  4. ​ 第 4 步:Clash 处理 ​

    • Clash 接收到这个原始的 SYN 包,从中提取出原始目标地址 (ipinfo.io:80)。
    • Clash 根据自身的规则决定如何代理这个请求(直连、代理、拒绝等)。
    • 如果决定代理,Clash 会代表您的终端程序,发起一个新的连接到目标服务器或上游代理。
  5. ​ 第 5 步:后续流量 ​

    • 此后,这个连接的所有后续包(ACK、数据包等)都会因为 conntrack 系统的存在,被自动关联到第 3 步中建立的“Clash socket -> 目标服务器”的链接上,从而持续被 Clash 处理。

为什么必须加上 PREROUTING规则?#

  1. ​ 处理非对称路径(主要原因):​​ 网络流量并不总是对称的。请求路径和响应路径可能不同。PREROUTING规则确保 ​​ 所有从外部进入的流量 ​​(无论是响应包,还是其他形式的入站连接)都有一个机会被检查并重定向到 Clash,从而保证连接的完整性和稳定性。没有它,在某些复杂的网络场景或连接状态异常时,响应包可能无法正确送达。
  2. ​ 建立完整的连接跟踪上下文:​TPROXYPREROUTING链中对数据包的处理,帮助内核更好地维护和识别哪些连接应该由 Clash 来处理。这为整个代理过程提供了更可靠的上下文环境。
  3. ​ 覆盖所有入口:​PREROUTING是网络数据包进入本机后的 ​​ 第一个钩子点 ​​。在这里设置规则相当于设置了一个“总阀门”,确保没有任何需要代理的流入流量被遗漏。结合 OUTPUT的“总阀门”,就构成了一个完整的闭环。

结论#

​ 一个生产环境可用的、健壮的 TPROXY 透明代理配置,必须同时包含 PREROUTINGOUTPUT两部分的规则。​

  • OUTPUT链 + 策略路由:​​ 负责 ​​ 捕获 ​​ 本机进程发出的 ​​ 初始请求 ​​。
  • PREROUTING链 + TPROXY:​​ 负责 ​​ 接收 ​​ 和 ​​ 引导 ​​ 所有从外部返回的 ​​ 响应数据包 ​​,确保它们能到达 Clash 进程,从而完成整个通信回路。

两者相辅相成,缺一不可。