为什么要用 external-controller API 自动切换节点
在日常使用中,Clash 的节点切换通常依赖桌面客户端:打开 Clash Verge、Clash Verge Rev 或 Mihomo,进入代理组,再手动点击某个节点。这种方式适合临时操作,却不适合服务器、远程开发机、家庭网关或需要长期无人值守运行的环境。只要节点发生超时、出口 IP 被限制,或者当前线路在晚高峰明显变慢,人工切换就会变成持续的运维负担。
external-controller API 提供了另一条路径。它允许脚本通过 HTTP 请求读取代理组、节点状态、延迟测试结果,并向指定策略组写入新的选择结果。换句话说,Clash 负责维护连接、规则和出站协议,Python 或其他自动化程序负责根据健康检查结果做决策。两者职责分开后,节点自动切换就不再是「定时随机换一个」,而可以变成有条件、有记录、可回滚的调度流程。
一个可靠的方案至少要回答四个问题:当前策略组有哪些候选节点;怎样判断某个节点真的可用;什么时候应该触发切换;切换失败后如何恢复到上一次正常节点。只测一个公共网址的延迟并不等于业务可用,因为 DNS、TLS、HTTP 响应和长连接都可能在不同阶段失败。本文以 Mihomo 兼容的 external-controller 为基础,演示一套适合个人服务器和小型内网环境的实现思路。
127.0.0.1,或仅允许可信内网访问,并为接口设置强随机密钥。自动化脚本应保存密钥权限,避免把控制权写进公开仓库、容器镜像或普通日志。
配置 external-controller 与可控的策略组
在 YAML 配置中,先确认控制器地址、端口和密钥。不同版本的字段名称可能略有差异,常见写法如下。若当前客户端已经生成了这些字段,不要机械地重复添加;应先检查是否存在重复键,因为 YAML 中同名键的实际解析结果可能由内核实现决定,容易造成「文件看起来正确、运行行为却不一致」的问题。
external-controller: 127.0.0.1:9090
secret: "replace-with-a-long-random-secret"
proxy-groups:
- name: AUTO_SELECT
type: select
proxies:
- node-a
- node-b
- node-c
- DIRECT
external-controller 的地址用于让脚本找到控制接口,secret 则通常通过 Authorization: Bearer 请求头传递。自动切换脚本所操作的对象不是某个普通节点,而是 proxy-groups 中的策略组。因此,策略组名称必须稳定,建议使用不含空格和表情符号的内部名称,例如 AUTO_SELECT、SERVER_EGRESS 或 AI_TRAFFIC。界面显示名可以保持友好,但脚本依赖的组名应尽量避免随订阅更新改变。
如果节点来自远程订阅,节点名称可能包含地区、倍率、流量或动态编号。脚本不要把所有名称硬编码为永久不变的字符串,而应在启动时通过 API 读取组成员,再根据关键词、正则表达式或维护文件筛选候选。例如,可以排除名称中包含「过期」「剩余 0」「维护」的节点,也可以只保留指定国家或线路类型。这样,当订阅新增节点时,自动切换逻辑不必每次都重新改代码。
| 项目 | 建议做法 | 常见风险 |
|---|---|---|
| 监听地址 | 服务器本机使用 127.0.0.1 | 绑定 0.0.0.0 后被公网扫描 |
| 控制密钥 | 使用随机长字符串并限制文件权限 | 写入 Git、Shell 历史或公开日志 |
| 策略组名称 | 使用稳定、可预测的英文标识 | 订阅更新后组名改变导致 404 |
| 候选节点 | 启动时读取并动态过滤 | 永久硬编码已失效的节点名称 |
读取节点状态并设计健康检查
自动切换的核心不是「调用接口」,而是定义什么叫作健康。通常可以先读取当前配置中的代理组和节点信息,再对候选节点执行延迟测试。常见 API 路径包括读取代理组的 /proxies/{name}、发起延迟测试的 /proxies/{name}/delay,以及通过 PUT /proxies/{group} 切换策略组。实际可用字段应以当前 Mihomo 版本的 API 响应为准,建议先使用 curl 查看返回 JSON,而不是直接假设所有 Clash 分支的字段完全相同。
curl -H "Authorization: Bearer YOUR_SECRET" \
http://127.0.0.1:9090/proxies/AUTO_SELECT
curl -X GET \
-H "Authorization: Bearer YOUR_SECRET" \
"http://127.0.0.1:9090/proxies/node-a/delay?url=https%3A%2F%2Fwww.gstatic.com%2Fgenerate_204&timeout=5000"
延迟测试地址应尽量选择稳定、响应体很小、与实际使用方向相近的目标。若脚本服务的是服务器上的 API 请求,不能只用本地局域网地址;若服务的是国内办公流量,也不应只用海外站点判断线路。更稳妥的方式是准备一到两个固定探针,并为一次检查设置连接超时、读取超时和整体超时。探针能够建立 TCP 连接但返回 HTTP 403 时,是否算失败要根据业务定义决定,不能把所有非 2xx 响应都简单归为节点故障。
健康检查最好分成快速指标和确认指标。快速指标可以是 API 返回的毫秒延迟,用于筛掉明显不可用的节点;确认指标则要求连续两次或三次请求成功,避免因为瞬时丢包就切换。对于长时间运行的服务器,还应记录最近一次成功时间、连续失败次数、最后一次延迟和当前选择结果。这样,脚本重启后可以读取状态文件,知道此前哪条线路正在服务,而不是立即对所有节点进行激进切换。
避免频繁抖动的判定规则
节点自动切换最常见的问题是抖动:节点 A 测得 180 毫秒,节点 B 测得 170 毫秒,下一轮结果又反过来,脚本每分钟切换一次,最终让已有连接不断重建。解决方法是设置最小收益阈值和冷却时间。例如,只有新节点比当前节点快至少 30 毫秒,且当前节点连续失败两次,或者当前节点延迟超过 800 毫秒时,才允许切换;每次切换后至少等待五分钟再做下一次决策。
还可以采用主备模型,而不是每次都选择绝对最低延迟。把节点分成首选、备用和禁用三类:首选节点连续失败后进入备用列表,恢复稳定后也不要立即抢回主线路,等它连续通过多轮检查再恢复。对于支付、SSH、数据库同步等不能频繁断线的业务,宁可保留一个延迟略高但稳定的节点,也不要追逐每一轮测速中的最低值。
用 Python 实现读取、测速与自动切换
下面的示例使用 Python 标准库完成请求,适合放在服务器的虚拟环境、systemd 服务或定时任务中。脚本包含三个重要原则:所有请求都设置超时;切换前再次确认候选节点仍存在;切换动作写入日志并保存旧值。示例中的地址、策略组和探针仅供说明,部署前应根据当前配置修改。
import json
import logging
import os
import time
from urllib.parse import quote
from urllib.request import Request, urlopen
CONTROLLER = os.getenv("CLASH_CONTROLLER", "http://127.0.0.1:9090")
SECRET = os.environ["CLASH_SECRET"]
GROUP = os.getenv("CLASH_GROUP", "AUTO_SELECT")
PROBE_URL = os.getenv("CLASH_PROBE_URL", "https://www.gstatic.com/generate_204")
TIMEOUT = 5000
MIN_IMPROVEMENT = 30
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
def request_json(method, path, payload=None):
body = None
headers = {"Authorization": f"Bearer {SECRET}"}
if payload is not None:
body = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json"
request = Request(
CONTROLLER + path,
data=body,
headers=headers,
method=method
)
with urlopen(request, timeout=8) as response:
return json.loads(response.read().decode("utf-8"))
def get_group():
return request_json("GET", "/proxies/" + quote(GROUP, safe=""))
def test_delay(name):
path = (
"/proxies/" + quote(name, safe="") +
"/delay?url=" + quote(PROBE_URL, safe="") +
"&timeout=" + str(TIMEOUT)
)
try:
result = request_json("GET", path)
return int(result["delay"])
except Exception as exc:
logging.warning("probe failed for %s: %s", name, exc)
return None
def switch_to(name):
request_json(
"PUT",
"/proxies/" + quote(GROUP, safe=""),
{"name": name}
)
logging.info("switched %s to %s", GROUP, name)
def main():
group = get_group()
current = group.get("now")
candidates = [
name for name in group.get("all", [])
if name not in {"DIRECT", "REJECT"} and name != GROUP
]
results = {name: test_delay(name) for name in candidates}
healthy = {
name: delay for name, delay in results.items()
if delay is not None
}
if not healthy:
logging.error("no healthy candidate")
return
best = min(healthy, key=healthy.get)
current_delay = healthy.get(current)
best_delay = healthy[best]
if current not in healthy or best_delay + MIN_IMPROVEMENT < current_delay:
switch_to(best)
else:
logging.info("keep %s, current=%s best=%s", current, current_delay, best_delay)
if __name__ == "__main__":
main()
生产环境使用时,不建议把密钥直接写在脚本常量中。可以通过环境变量、systemd 的受保护凭据或权限为 600 的配置文件注入。脚本还应处理 HTTP 401、404、409、连接拒绝和 JSON 格式异常:401 通常表示密钥不正确,404 可能是策略组或节点名称不存在,连接拒绝则可能意味着内核未启动或控制端口配置错误。把这些错误全部当作「节点变慢」会让脚本错误地反复切换,甚至把故障扩大。
需要注意的是,延迟测试往往会并发访问多个节点。如果节点数量很多,服务器可能在短时间内创建大量连接,订阅商也可能把这种行为识别为异常探测。可以限制候选数量、串行测试,或使用小规模并发池,并在两次扫描之间设置间隔。对于只有三到五条候选线路的小型环境,串行测试通常已经足够,代码更容易审计,也更容易判断到底是哪一个请求失败。
回滚、日志与服务器长期运行
自动切换必须把「切换成功」和「业务恢复」区分开。API 返回成功,只能说明策略组当前选择字段已经改变,并不能证明新节点可以承载真实业务。切换后应等待一个短暂观察窗口,再使用实际业务探针验证,例如请求一个轻量 HTTPS 地址、检查特定 API 的状态码,或从连接日志确认新连接确实经过目标出站。如果验证失败,脚本应把策略组恢复为切换前保存的节点,而不是继续寻找下一条线路。
一个简单的状态文件可以保存当前节点、上一个节点、最近切换时间和连续失败次数。状态文件写入时应采用临时文件加原子替换,避免进程被终止后留下半截 JSON。日志中不要打印完整控制密钥,也不要记录包含 Cookie、Authorization 或用户令牌的请求头。建议至少记录时间、策略组、旧节点、新节点、测速结果、触发原因和回滚结果,方便你在第二天回看「为什么昨晚发生了切换」。
在 Linux 服务器上,可以让 systemd timer 每隔几分钟调用脚本,也可以由常驻进程执行循环。定时任务更容易限制资源和重启,常驻进程则能维护连续失败计数。无论采用哪种方式,都要设置锁,防止上一次扫描尚未结束时下一次任务再次启动。还应为切换频率设置上限,例如一小时最多切换三次;达到上限后进入保护状态,保留最后一个可用节点并发出告警,避免所有候选都不稳定时不停地产生控制请求。
运维建议:先用只读模式运行脚本一到两天,只记录候选节点、延迟和预期动作,不真正调用切换接口。确认探针、阈值和候选过滤没有误判后,再开启自动切换。这样可以把「脚本逻辑错误」与「线路本身不稳定」分开,降低上线时突然中断业务的风险。
如果你还需要从浏览器或远程管理机调用控制器,应在 Clash 前增加访问控制,而不是直接把 9090 端口映射到公网。可以使用 SSH 端口转发、内网 VPN 或受限反向代理,并额外限制来源地址和请求方法。external-controller 具备切换节点、读取配置和查看运行状态的能力,泄露后影响远大于普通代理端口暴露,因此安全设计应当和自动化逻辑同等重要。
相比只提供手动下拉选择的轻量客户端,部分旧版 Clash 壳层缺少稳定的 API 管理入口,遇到服务器断线时只能依赖人工登录桌面;而单纯用定时脚本随机轮换节点的方案又没有健康阈值、冷却时间和回滚机制。Clash V.CORE 在 external-controller、策略组管理、节点测速与日志观测之间提供了更完整的自动化基础,适合把本文的 Python 调度流程落到桌面、家用网关或无界面服务器上;如果你希望少维护一套兼容性补丁,可以前往下载 Clash V.CORE,再按照实际环境启用 API 自动切换。