跳转到内容

系统代理流量如何进入 Mihomo

解释系统代理是什么、Node 程序如何读取代理地址,以及 mihomo core 如何转发 HTTP、HTTPS 和 mixed-port 流量。

系统代理不是“系统强制劫持所有流量”,而是一组告诉应用该把网络请求交给谁的系统级配置。

在系统代理模式下,mihomo core 的角色很具体:它监听一个本地端口,例如 127.0.0.1:7890;愿意遵循系统代理的应用会主动连接这个端口;mihomo 从 HTTP proxy、HTTP CONNECT 或 SOCKS 握手里拿到目标地址,再用规则选择 DIRECTREJECT 或某个代理节点,最后在两条连接之间搬运字节。

这篇只讲系统代理流量,不讲 TUN。TUN 的入口不是“应用主动连接本地代理”,而是“虚拟网卡接住网络层流量”,心智模型要另起一篇。

当你在 Clash Verge、Clash Party、mihomo-party 或自己的桌面客户端里点下“系统代理”时,直觉上会以为系统接下来把所有网络流量都转给了 mihomo。

实际不是这样。

系统代理更像一张系统级便签:

HTTP Proxy -> 127.0.0.1:7890
HTTPS Proxy -> 127.0.0.1:7890
SOCKS Proxy -> 127.0.0.1:7890

这张便签不会自动改写所有 connect()。应用如果使用系统网络库,或者主动读取系统代理配置,就会把请求发给 127.0.0.1:7890。应用如果绕开这套机制,直接自己建连接,系统代理就不会天然生效。

在 macOS 上,原生应用通常不是自己读某个配置文件,而是走系统网络框架。

常见路径是:

应用
-> URLSession / WebKit / CFNetwork / SystemConfiguration
-> 当前网络服务的代理配置
-> 得到 HTTP / HTTPS / SOCKS / PAC 代理信息

Apple 提供的核心接口有两类:

  • CFNetworkCopySystemProxySettings:读取当前系统 internet proxy settings。
  • CFNetworkCopyProxiesForURL:给定一个 URL 和系统代理设置,返回访问这个 URL 应该使用的代理列表。

官方文档里对 CFNetworkCopyProxiesForURL 的描述很关键:它返回的是“访问指定 URL 应该使用的代理列表”,而且代理字典里会包含代理类型、host 和 port。也就是说,系统代理不是一个单纯的全局端口;遇到 PAC、绕过列表、不同协议时,应用应该按目标 URL 计算最终代理。

Node 程序如果只是想知道 macOS 当前系统代理,最直接的方法是调用系统命令:

import { execFileSync } from "node:child_process";
const output = execFileSync("scutil", ["--proxy"], { encoding: "utf8" });
console.log(output);

典型输出会长这样:

HTTPEnable : 1
HTTPProxy : 127.0.0.1
HTTPPort : 7890
HTTPSEnable : 1
HTTPSProxy : 127.0.0.1
HTTPSPort : 7890
SOCKSEnable : 1
SOCKSProxy : 127.0.0.1
SOCKSPort : 7890

如果要做得更像原生应用,就不要只解析 scutil --proxy,而是通过 native addon、FFI 或平台绑定去调用 CFNetwork。原因是 scutil --proxy 更像拿原始设置;CFNetworkCopyProxiesForURL 才会按具体 URL 处理 PAC、绕过规则和协议差异。

但大多数 Node CLI 还有另一套习惯:环境变量。

Terminal window
HTTP_PROXY=http://127.0.0.1:7890
HTTPS_PROXY=http://127.0.0.1:7890
ALL_PROXY=socks5://127.0.0.1:7890
NO_PROXY=localhost,127.0.0.1

这和 macOS 系统代理不是同一个东西。很多命令行工具不读系统设置,只读这些环境变量。

新版 Node 的 http 全局代理也支持环境变量,但需要显式启用:

Terminal window
NODE_USE_ENV_PROXY=1 \
HTTP_PROXY=http://127.0.0.1:7890 \
HTTPS_PROXY=http://127.0.0.1:7890 \
node client.js

或者:

Terminal window
HTTP_PROXY=http://127.0.0.1:7890 \
HTTPS_PROXY=http://127.0.0.1:7890 \
node --use-env-proxy client.js

所以判断一个 Node 程序是否走系统代理,要先问它使用哪一层网络能力:

程序或库默认更可能读取什么
node:http / node:https 旧用法通常不自动读 macOS 系统代理
新版 Node global agent可通过 NODE_USE_ENV_PROXY=1--use-env-proxy 读取代理环境变量
fetch / Undici通常要看 dispatcher、proxy agent 或 Node 运行时配置
Axios / Got 等 HTTP 客户端常见路径是环境变量或显式传 agent / proxy
Electron 渲染进程 / Chromium 网络栈更可能遵循系统代理
Electron 主进程里的 Node HTTP 请求仍要看 Node 侧配置,不等同于 Chromium

macOS 原生程序大体可以这样分:

类型是否通常走系统代理
URLSession / CFNetwork / WebKit 请求通常会
Safari、基于 WebKit 的网页请求通常会
Electron / Chromium 应用经常会读取系统代理,但具体行为由 Chromium 网络栈决定
Go / Rust / Node 命令行工具不一定,常见是读环境变量
自己实现 socket、DNS、TLS、QUIC 或连接池的程序通常不会,除非显式支持代理

原因很简单:系统代理是应用层约定,不是内核路由规则。

如果一个程序直接做:

connect("api.openai.com:443")

内核只负责帮它连接 api.openai.com:443,不会偷偷改成:

connect("127.0.0.1:7890")

要走系统代理,程序必须主动改成连接代理:

connect("127.0.0.1:7890")
发送 CONNECT api.openai.com:443

这也是为什么有些程序打开系统代理后仍然直连:它根本没有看系统代理这张便签。

在 mihomo 配置里,常见入口是:

port: 7890
socks-port: 7891
mixed-port: 7892

它们的含义不是“HTTP 网站走 port,HTTPS 网站走另一个 HTTPS 端口”。更准确地说:

配置入口协议
portHTTP 代理端口;HTTPS 网站通常也通过 HTTP CONNECT 进入
socks-portSOCKS4 / SOCKS5 代理端口
mixed-port一个端口同时接 HTTP 代理和 SOCKS 代理

“HTTPS 代理”这个说法很容易误导。浏览器访问 https://api.openai.com 时,不是把完整 HTTPS 请求明文交给 mihomo,而是先向 HTTP 代理发送:

CONNECT api.openai.com:443 HTTP/1.1
Host: api.openai.com:443

mihomo 收到 CONNECT 后,知道目标是 api.openai.com:443,于是建立一条隧道。后面的 TLS 握手仍然发生在应用和目标网站之间。默认情况下,mihomo 只是转发加密字节流,并不解密 HTTPS 内容。

mixed-port 的便利在于客户端不用纠结要连 HTTP 端口还是 SOCKS 端口。连接刚进来时,mihomo 看第一个字节:

  • 如果像 SOCKS4 / SOCKS5,就交给 SOCKS 处理。
  • 否则按 HTTP 代理请求处理。

这就是为什么很多客户端只配置一个 mixed-port: 7890 就够了。

假设系统代理已经被客户端设置成:

HTTP Proxy = 127.0.0.1:7890
HTTPS Proxy = 127.0.0.1:7890

mihomo 配置如下:

mixed-port: 7890
mode: rule
proxies:
- name: US-Proxy
type: socks5
server: 203.0.113.10
port: 1080
rules:
- DOMAIN-SUFFIX,openai.com,US-Proxy
- MATCH,DIRECT

现在一个遵循系统代理的应用访问:

https://api.openai.com/v1/models

真实路径是:

应用
-> 读取系统代理,发现 HTTPS 代理是 127.0.0.1:7890
-> 连接 mihomo mixed-port
-> 发送 CONNECT api.openai.com:443
mihomo listener
-> mixed 入口判断这是 HTTP 代理请求
-> HTTP handler 识别 CONNECT
-> 生成 metadata: host=api.openai.com, port=443, network=tcp
mihomo tunnel
-> 进入 rule 模式
-> DOMAIN-SUFFIX,openai.com 命中
-> 选择 US-Proxy
mihomo outbound
-> 连接 203.0.113.10:1080
-> 通过 SOCKS5 告诉远端代理:我要访问 api.openai.com:443
-> 远端代理连接 api.openai.com:443
之后
-> 应用和 api.openai.com 继续 TLS 握手
-> mihomo 在本地连接和远端连接之间双向转发字节

如果应用访问的是:

https://example.org

DOMAIN-SUFFIX,openai.com 不匹配,最后命中:

MATCH,DIRECT

这时 mihomo 不会连接 US-Proxy,而是自己直接拨到 example.org:443,再继续做双向转发。

mihomo 能转发流量,不是因为它拥有系统级魔法,而是因为它实现了一个完整的本地代理服务器和转发管线:

  1. 监听本地端口:例如 127.0.0.1:7890
  2. 理解入口协议:HTTP proxy、HTTP CONNECT、SOCKS4、SOCKS5。
  3. 抽象目标信息:把目标 host、端口、入口类型、来源地址整理成 metadata
  4. 匹配规则:根据域名、IP、GEOIP、进程、兜底 MATCH 等规则选择出口。
  5. 拨出连接:走 DIRECT、代理节点或代理组。
  6. 双向复制字节:把应用连接和远端连接接起来。

这套机制只要求应用把连接交给 mihomo。只要连接已经进来,mihomo 就可以按协议拿到目标地址,然后替应用去建立另一条连接。

所以系统代理模式的核心模型是:

应用不是直接连目标服务器
应用先连本地 mihomo
mihomo 再按规则替应用连目标服务器

本节源码基于 MetaCubeX/mihomo commit 5e22035118d13fa609164670111cc674906bb2a4

  • config.RawConfig:证明 portsocks-portmixed-port 是配置层入口,而不是系统代理开关。 源码:config/config.go

  • executor.updateListeners:配置生效后,mihomo 会按端口创建 HTTP、SOCKS、redir、tproxy、mixed 等监听器。 源码:hub/executor/executor.go

  • listener.ReCreateMixedmixed-port 会创建 mixed TCP listener 和 SOCKS UDP listener。 源码:listener/listener.go

  • mixed.handleConn:mixed 入口通过首字节判断 SOCKS4、SOCKS5 或 HTTP。 源码:listener/mixed/mixed.go

  • http.HandleConn:HTTP 代理识别 CONNECT,然后把连接交给 tunnel;普通 HTTP 请求则通过内部 client 转发。 源码:listener/http/proxy.go

  • socks.HandleSocks5:SOCKS5 握手后拿到目标地址,再交给 tunnel。 源码:listener/socks/tcp.go

  • inbound.NewHTTPS / inbound.NewSocket:入口协议会被统一整理成 metadata。 源码:adapter/inbound/https.goadapter/inbound/socket.go

  • tunnel.resolveMetadata:rule / global / direct 模式会在这里决定使用哪个代理出口。 源码:tunnel/tunnel.go

  • proxy.DialContext 调用点:规则选出的出口最终会负责建立远端连接。 源码:tunnel/tunnel.go

  • 单独写 TUN:虚拟网卡、auto-routedns-hijack 和流量防泄漏。
  • 单独写 DNS:fake-ip、hosts、规则匹配前后的解析时机。
  • 单独写 outbound:DIRECT、SOCKS5 节点、代理组和 DialContext 的关系。