TECH · 9 MIN READ · 6192 字

Clash 系统代理已开启,为什么浏览器仍然直连?

显式指定 7897 端口可以访问,系统代理却始终不生效。沿着浏览器、Mihomo 与活动拨号连接逐层验证,最终定位 WinINET 的连接级代理配置。

FIELD NOTES / 排障速览

故障现象
显式代理正常,浏览器默认直连
定位关键
核对活动 PPPoE 连接的准确名称
修复闭环
连接级配置 → 真实请求 → 开关验证

问题摘要:电脑通过光猫有线连接,并由 Windows PPPoE 拨号上网。Clash Verge Rev 的 System Proxy 显示已开启,Windows 代理页面也显示 127.0.0.1:7897,但 Chrome / Edge 正常启动时未进入 Mihomo;显式指定代理端口却完全正常。最终发现活动 PPPoE 连接的 WinINET 连接级代理配置没有启用手动代理。使用 Windows WinINet API 按准确连接名修复后,浏览器无需指定参数即可访问 Google,Mihomo Connections 出现相应流量,且 Clash 的系统代理开关能够在代理与直连间正常切换。

0. 环境与故障表现

项目 实测环境
系统 Windows,光猫有线接入,Windows PPPoE 拨号
客户端 Clash Verge Rev v2.4.7
内核 Mihomo v1.19.21
本地 HTTP 代理 127.0.0.1:7897
Clash 模式 System Proxy 开、TUN 关、DNS Override 关
活动拨号连接名 Boradband(原中文连接重命名后的拼写;不是 Broadband)
浏览器 Chrome / Edge

初始症状:

  • Clash Verge 的系统代理开关处于开启状态,Windows 设置中也能看到 ProxyEnable=1 和 ProxyServer=127.0.0.1:7897。
  • Chrome 和 Edge 直接打开 Google 失败,Mihomo 的 Connections 没有 Google 记录。
  • curl 显式指定代理、或者给 Chrome 添加 --proxy-server 参数后,Google 可以正常访问。
  • Wi-Fi 环境表现与光猫有线、PPPoE 拨号环境不同,提示可能存在按连接分别存储的代理配置。

排障目标:不依赖 Chrome 固定启动参数,也不启用 TUN;开启 Clash 时浏览器进入 Mihomo,关闭 Clash 系统代理时恢复直连。

1. 先建立正确的网络模型

TRAFFIC FLOW / 请求如何离开浏览器
Chrome / Edge↓ Windows 按当前连接与 URL 选择代理
01 / 系统代理开启且配置有效↓ 127.0.0.1:7897

Mihomo Rule 决策
DIRECT → PPPoE Boradband
PROXY → 代理节点

02 / 系统代理关闭或选择 DIRECT↓ Windows 原始网络

绕过本地 HTTP 代理
经 PPPoE Boradband 出站

↓ INTERNET / 目标网站

查看原始 Mermaid 流程图代码
graph TD
    A[Chrome / Edge] --> B{Windows 对当前 URL 的代理选择}
    B -->|System Proxy ON 且配置有效| C[127.0.0.1:7897]
    B -->|System Proxy OFF / 选择 DIRECT| D[Windows 原始网络]
    C --> E[Mihomo Rule]
    E -->|DIRECT| F[PPPoE Boradband]
    E -->|PROXY| G[代理节点]
    D --> F
    G --> H[Internet]
    F --> H

核心区分:Clash 的 System Proxy 是应用层的代理配置,并不等同于网卡级拦截;ping 使用 ICMP,通常不会经过 HTTP 代理。PPPoE 是逻辑拨号接口,虽然底层借助以太网,但可以拥有与默认 LAN 不同的代理配置。

2. 确认代理服务器与节点本身无故障

先确认本地端口是否监听:

Get-NetTCPConnection -LocalPort 7897 -State Listen

再明确要求 curl.exe 使用代理,而不是依赖 Windows 系统代理:

curl.exe -4 -I -x http://127.0.0.1:7897 https://www.google.com
curl.exe -4 -I -x http://127.0.0.1:7897 https://www.youtube.com

本次实测:返回 HTTP/1.1 200 Connection established,随后网站返回成功状态。说明 7897 → Mihomo → 代理节点 → 目标网站 这条链路能够工作。

注意:curl.exe 不一定会自动采用 Windows 的 WinINET 系统代理,所以“不带 -x 的 curl 失败”不能直接证明 System Proxy 的功能异常。

随后给 Chrome 临时增加启动参数进行对照:

--proxy-server=http://127.0.0.1:7897

结果:Google 可以打开,Mihomo Connections 能看到连接;百度匹配 DIRECT。这同时验证了浏览器访问本地代理能力、Mihomo 规则和出站链路。后续诊断应聚焦“系统代理配置如何被应用读取”,不再反复排查节点。

3. 默认系统代理“看起来正常”,不等于拨号连接代理生效

检查通常可见的 WinINET 注册表值:

Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' |
    Format-List ProxyEnable,ProxyServer,AutoConfigURL,ProxyOverride

当时输出类似:

ProxyEnable   : 1
ProxyServer   : 127.0.0.1:7897
AutoConfigURL :
ProxyOverride : localhost;127.*;192.168.*;10.*;172.16.*;...;<local>

ProxyOverride 没有包含 Google,也没有全局通配符 *。然而,Chrome / Edge 不带参数时仍未进入 Mihomo。

使用 .NET 系统代理解析器做只读交叉验证:

[System.Net.WebRequest]::GetSystemWebProxy().GetProxy(
    [Uri]'https://www.google.com'
).AbsoluteUri

修复前返回:

https://www.google.com/

这里的原始 URL 表示该解析器对这个 URL 选择 DIRECT。它不是真正的 HTTP 请求,不代表 Google 已经连通;也不能单独代表所有浏览器的行为,但与 Chrome / Edge 未进入代理的现象一致。

netsh winhttp show proxy 显示“直接访问”也不足以否定浏览器系统代理:WinHTTP 代理与用户的 WinINET/Internet Settings 配置不是同一个设置来源。

4. 查出真正的 PPPoE 连接名称

先列出当前活动的 Windows 拨号连接:

rasdial

本次最终确认:

已连接
Boradband

注意拼写差异:

  • Boradband:真正活动的旧 PPPoE 连接,名字是重命名后留下的拼写。
  • Broadband:此前尝试创建另一连接时留下的独立名称/相关代理记录。
  • 不能仅凭注册表中存在某个名称,就认定对应拨号连接仍在使用。

也可以查看用户电话簿中的拨号条目(只输出名称,不涉及密码):

$pbk = "$env:APPDATA\Microsoft\Network\Connections\Pbk\rasphone.pbk"
Select-String -Path $pbk -Pattern '^\[.+\]$' |
    ForEach-Object { $_.Line }

本次 rasphone.pbk 中存在 [Boradband];但最终确定活动连接仍以 rasdial 为准。

5. 读取 WinINET 的“连接级”代理标志

Windows 允许 LAN、拨号和 VPN 连接使用各自的代理设置。相关二进制配置位于:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections

可用只读 PowerShell 列出各连接代理标志:

$path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections'
$key = Get-Item $path

foreach ($name in $key.GetValueNames()) {
    $data = $key.GetValue($name)
    if ($data -is [byte[]] -and $data.Length -ge 12) {
        $flags = [BitConverter]::ToUInt32($data, 8)
        [PSCustomObject]@{
            Name         = $name
            Flags        = ('0x{0:X2}' -f $flags)
            ProxyEnabled = [bool]($flags -band 0x02)
            AutoConfig   = [bool]($flags -band 0x04)
            AutoDetect   = [bool]($flags -band 0x08)
        }
    }
}

关键实测结果:

名称 Flags 解释
Boradband(活动连接) 0x08 自动检测开启,手动代理未开启
Broadband(非活动的测试名称) 0x03 直连能力 + 手动代理开启
DefaultConnectionSettings 0x03 默认连接已启用手动代理

常用标志:0x01 = PROXY_TYPE_DIRECT,0x02 = PROXY_TYPE_PROXY,0x04 = 自动脚本,0x08 = 自动检测;0x03 通常表示 0x01 | 0x02。上面的偏移 8 是针对本次 WinINET Connections 二进制格式的诊断读取,不应推广为所有 Windows 网络配置的通用格式。

到这里,故障已经有了非常明确的解释:默认代理配置和错误名称 Broadband 都已开启手动代理,但真正活动的 Boradband 没有。这比“Windows 代理设置显示开启”更有诊断价值。

6. 修复:通过 WinINet API 精确设置活动拨号连接

6.1 先备份

reg.exe export "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" "$env:USERPROFILE\Connections-before-fix.reg" /y

备份文件保存在当前用户主目录。不要直接删除 Boradband、DefaultConnectionSettings 或 SavedLegacySettings;也不要在不了解二进制结构的情况下手改字节。

6.2 用 InternetSetOption 写入指定连接

微软提供 INTERNET_OPTION_PER_CONNECTION_OPTION(值为 75),用于针对特定拨号连接更新代理配置。本次通过 PowerShell 调用 WinINet API,写入三个选项:手动代理标志、代理服务器、绕过列表,并通知设置变更。

仅适用于连接名称与代理端口已确认的环境;执行前把 Boradband 改成你实际的 rasdial 连接名。

发布校订:以下示例依据微软结构定义补齐 dwOptionError 与完整的选项值联合体,避免将指针大小误当成原生联合体大小。文章保留原案例实测结论;校订后的代码未在本次发布中执行或进行网络配置写入验证。

$source = @'
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

public static class DialProxyFix {
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct ConnList {
        public int dwSize;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string pszConnection;
        public int dwOptionCount;
        public int dwOptionError;
        public IntPtr pOptions;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct OptionValue {
        [FieldOffset(0)] public uint dwValue;
        [FieldOffset(0)] public IntPtr pszValue;
        [FieldOffset(0)] public System.Runtime.InteropServices.ComTypes.FILETIME ftValue;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct ConnOption {
        public int dwOption;
        public OptionValue value;
    }

    [DllImport("wininet.dll", EntryPoint="InternetSetOptionW", SetLastError=true)]
    static extern bool SetConnection(
        IntPtr handle, int option, ref ConnList list, int length);

    [DllImport("wininet.dll", EntryPoint="InternetSetOptionW", SetLastError=true)]
    static extern bool Notify(
        IntPtr handle, int option, IntPtr buffer, int length);

    public static void Enable(string connection, string proxy, string bypass) {
        int itemSize = Marshal.SizeOf(typeof(ConnOption));
        IntPtr items = Marshal.AllocHGlobal(itemSize * 3);
        IntPtr server = Marshal.StringToHGlobalUni(proxy);
        IntPtr exclusions = Marshal.StringToHGlobalUni(bypass);

        try {
            ConnOption[] options = {
                new ConnOption { dwOption=1, value=new OptionValue { dwValue=3 } },
                new ConnOption { dwOption=2, value=new OptionValue { pszValue=server } },
                new ConnOption { dwOption=3, value=new OptionValue { pszValue=exclusions } }
            };

            for (int i = 0; i < options.Length; i++) {
                Marshal.StructureToPtr(
                    options[i], IntPtr.Add(items, i * itemSize), false);
            }

            ConnList list = new ConnList {
                pszConnection=connection,
                dwOptionCount=3,
                pOptions=items
            };
            list.dwSize = Marshal.SizeOf(typeof(ConnList));

            if (!SetConnection(IntPtr.Zero, 75, ref list, list.dwSize))
                throw new Win32Exception(Marshal.GetLastWin32Error());

            if (!Notify(IntPtr.Zero, 39, IntPtr.Zero, 0))
                throw new Win32Exception(Marshal.GetLastWin32Error());
            if (!Notify(IntPtr.Zero, 37, IntPtr.Zero, 0))
                throw new Win32Exception(Marshal.GetLastWin32Error());
        }
        finally {
            Marshal.FreeHGlobal(items);
            Marshal.FreeHGlobal(server);
            Marshal.FreeHGlobal(exclusions);
        }
    }
}
'@

Add-Type -TypeDefinition $source

$bypass = (Get-ItemProperty `
    'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings').ProxyOverride

[DialProxyFix]::Enable('Boradband', '127.0.0.1:7897', $bypass)

代码里的 3 是 PROXY_TYPE_DIRECT | PROXY_TYPE_PROXY,不是“强制所有流量走节点”;站点进入 Mihomo 后仍由 Rule 决定 DIRECT 还是 PROXY。这段代码调整的是 WinINET 代理选项,不是 PPPoE 拨号账号、DNS 或路由。若同一 PowerShell 会话重复执行 Add-Type,可能出现类型已存在的错误,应使用新会话或避免重复定义。

本次曾尝试在 inetcpl.cpl → 连接 → 拨号连接设置 中勾选代理,但设置无法稳定保留。删除部分乱码名称注册表记录也没有立即解决问题。最终起作用的是针对准确的活动连接名写入连接级代理选项。不要把“删除乱码项”当作必经步骤。

7. 修复验证:三层闭环

第一层:系统代理解析

[System.Net.WebRequest]::GetSystemWebProxy().GetProxy(
    [Uri]'https://www.google.com'
).AbsoluteUri

修复后、System Proxy 开启时返回:

http://127.0.0.1:7897/

第二层:浏览器真实请求

Chrome 不添加 --proxy-server 参数,正常打开 Google:网页可以访问;Mihomo Connections 出现 Google 请求。这比单独看注册表或 .NET 代理解析器更有说服力。

第三层:Clash 开关同步

操作 .NET 代理解析器返回 实测判断
关闭 Clash System Proxy https://www.google.com/ 选择直连
重新开启 Clash System Proxy http://127.0.0.1:7897/ 选择代理

上述开关行为已实测成功。由此确认:本次的浏览器系统代理接入问题已修复。不过,完整退出并重新启动 Clash/Windows 后的持久性属于另一个可选回归测试,不能凭开关成功自动推断已通过重启测试。

8. 重要误区与经验

  1. ProxyEnable=1 ≠ 活动拨号连接已经启用代理。 先核实 rasdial 的实际连接名称,再查询对应连接的 WinINET 代理选项。
  2. Broadband ≠ Boradband。 字符串哪怕只差一个字母,都会对应不同的连接级配置。旧名称、中文乱码或测试残留项不能仅凭外观直接删除。
  3. ping google.com 丢包 ≠ HTTP 代理不可用。 Ping 走 ICMP,不走本地 HTTP 代理。实测 Ping 超时,但 Chrome 代理已恢复正常。
  4. 显式 curl.exe -x 是重要的隔离实验。 能迅速证明本地端口、Mihomo 和代理节点是否可用。
  5. System Proxy 与 TUN 不是一回事。 本问题最后不需要启用 TUN;TUN、Fake-IP 和 DNS 复写不是本次修复的必要条件。
  6. DNS 问题独立处理。 排查期间发现明文 DNS 查询和经代理 DoH 的结果不同,但它不能直接解释“请求完全不进入 Mihomo”。不要把两个故障混成一个。
  7. 浏览器访问成功还不够。 应结合 Mihomo Connections 确认确实进入代理服务,再测试开关关闭后能否恢复 DIRECT。

9. 可复用的故障决策表

步骤 检查 不同结果对应方向
① Get-NetTCPConnection -LocalPort 7897 -State Listen 未监听:查 Mihomo 启动、端口;已监听:继续
② curl.exe -x http://127.0.0.1:7897 ... 失败:查节点/出站;成功:排除代理服务本身
③ Chrome 临时 --proxy-server 成功:重点查系统代理接入;失败:查浏览器到本地端口
④ rasdial 确认真实 PPPoE 连接名,防止修错条目
⑤ Connections 下每连接 flags 活动连接无 0x02:修复连接级代理;有:进一步查配置解析
⑥ GetSystemWebProxy().GetProxy(...) 返回原 URL:该解析器选择直连;返回 7897:代理选择正确
⑦ Chrome + Mihomo Connections 有连接才证明实际进入 Mihomo
⑧ 关闭/开启 System Proxy 两轮验证 确认能自动从 DIRECT 切换为代理并恢复

10. 回滚与风险控制

  • 修改前导出 Connections 注册表键;不要随意删除活动连接的注册表值或 PPPoE 拨号配置。
  • 如需撤销手工修改,可在了解覆盖范围后使用备份 .reg 恢复;注册表导入会覆盖该键内的现有值,可能撤销此后产生的有效代理更改,操作前应确保备份可靠。
  • 当前配置中已验证的自动切换功能,不需要再手动清理 Boradband。
  • 公开博客截图请打码:PPPoE 拨号账号、密码、代理订阅地址、节点凭据等。

11. 最终结论

本次故障不是 Mihomo 代理节点不可用,也不是浏览器无法连接 7897,而是 Windows 活动 PPPoE 连接 Boradband 的连接级 WinINET 代理选项未启用手动代理。默认系统代理值和另一条错误名称 Broadband 的记录虽然正常,却不能代表当前拨号连接。

通过官方 WinINet API 对真实活动连接名写入代理设置并刷新配置后,Windows 系统代理解析器返回 127.0.0.1:7897,Chrome 无启动参数可以打开 Google,Mihomo Connections 出现请求,Clash System Proxy 开/关分别对应代理/直连。这是本次已验证的修复闭环。

尚未证明的部分:Clash Verge 最初为何没有同步写好该连接的配置(连接名称编码、历史残留、版本行为等均可能参与);不能仅凭本次修复断言某个程序版本存在确定缺陷。

参考资料

  1. Microsoft Learn:设置和检索 Internet 选项(按连接配置 WinINet)
  2. Microsoft Learn:INTERNET_PER_CONN_OPTION 结构
  3. Microsoft Learn:INTERNET_OPTION_PER_CONNECTION_OPTION(75)等选项
  4. Microsoft Learn:WinHttpGetIEProxyConfigForCurrentUser(活动连接代理)
  5. Clash Verge Rev Windows FAQ:宽带拨号与系统代理(文档标注“2.0 版以后无此问题”;本案例并不直接证明该已修复问题复现)