実接続で流入・domain/IP・一致規則・最終 proxy を追う。設定ファイル、稼働設定、既存接続は異なる時点の状態になり得る。
稼働ファイルと controller を特定
設定を実行中のインスタンスと照合する
systemctl cat mihomo.service
バイナリと -d/-f を確認する。コンテナ・OpenWrt plugin はその管理元を使い、別設定を systemd から操作しない。
systemctl status mihomo.service -- no-pager
詳細は journalctl を読む。ファイルの存在だけではこのプロセスが使っている証明にならない。
mihomo - v
ExecStart と同じバイナリを使う。PATH 上の同名コマンドは別版の場合がある。
mihomo - t - f /opt/mihomo/mihomo_config/config.yaml
実環境のパス・作業場所、特に相対 provider パスに合わせる。エラーを直してから、API・購読・実通信も確認する。
このパスと127.0.0.1:9999 は配置例で既定値ではない。ExecStart の -d/-f と controller・secret を確認する。構文検査は provider・DNS・upstream の可用性を保証しない。
クライアント側で認証を用意してから読む
controller に届くクライアントで実行する。例は localhost:9999。管理された秘密値読込か対話入力で 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 か先に確認する。
再読込前に元ファイルを保存し、workdir/SAFE_PATHS 内か確認する。PATCH は元ファイルへの永続保存ではない。force は無効設定を無視する意味ではない。
設定読込と一時 PATCH を分ける
旧ファイルを保存して新版を検証する。path は API クライアントではなく Mihomo ホスト上の場所で、作業場所・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 する。
204 後も稼働設定・規則・新接続を確認する。listener/tunnel の再構成で既存接続に影響し得る。
最初に一致した規則が行先を決める
図を準備しています 再読み込み
例外は広い規則の前か後か 規則は上から最初の一致。api.example.com は完全名と suffix の両方に一致し、DIRECT suffix を先に置くと REJECT 例外が隠れる。
{"id":"ops-mihomo-rules","title":"例外は広い規則の前か後か","summary":"規則は上から最初の一致。api.example.com は完全名と suffix の両方に一致し、DIRECT suffix を先に置くと 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":"domain 情報がある三規則。DNS・IP 規則・実 proxy は省略。request と順序で先頭一致を見る。","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'
type・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 encode、JSON 値は適切に escape する。
手動選択可能な group だけを変更する
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 と新接続の chains で確認する。自動 group を 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
ID は connections から取得する。その通信を中断するため、消失確認後にアプリから再接続する。ID を省くと全接続が対象になる。
規則の group 選択と group の node 選択を分け chains・now を見る。domain 情報は DNS mapping や sniffing 由来で、redir-host が sniffing 限定とは限らない。fake IP は実宛先の public IP ではない。
既存接続は以前の選択を保持し得るので新接続で試す。必要な切断は特定 ID を選ぶ。全 /connections の DELETE を日常更新操作にしない。
明示 proxy と透過入口を分けて試す
明示 proxy を先に検証する
curl - v -- connect-timeout 5 -- proxy http://127.0.0.1:7890 https://example.com/
7890 は実行設定に合わせ、rule/chains と照合する。遠隔クライアントの localhost は proxy ホストではない。
ip rule show
実際の gateway で元・mark を調べ、テーブル内容は別途確認する。
ss - lntup
設定ポートと照合する。ループバック待ち受けは直接には自機用。
sudo nft - a list ruleset
入口・ポート・mark・経路表の対応を確認する。plugin の規則は管理元から変更し、全体を flush しない。
観測 次の確認
明示 proxy は成功、LAN は失敗 client gateway・DNS・透過捕捉・route を確認
connections に現れない listener・process・入口とログ。短い接続は終了済みの可能性
規則は正しいが宛先失敗 node・protocol・DNS・TLS・service。delay 試験は全宛先の保証でない
Nikki/TUN/TProxy は実設定を確認し port を固定視しない。fail-open は実装時のみ存在し、直結が方針に反する場合もある。復旧後に DNS・TCP・UDP・LAN 例外を確認する。
Mihomo API · Routing rules · DNS · TUN