按实际连接追踪:流量是否进入 Mihomo、带着什么域名/IP、命中哪条规则、最终经过哪个代理。配置文件、运行配置和现有连接可能属于三个不同时间点。
先确定实际配置路径与 controller
配置文件要与运行实例对应
systemctl cat mihomo.service
确认二进制与 -d/-f 路径。容器或 OpenWrt 插件部署应查其管理入口,不能同时用本例 systemd 修改另一份配置。
systemctl status mihomo.service -- no-pager
需要完整日志另用 journalctl -u mihomo.service -b -n 100 --no-pager;配置文件存在不代表此进程正在使用它。
mihomo - v
应使用 ExecStart 指向的同一二进制;同名 PATH 命令可能是另一版本。
mihomo - t - f /opt/mihomo/mihomo_config/config.yaml
路径与工作目录按真实部署调整,尤其相对 rule-provider 路径。失败先修复;通过后还需运行 API、订阅与业务验证。
路径与 127.0.0.1:9999 是本手册保留的部署示例,不是 Mihomo 默认值。以 ExecStart 的 -d/-f、external-controller 和 secret 为准;API 不应无鉴权暴露到公网。配置检查不证明订阅、DNS 或代理目标实时可用。
在客户端加载认证后再读 API
以下在能够访问 controller 的客户端执行,示例 controller 位于本机 9999。通过当前 shell 的受控密钥加载方式设置 MIHOMO_SECRET;也可在终端交互读取,不将它写入历史。每个请求检查该变量非空。
read - r - s MIHOMO_SECRET
输入后按 Enter;这是设置变量的一种方式,使用密钥管理器时不重复执行。结束本轮操作后可 unset MIHOMO_SECRET 清除当前 shell 变量。
curl - fsS - H " Authorization: Bearer $ { MIHOMO_SECRET :? Load the controller secret in this shell } " http://127.0.0.1:9999/version
401 先核对 secret,连接拒绝查监听地址/端口;若状态码需要详细检查,临时加 -i 展示响应头。
curl - fsS - H " Authorization: Bearer $ { MIHOMO_SECRET :? Load the controller secret in this shell } " http://127.0.0.1:9999/configs | jq ' {mode,port,"mixed-port": .["mixed-port"],tun}'
需要 jq;重点比较 mode、mixed-port 与 tun 是否符合预期。文件内容与运行值不同,先确认读取的是同一 controller。
重载前备份源文件,确认新配置在进程允许的工作目录/SAFE_PATHS 中。运行时 PATCH 不等于把变更保存回源文件;服务重启后按实际持久配置重新检查。文档中的 force 参数不表示“忽略无效配置”。
重载源文件与临时 PATCH 分开
先保存可回退的旧文件,并用上节 -t 校验新文件。下面的 path 属于 Mihomo 进程所在机器,不是 API 客户端的路径;需满足工作目录 / SAFE_PATHS 约束。
curl - fsS - i - X PUT " http://127.0.0.1:9999/configs?force=true" \
- H " Authorization: Bearer $ { MIHOMO_SECRET :? Load the controller secret in this shell } " \
- H ' Content-Type: application/json' \
-- data ' {"path":"/opt/mihomo/mihomo_config/config.yaml"}'
核对 HTTP 状态,再重复 GET configs / rules 与新建连接验证。失败恢复旧文件后按同一流程校验、重载;不靠 force 忽略错误。
curl - fsS - i - X PATCH http://127.0.0.1:9999/configs \
- H " Authorization: Bearer $ { MIHOMO_SECRET :? Load the controller secret in this shell } " \
- H ' Content-Type: application/json' \
-- data ' {"mode":"rule"}'
这是独立选择,不是重载后的必做步骤。先记录旧 mode,GET 核对新值;需要长期保存应更新受管理源配置,回退按记录值 PATCH。
HTTP 204 表示请求完成;再 GET configs、查看规则和新连接。重载可能重建监听/隧道,不能承诺已有连接全部无感。
先匹配到的规则决定去向
正在呈现知识画面 重新加载
例外规则放在通配规则前还是后 规则自上而下首条匹配。api.example.com 同时满足精确域名和域名后缀,把 DIRECT 后缀放前面会遮住 REJECT 例外。
{"id":"ops-mihomo-rules","title":"例外规则放在通配规则前还是后","summary":"规则自上而下首条匹配。api.example.com 同时满足精确域名和域名后缀,把 DIRECT 后缀放前面会遮住 REJECT 例外。","height":600,"html":"<main><p class=\"quiet\" data-i18n=\"scope\"></p><div class=\"presets\" role=\"group\"><button type=\"button\" data-host=\"api.example.com\" aria-pressed=\"false\" data-i18n=\"api\"></button><button type=\"button\" data-host=\"www.example.com\" aria-pressed=\"false\" data-i18n=\"www\"></button><button type=\"button\" data-host=\"other.test\" aria-pressed=\"false\" data-i18n=\"other\"></button></div><div class=\"checks\"><label><input id=\"first\" type=\"checkbox\" checked><span data-i18n=\"first\"></span></label></div><div class=\"flow\"><div class=\"row code\" id=\"rule0\"></div><div class=\"row code\" id=\"rule1\"></div><div class=\"row code\" id=\"rule2\"></div></div><section class=\"card \"><h3 data-i18n=\"result\"></h3><strong id=\"result\"></strong></section><div class=\"explain\"><div class=\"stable \"><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"hint\"></p><p id=\"message\" aria-live=\"polite\"></p></div></div></main>\n","css":"main {\n display: grid;\n gap: 18px;\n min-width: 0;\n}\np {\n overflow-wrap: anywhere;\n}\n.controls {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n}\n.presets {\n display: flex;\n flex-wrap: wrap;\n}\n.presets button {\n flex: 1 1 85px;\n font-size: 15px;\n}\n.cards {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 10px;\n}\n.card {\n border: 1px solid var(--rule);\n border-radius: 10px;\n padding: 14px;\n min-width: 0;\n background: var(--surface);\n}\n.card h3 {\n font-size: 15px;\n color: var(--muted);\n margin-bottom: 8px;\n}\n.card strong {\n font-size: 23px;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.code {\n font-family: var(--viz-font-mono);\n font-size: 15px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.stable {\n display: grid;\n min-width: 0;\n}\n.stable > * {\n grid-area: 1/1;\n margin: 0;\n}\n.stable > .reserve {\n visibility: hidden;\n pointer-events: none;\n}\n.explain {\n border-left: 3px solid var(--accent);\n padding: 12px 14px;\n background: var(--accent-soft);\n}\n.arrow {\n text-align: center;\n color: var(--accent);\n font-size: 25px;\n}\n.row {\n padding: 12px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n min-width: 0;\n}\n.row.on {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n.row.off {\n color: var(--muted);\n}\n.bar {\n height: 18px;\n background: var(--surface);\n border: 1px solid var(--rule);\n border-radius: 5px;\n overflow: hidden;\n margin-top: 8px;\n}\n.bar > i {\n display: block;\n height: 100%;\n background: var(--accent);\n width: 0;\n transition: width 0.22s;\n}\n.checks {\n display: grid;\n gap: 8px;\n}\n.checks label {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 9px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n font-size: 16px;\n}\n.checks input {\n width: 18px;\n height: 18px;\n accent-color: var(--accent);\n flex-shrink: 0;\n}\nlabel.range {\n display: block;\n font-size: 16px;\n}\nlabel.range output {\n float: right;\n font-variant-numeric: tabular-nums;\n}\n.pair {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n}\n.flow {\n display: grid;\n gap: 6px;\n}\n.quiet {\n font-size: 14px;\n color: var(--muted);\n}\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n}\nbutton {\n overflow-wrap: anywhere;\n}\n.large {\n font-size: 32px;\n color: var(--accent);\n font-weight: 500;\n}\n.layers {\n display: grid;\n gap: 12px;\n}\n.layer {\n border-left: 3px solid var(--rule);\n padding-left: 12px;\n}\n.layer.on {\n border-color: var(--accent);\n}\n@media (max-width: 520px) {\n .cards,\n .pair {\n grid-template-columns: 1fr;\n }\n .card {\n padding: 12px;\n }\n .card strong {\n font-size: 22px;\n }\n}\n.row.code {\n min-height: 5.3em;\n}\n.presets button {\n flex-basis: 140px;\n}\n","js":"const $ = (s) => document.querySelector(s);\nconst $$ = (s) => [...document.querySelectorAll(s)];\nconst t = (k) => viz.t(k);\nconst put = (id, v) => ($(\"#\" + id).textContent = v);\nconst say = (k) => put(\"message\", t(k));\nconst state = (v) => ($(\"main\").dataset.state = JSON.stringify(v));\nconst press = (key, v) =>\n $$(\"[data-\" + key + \"]\").forEach((b) =>\n b.setAttribute(\"aria-pressed\", String(b.dataset[key] === v)),\n );\nlet host = \"api.example.com\";\nfunction draw() {\n const first = $(\"#first\").checked;\n let rules = [\n { kind: \"DOMAIN\", value: \"api.example.com\", action: \"REJECT\" },\n { kind: \"DOMAIN-SUFFIX\", value: \"example.com\", action: \"DIRECT\" },\n ];\n if (!first) rules.reverse();\n rules.push({ kind: \"MATCH\", value: \"\", action: \"PROXY\" });\n let match = rules.findIndex(\n (r) =>\n r.kind === \"MATCH\" ||\n (r.kind === \"DOMAIN\" ? host === r.value : host === r.value || host.endsWith(\".\" + r.value)),\n );\n rules.forEach((r, i) => {\n put(\"rule\" + i, i + 1 + \". \" + [r.kind, r.value, r.action].filter(Boolean).join(\", \"));\n $(\"#rule\" + i).classList.toggle(\"on\", i === match);\n });\n put(\"result\", rules[match].action);\n press(\"host\", host);\n state({ host, first, match, action: rules[match].action });\n}\n$$(\"[data-host]\").forEach(\n (b) =>\n (b.onclick = () => {\n host = b.dataset.host;\n draw();\n }),\n);\n$(\"#first\").onchange = draw;\nsay(\"hint\");\ndraw();\n","audio":false,"strings":{"api":"api.example.com","first":"精确例外放前面","hint":"高亮行终止匹配,下面即使还有更精确的规则也不会再执行。改顺序后用新连接验证。","other":"other.test","result":"最终动作","scope":"已知域名的三条规则,不模拟 DNS、IP 规则或真实代理。选择请求与顺序,看哪一行首先命中。","www":"www.example.com"}}
用运行规则与连接互相核对
curl - fsS - H " Authorization: Bearer $ { MIHOMO_SECRET :? Load the controller secret in this shell } " http://127.0.0.1:9999/rules | jq ' .rules'
比对规则类型、payload、策略目标,首条匹配优先。文件更新但这里未更新时先查重载。
curl - fsS - H " Authorization: Bearer $ { MIHOMO_SECRET :? Load the controller secret in this shell } " http://127.0.0.1:9999/connections |
jq ' .connections[] | {id,source: .metadata.sourceIP,host: .metadata.host,destination: .metadata.destinationIP,port: .metadata.destinationPort,rule,rulePayload,chains}'
按源 IP、目标、端口与时间关联;id 用于后续精确操作。短连接可能已消失,可保持请求或结合日志重现。
curl - fsS - H " Authorization: Bearer $ { MIHOMO_SECRET :? Load the controller secret in this shell } " http://127.0.0.1:9999/proxies | jq ' .proxies'
组名 PROXY、节点名 NODE 必须从实际返回取得;名称含空格/非 ASCII 时对 URL 路径做编码,JSON 值也需正确转义。
选择节点只针对可手动选择的组
curl - fsS - i - X PUT http://127.0.0.1:9999/proxies/PROXY \
- H " Authorization: Bearer $ { MIHOMO_SECRET :? Load the controller secret in this shell } " \
- H ' Content-Type: application/json' \
-- data ' {"name":"NODE"}'
先记录旧 now;成功后 GET proxies 核对,再建立新连接看 chains。自动选择组不应按 Selector 操作;持久化取决于 profile.store-selected 等实际配置。
curl - fsS - i - X DELETE - H " Authorization: Bearer $ { MIHOMO_SECRET :? Load the controller secret in this shell } " http://127.0.0.1:9999/connections/CONNECTION_ID
CONNECTION_ID 从 connections 取得;该请求会中断这条业务连接。再次查询确认消失,再由应用重连;不省略 ID 误断全部连接。
区分“规则选到策略组”和“组最终选到节点”;检查 chains 与 now,而非只看配置文本。域名规则需要可用域名信息,IP 流量可能靠 DNS 映射或嗅探关联;redir-host 不等于一定只能靠嗅探。fake-ip 是映射标识,不是目标服务器真实 IP,直接拿它测公网路由会误判。
只验证新建连接,因为已有连接可能保留旧选择。确需断开时先读取特定 id,再 DELETE /connections/ID;DELETE /connections 会断开全部连接,不当作日常“刷新按钮”。
显式代理与透明入口分开验证
先验证显式入口
curl - v -- connect-timeout 5 -- proxy http://127.0.0.1:7890 https://example.com/
7890 按 GET configs 实际端口替换;核对连接记录的规则与 chains。远程客户端 localhost 指自己,不能指向另一台设备的代理。
ip rule show
在实际透明网关执行,结合来源和 mark 判断规则;表内容另用 ip route show table all。
ss - lntup
与配置端口逐项对应;监听在回环只供本机访问。
sudo nft - a list ruleset
关注实际入口接口、端口、mark 与路由表联动;插件生成规则要回到插件配置修改,不能把整个 ruleset 清空。
现象 下一步
显式代理成功,LAN 客户端失败 查客户端网关/DNS、透明截获与路由,而非先换所有节点
请求不在 connections 中 确认端口、进程和入口;瞬时连接可能已结束,结合日志抓复现
规则正确但目标失败 查节点可达性、协议、DNS、证书和目标服务,不把 delay 测试当全站可用
Nikki/TUN/TProxy 的规则与端口按实际版本配置查看,不固定写死为某个 7891。旁路由自定义 fail-open 只有部署了对应机制才存在,而且直连可能违背路由策略;不要把停进程后“应该自动恢复直连”当默认事实。恢复后同时验证 DNS、TCP、UDP 和局域网例外。
Mihomo API · Routing rules · DNS · TUN