所有网络通道都不通?Mihomo 代理故障排查与脚本直连降级

2026-09-02

所有网络通道都不通?Mihomo 代理故障排查与脚本直连降级

背景:一次简单的天气查询

有人在群里问了一句「明天杭州天气怎么样」。我的自动化助手按惯例调用本地天气脚本,脚本里配了代理,准备去请求公开天气 API。

本来这是一次再普通不过的查询,结果演变成一场「断网恐慌」。所有网络通道都失败了,看起来整个网络都挂了。

现象:所有通道都不通

当时实际测到的情况是这样的:

通道 结果 错误
天气脚本(wttr.in) ❌ 3 次重试全失败 Errno 111 Connection refused
天气脚本(Open-Meteo) ❌ 同样失败 Errno 111 Connection refused
备用代理(NAS 上的代理) ⛔ 被审批拦截 命令未授权
网页搜索(自建实例) ❌ 超时 timed out
搜索重试 + 备用搜索 ❌ 失败 keyless 救援也失败
浏览器自动化 ❌ 服务未运行 Cannot connect

一个接一个的失败堆叠在一起,结论很自然地指向「网络全挂了」。

问题在于,这个结论是错的。

转折:不走代理不行吗

关键转折是有人反问了一句:「不走代理不行吗?」

这句话点破了盲区。排查过程从本地代理试到备用代理、再到搜索、再到浏览器,唯独没试过「不经过任何代理,直接连」。

而天气 API(wttr.in / Open-Meteo)恰恰是国内直连完全可达的公开服务,根本不需要代理。测试一下:直连请求 200 返回,耗时不到 1 秒。

网络一直是好的。挂掉的只是本地代理进程。

根因:代理挂了,被误判成断网

事后确认根因链是这样的:

  • 天气脚本默认走本地代理 http://127.0.0.1:7890
  • 本地 Mihomo 代理进程挂掉了,7890 端口没有服务在监听
  • 脚本所有请求被本地拒绝 → Errno 111 Connection refused
  • 「本地端口拒绝连接」被错误地解读成「网络不通」

这里有个容易踩的坑:Errno 111 Connection refused 表示本地端口没有服务监听,不是网络不通。 网络是通的,只是你要连的那个本地代理服务死了。

对比一下真正断网的报错长什么样:

错误 含义
Errno 111 Connection refused 目标端口无服务监听(通常是本地代理挂了)
Name or service not known DNS 解析失败
Network is unreachable 路由不通
Connection timed out 请求超时(可能被墙或路由不通)

另一个教训是:读错误信息要看连接对端是本地还是远程。 127.0.0.1:7890 refused 是本地代理问题,8.8.8.8 refused 才是外网问题。当时盯着「refused」以为是外网的事,其实错误信息里明明白白写着本地地址。

修复:给脚本加直连兜底

问题找到了,修复就简单了。给天气脚本的 fetch_url() 加一个代理失败自动直连兜底:

import urllib.request
import os

# 代理配置:优先从环境变量读取,默认本地代理
PROXY_URL = os.environ.get("HERMES_PROXY", "http://127.0.0.1:7890")
# 环境变量开关:HERMES_NO_PROXY=true 强制直连
USE_PROXY = os.environ.get("HERMES_NO_PROXY", "false").lower() != "true"

def fetch_url(url, timeout=8):
    """带代理的 URL 请求(单次,无重试),代理失败自动降级直连"""
    attempts = []
    if USE_PROXY:
        proxy_handler = urllib.request.ProxyHandler(
            {"http": PROXY_URL, "https": PROXY_URL}
        )
        attempts.append(urllib.request.build_opener(proxy_handler))
    attempts.append(urllib.request.build_opener())  # 直连兜底

    last_err = None
    for i, opener in enumerate(attempts):
        try:
            return opener.open(url, timeout=timeout)
        except Exception as e:
            last_err = e
            if i == 0 and len(attempts) > 1:
                continue  # 代理阶段失败 → 降级直连
    raise last_err

核心逻辑就是一个 attempts 列表:先放「代理 opener」,再放「直连 opener」。代理抛异常就自动用直连重试一次,全程无感。

这个小改动带来的效果是:代理进程死掉时,脚本不再全线报错,而是静默降级直连,天气照样能查。

通用方法论:三步定位「代理故障 vs 真断网」

这次经历可以提炼成一个通用的三步定位法,任何用代理的开发、运维、自建服务场景都适用:

# 1. 先看本地代理端口是否监听(Connection refused 的元凶)
ss -tlnp | grep 7890        # 无输出 = 代理没跑

# 2. 直连测一个国内可达的站点(不依赖代理)
curl -s -o /dev/null -w '%{http_code}' https://www.baidu.com

# 3. 再测被墙站点走代理
curl -x http://127.0.0.1:7890 -s -o /dev/null -w '%{http_code}' https://www.google.com

判定规则很简单:

  • 第 1 步失败 + 第 2 步成功 → 代理挂了,网络正常 → 走直连降级
  • 第 2 步也失败 → 真断网 → 查路由器 / 出口

这套顺序强调一个原则:测试通道时把「直连」列为独立项,不要默认所有请求都需要代理。 代理只对「被墙的站点」是必要路径(Google、GitHub API 等);国内直连可达的公开服务(天气 API、大部分国内站点)走代理反而引入单点故障——代理一挂,依赖代理的脚本全挂。

踩坑总结

这次排查踩了这些坑,都值得记住:

  1. 代理挂了被误判为断网。脚本全链路走代理,代理进程死掉 → 所有请求 refused → 误判「网络全挂」,实际只是本地代理服务没跑。
  2. 漏掉「直连」通道。多通道排查时测了本地代理、备用代理、搜索、浏览器,唯独没测直连。而目标站点根本不需要代理。
  3. 备用代理也被审批拦截。换备用代理的命令在安全审批环节被卡,代理路径雪上加霜,进一步强化「全挂」错觉。
  4. 搜索通道的连带失败。搜索失败可能是独立原因(自建实例本身的问题),但它在当时被当成了「网络全挂」的又一个佐证。
  5. 错误信息误导。Connection refused 让人以为是外网问题,实际是本地 127.0.0.1:7890 拒绝连接——看错误信息要先分清对端是本地还是远程。

总结

代理是加速器,不是必要路径。把「代理挂了」和「断网」当成两回事,排查网络问题时把直连作为独立通道列入测试清单,再给关键脚本加一个「代理失败→直连」的兜底——这三点能做到,下次代理进程意外退出时,你的脚本会像什么都没发生一样继续工作。

另外提一句,代理进程属于「易碎基础设施」,除了兜底代码,给它配个自启动或看门狗(systemd 的 Restart 策略,或一个一键拉起的守护脚本)会更省心。两者配合,代理相关的故障基本就翻不了天。

标签: 代理 排障 Python