系统代理不是“系统强制劫持所有流量”,而是一组告诉应用该把网络请求交给谁的系统级配置。
在系统代理模式下,mihomo core 的角色很具体:它监听一个本地端口,例如 127.0.0.1:7890;愿意遵循系统代理的应用会主动连接这个端口;mihomo 从 HTTP proxy、HTTP CONNECT 或 SOCKS 握手里拿到目标地址,再用规则选择 DIRECT、REJECT 或某个代理节点,最后在两条连接之间搬运字节。
这篇只讲系统代理流量,不讲 TUN。TUN 的入口不是“应用主动连接本地代理”,而是“虚拟网卡接住网络层流量”,心智模型要另起一篇。
当你在 Clash Verge、Clash Party、mihomo-party 或自己的桌面客户端里点下“系统代理”时,直觉上会以为系统接下来把所有网络流量都转给了 mihomo。
实际不是这样。
系统代理更像一张系统级便签:
HTTP Proxy -> 127.0.0.1:7890HTTPS Proxy -> 127.0.0.1:7890SOCKS Proxy -> 127.0.0.1:7890这张便签不会自动改写所有 connect()。应用如果使用系统网络库,或者主动读取系统代理配置,就会把请求发给 127.0.0.1:7890。应用如果绕开这套机制,直接自己建连接,系统代理就不会天然生效。
应用如何知道系统代理
Section titled “应用如何知道系统代理”在 macOS 上,原生应用通常不是自己读某个配置文件,而是走系统网络框架。
常见路径是:
应用 -> URLSession / WebKit / CFNetwork / SystemConfiguration -> 当前网络服务的代理配置 -> 得到 HTTP / HTTPS / SOCKS / PAC 代理信息Apple 提供的核心接口有两类:
CFNetworkCopySystemProxySettings:读取当前系统 internet proxy settings。CFNetworkCopyProxiesForURL:给定一个 URL 和系统代理设置,返回访问这个 URL 应该使用的代理列表。
官方文档里对 CFNetworkCopyProxiesForURL 的描述很关键:它返回的是“访问指定 URL 应该使用的代理列表”,而且代理字典里会包含代理类型、host 和 port。也就是说,系统代理不是一个单纯的全局端口;遇到 PAC、绕过列表、不同协议时,应用应该按目标 URL 计算最终代理。
Node 程序怎么拿代理端口
Section titled “Node 程序怎么拿代理端口”Node 程序如果只是想知道 macOS 当前系统代理,最直接的方法是调用系统命令:
import { execFileSync } from "node:child_process";
const output = execFileSync("scutil", ["--proxy"], { encoding: "utf8" });console.log(output);典型输出会长这样:
HTTPEnable : 1HTTPProxy : 127.0.0.1HTTPPort : 7890HTTPSEnable : 1HTTPSProxy : 127.0.0.1HTTPSPort : 7890SOCKSEnable : 1SOCKSProxy : 127.0.0.1SOCKSPort : 7890如果要做得更像原生应用,就不要只解析 scutil --proxy,而是通过 native addon、FFI 或平台绑定去调用 CFNetwork。原因是 scutil --proxy 更像拿原始设置;CFNetworkCopyProxiesForURL 才会按具体 URL 处理 PAC、绕过规则和协议差异。
但大多数 Node CLI 还有另一套习惯:环境变量。
HTTP_PROXY=http://127.0.0.1:7890HTTPS_PROXY=http://127.0.0.1:7890ALL_PROXY=socks5://127.0.0.1:7890NO_PROXY=localhost,127.0.0.1这和 macOS 系统代理不是同一个东西。很多命令行工具不读系统设置,只读这些环境变量。
新版 Node 的 http 全局代理也支持环境变量,但需要显式启用:
NODE_USE_ENV_PROXY=1 \HTTP_PROXY=http://127.0.0.1:7890 \HTTPS_PROXY=http://127.0.0.1:7890 \node client.js或者:
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 程序会遵循系统代理
Section titled “哪些 macOS 程序会遵循系统代理”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这也是为什么有些程序打开系统代理后仍然直连:它根本没有看系统代理这张便签。
HTTP、HTTPS 和 mixed-port 的区别
Section titled “HTTP、HTTPS 和 mixed-port 的区别”在 mihomo 配置里,常见入口是:
port: 7890socks-port: 7891mixed-port: 7892它们的含义不是“HTTP 网站走 port,HTTPS 网站走另一个 HTTPS 端口”。更准确地说:
| 配置 | 入口协议 |
|---|---|
port | HTTP 代理端口;HTTPS 网站通常也通过 HTTP CONNECT 进入 |
socks-port | SOCKS4 / SOCKS5 代理端口 |
mixed-port | 一个端口同时接 HTTP 代理和 SOCKS 代理 |
“HTTPS 代理”这个说法很容易误导。浏览器访问 https://api.openai.com 时,不是把完整 HTTPS 请求明文交给 mihomo,而是先向 HTTP 代理发送:
CONNECT api.openai.com:443 HTTP/1.1Host: api.openai.com:443mihomo 收到 CONNECT 后,知道目标是 api.openai.com:443,于是建立一条隧道。后面的 TLS 握手仍然发生在应用和目标网站之间。默认情况下,mihomo 只是转发加密字节流,并不解密 HTTPS 内容。
mixed-port 的便利在于客户端不用纠结要连 HTTP 端口还是 SOCKS 端口。连接刚进来时,mihomo 看第一个字节:
- 如果像 SOCKS4 / SOCKS5,就交给 SOCKS 处理。
- 否则按 HTTP 代理请求处理。
这就是为什么很多客户端只配置一个 mixed-port: 7890 就够了。
一条实际规则的转发路径
Section titled “一条实际规则的转发路径”假设系统代理已经被客户端设置成:
HTTP Proxy = 127.0.0.1:7890HTTPS Proxy = 127.0.0.1:7890mihomo 配置如下:
mixed-port: 7890mode: 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.orgDOMAIN-SUFFIX,openai.com 不匹配,最后命中:
MATCH,DIRECT这时 mihomo 不会连接 US-Proxy,而是自己直接拨到 example.org:443,再继续做双向转发。
Mihomo 为什么能转发流量
Section titled “Mihomo 为什么能转发流量”mihomo 能转发流量,不是因为它拥有系统级魔法,而是因为它实现了一个完整的本地代理服务器和转发管线:
- 监听本地端口:例如
127.0.0.1:7890。 - 理解入口协议:HTTP proxy、HTTP
CONNECT、SOCKS4、SOCKS5。 - 抽象目标信息:把目标 host、端口、入口类型、来源地址整理成
metadata。 - 匹配规则:根据域名、IP、GEOIP、进程、兜底
MATCH等规则选择出口。 - 拨出连接:走
DIRECT、代理节点或代理组。 - 双向复制字节:把应用连接和远端连接接起来。
这套机制只要求应用把连接交给 mihomo。只要连接已经进来,mihomo 就可以按协议拿到目标地址,然后替应用去建立另一条连接。
所以系统代理模式的核心模型是:
应用不是直接连目标服务器应用先连本地 mihomomihomo 再按规则替应用连目标服务器本节源码基于 MetaCubeX/mihomo commit 5e22035118d13fa609164670111cc674906bb2a4。
-
config.RawConfig:证明port、socks-port、mixed-port是配置层入口,而不是系统代理开关。 源码:config/config.go -
executor.updateListeners:配置生效后,mihomo 会按端口创建 HTTP、SOCKS、redir、tproxy、mixed 等监听器。 源码:hub/executor/executor.go -
listener.ReCreateMixed:mixed-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.go、adapter/inbound/socket.go -
tunnel.resolveMetadata:rule / global / direct 模式会在这里决定使用哪个代理出口。 源码:tunnel/tunnel.go -
proxy.DialContext调用点:规则选出的出口最终会负责建立远端连接。 源码:tunnel/tunnel.go
- Mihomo 源码:https://github.com/MetaCubeX/mihomo
- Mihomo 配置示例:https://github.com/MetaCubeX/mihomo/blob/5e22035118d13fa609164670111cc674906bb2a4/docs/config.yaml
- Apple
CFNetworkCopyProxiesForURL:https://developer.apple.com/documentation/cfnetwork/cfnetworkcopyproxiesforurl%28_%3A_%3A%29?language=objc - Node.js built-in proxy support:https://nodejs.org/download/release/latest-jod/docs/api/http.html#built-in-proxy-support
- 单独写 TUN:虚拟网卡、
auto-route、dns-hijack和流量防泄漏。 - 单独写 DNS:fake-ip、hosts、规则匹配前后的解析时机。
- 单独写 outbound:
DIRECT、SOCKS5 节点、代理组和DialContext的关系。