ClashやMihomoで複数のプロキシノードを運用していると、単にノードを登録するだけでは安定した通信を維持できません。あるノードの遅延が急上昇したり、TLS接続が断続的に失敗したり、サーバー自体は応答していても特定の宛先だけ到達できなくなったりするためです。external-controller APIを利用すると、現在のノード状態、HTTP遅延、プロキシグループの選択状況をプログラムから取得できます。さらに、一定間隔でヘルスチェックを実行し、条件を満たすノードへ自動的に切り替える仕組みも構築できます。この記事では、Clash APIでノード自動切替を実装するための設定、判定ロジック、実装例、運用上の注意点を順番に解説します。

external-controller APIの準備

ClashのREST APIは、設定ファイルのexternal-controllerで有効化します。Mihomo、Clash Verge Rev、Clash for Windowsなど、採用しているコアによって対応エンドポイントの細部が異なる場合がありますが、プロキシグループの取得と切り替えには共通するAPIを利用できます。まずはAPIをローカルホストだけに公開し、強力なシークレットを設定してください。

external-controller の基本設定
# config.yaml または Mihomo の設定ファイル
external-controller: 127.0.0.1:9090
secret: replace-with-a-long-random-secret

# 必要に応じてダッシュボードも設定
external-ui: ui

127.0.0.1:9090を指定した場合、APIへ接続できるのは同じ端末上のプログラムだけです。別のサーバーから監視したい場合は0.0.0.0:9090で待ち受ける方法もありますが、その場合はファイアウォール、VPN、リバースプロキシなどを組み合わせてアクセス元を制限してください。APIポートをインターネットへ直接公開するのは避けるべきです。

認証が有効な場合、HTTPリクエストにはAuthorization: Bearerヘッダーを付けます。まずAPIが応答するか、次のように確認します。

API接続の確認
curl -H "Authorization: Bearer replace-with-a-long-random-secret" \
  http://127.0.0.1:9090/version

正常な場合は、コアのバージョン情報を含むJSONが返ります。応答がない場合は、Clashが起動しているか、ポート番号が正しいか、設定ファイルが実際に読み込まれているかを確認してください。Clash VergeなどのGUIクライアントでは、画面上の設定と実際に起動中のMihomoプロセスが使用する設定が異なることもあります。

APIシークレットをコードに直書きしない:シェルスクリプトやPythonファイルを共有する場合、トークンがログやGit履歴に残る可能性があります。環境変数、権限を制限した設定ファイル、またはOSのシークレット管理機能を利用してください。

ノード状態と遅延を取得するAPI

自動切替の中心となるのは、プロキシグループの情報取得とノードの遅延測定です。代表的なエンドポイントは次のとおりです。

エンドポイント用途主な確認項目
GET /versionコアの稼働確認バージョン、API接続可否
GET /proxiesプロキシとグループの一覧取得グループ名、現在の選択ノード、候補ノード
GET /proxies/{name}/delay個別ノードの遅延測定HTTPステータス、応答時間
PUT /proxies/{group}プロキシグループを切り替え選択するノード名
GET /connections現在の接続を確認宛先、ルール、使用グループ

ノード名やグループ名に日本語、空白、記号が含まれる場合、URLへ埋め込む前に必ずURLエンコードします。Pythonのrequestsを使う場合は、パス部分を文字列連結するよりurllib.parse.quoteで変換するほうが安全です。なお、APIの応答形式や利用可能なフィールドはClash系コアのバージョンによって異なるため、最初に/proxiesのJSON全体を保存して確認すると実装ミスを減らせます。

遅延測定には、一般的に204レスポンスを返す軽量なHTTPS URLを利用します。たとえばhttps://www.gstatic.com/generate_204や、自分で管理する小さなヘルスチェック用URLが候補になります。判定先は一つに固定しすぎないことも重要です。特定のCDNや地域だけが一時的に遅い場合、実際の利用状況とは異なる結果になる可能性があります。

ノード遅延を問い合わせる例
curl -G \
  -H "Authorization: Bearer replace-with-a-long-random-secret" \
  --data-urlencode "url=https://www.gstatic.com/generate_204" \
  --data-urlencode "timeout=5000" \
  "http://127.0.0.1:9090/proxies/node-jp-01/delay"

測定値は「小さいほど良い」と単純に判断できますが、遅延だけではノードの品質を完全には評価できません。遅延が低くても、パケットロス、帯域幅不足、特定サイトへの接続失敗が発生する場合があります。そのため、実運用ではタイムアウト回数、連続失敗数、最低遅延、切替後の安定時間を組み合わせて判断します。

自動切替スクリプトの実装

最初から複雑な監視システムを作る必要はありません。基本形は「対象グループを取得する」「候補ノードを順番に測定する」「条件を満たすノードを選ぶ」「現在のノードと異なる場合だけ切り替える」という4段階です。次のPython例は、指定したグループの候補ノードを測定し、失敗していないノードの中から最も低遅延のものを選択します。

基本的なノード自動切替スクリプト
import os
import time
from urllib.parse import quote

import requests

API = os.getenv("CLASH_API", "http://127.0.0.1:9090")
SECRET = os.environ["CLASH_SECRET"]
GROUP = os.getenv("CLASH_GROUP", "Auto-Select")
TEST_URL = os.getenv(
    "CLASH_TEST_URL",
    "https://www.gstatic.com/generate_204"
)
TIMEOUT_MS = 5000
INTERVAL_SEC = 60
MIN_SWITCH_GAIN_MS = 40

session = requests.Session()
session.headers.update({
    "Authorization": f"Bearer {SECRET}"
})


def get_group():
    response = session.get(
        f"{API}/proxies/{quote(GROUP, safe='')}",
        timeout=10
    )
    response.raise_for_status()
    return response.json()


def measure(node):
    response = session.get(
        f"{API}/proxies/{quote(node, safe='')}/delay",
        params={"url": TEST_URL, "timeout": TIMEOUT_MS},
        timeout=10
    )
    response.raise_for_status()
    return int(response.json()["delay"])


def switch_to(node):
    response = session.put(
        f"{API}/proxies/{quote(GROUP, safe='')}",
        json={"name": node},
        timeout=10
    )
    response.raise_for_status()


while True:
    try:
        group = get_group()
        current = group.get("now")
        candidates = group.get("all", [])
        results = []

        for node in candidates:
            try:
                delay = measure(node)
                results.append((delay, node))
                print(f"{node}: {delay} ms")
            except requests.RequestException as error:
                print(f"{node}: failed ({error})")

        if results:
            best_delay, best_node = min(results)
            current_delay = None

            for delay, node in results:
                if node == current:
                    current_delay = delay
                    break

            should_switch = (
                current != best_node
                and (
                    current_delay is None
                    or best_delay + MIN_SWITCH_GAIN_MS < current_delay
                )
            )

            if should_switch:
                switch_to(best_node)
                print(f"switched: {current} -> {best_node}")
            else:
                print(f"keep: {current}")

    except (requests.RequestException, KeyError, ValueError) as error:
        print(f"monitor error: {error}")

    time.sleep(INTERVAL_SEC)

この例では、最速ノードとの差がMIN_SWITCH_GAIN_MS未満であれば切り替えません。実際のネットワークでは測定値が数十ミリ秒単位で揺れるため、常に最小値へ追従すると切り替えが頻発します。このようなヒステリシスを設けることで、通信中のノードが安定している限り不要な切替を抑えられます。

また、1回の失敗だけで即座にノードを交換する設計にも注意が必要です。瞬間的な輻輳やテストURL側の障害をノード障害と誤認する可能性があるからです。本番運用では、各ノードの連続失敗数をメモリやSQLiteなどに保存し、たとえば3回連続で失敗した場合だけ切替を実行する方式が安全です。逆に、現在のノードが明らかにタイムアウトしている場合は、待機時間を短くして復旧を優先できます。

切替は「最速」より「十分に良好」を目標にする:現在のノードが安定していて、最速ノードとの差が小さいなら、そのまま使うほうが実用的です。切替回数、連続失敗数、切替前後の遅延をログに残すと、しきい値の調整が容易になります。

グループの切替リクエストは、通常次のようなJSONボディを使用します。グループがurl-testfallbackの場合、コア自身が自動選択を管理していることがあるため、外部スクリプトから手動選択するグループと役割を分けてください。

プロキシグループの切替リクエスト
curl -X PUT \
  -H "Authorization: Bearer replace-with-a-long-random-secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"node-jp-01"}' \
  "http://127.0.0.1:9090/proxies/Auto-Select"

安定運用とトラブルシューティング

自動切替が動作した後に最も重要なのは、スクリプトを常駐させることと、異常を追跡できるログを残すことです。Linuxではsystemdサービス、Windowsではタスクスケジューラ、macOSではlaunchdを利用できます。単純にターミナルで実行するだけでは、ログアウトや再起動後に監視が停止するため、本番用途には向きません。

systemdで運用する場合は、APIシークレットをサービスファイルへ直接書かず、読み取り権限を制限したEnvironmentFileに分離します。サービスには自動再起動、ネットワーク起動後の待機、実行ユーザーの制限を設定してください。監視プログラム自体が異常終了した場合でも、systemdが再起動して復旧できます。

systemd サービス例
[Unit]
Description=Clash node auto switch monitor
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=clash-monitor
WorkingDirectory=/opt/clash-monitor
EnvironmentFile=/etc/clash-monitor.env
ExecStart=/usr/bin/python3 /opt/clash-monitor/monitor.py
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

切替ログには、少なくとも時刻、現在のノード、候補ノード、測定URL、遅延、失敗理由、実際に切り替えたかどうかを記録します。ログを見れば、APIが止まったのか、すべてのノードが遅いのか、特定のテスト先だけが不安定なのかを区別できます。ノードのパスワードやUUIDなどの認証情報はログへ出力しないでください。

よくある問題と確認方法は次のとおりです。

  • 401 Unauthorized:Authorizationヘッダーの形式、シークレットの値、接続先ポートを確認します。余分な空白や改行が環境変数に入っている場合もあります。
  • 404 Not Found:ClashとMihomoでAPIパスが異なる、またはプロキシ名をURLエンコードしていない可能性があります。まず/proxiesで実際の名前を取得します。
  • 切替しても通信が変わらない:ルールが別のプロキシグループを参照している、対象グループがルールに接続されていない、既存接続が維持されている可能性があります。
  • 頻繁に切り替わる:測定間隔を延ばし、遅延差のしきい値を上げ、連続失敗回数とクールダウン時間を追加します。
  • すべてのノードが失敗する:ClashのDNS、テストURL、システム時刻、ファイアウォール、ノード側の障害を順番に確認します。

さらに安全性を高めるなら、ノードを用途別に分ける設計が有効です。通常のWeb閲覧用グループと、業務システムや固定IPが必要な通信のグループを同じ自動切替対象にすると、出口IPの変化によって認証が無効になったり、セッションが切断されたりすることがあります。自動化する範囲を明確にし、固定接続が必要なサービスには手動選択または専用ノードを割り当ててください。

Clashの標準機能であるurl-testfallbackは、一般的な自動選択と障害切替に十分な機能を備えています。一方、APIスクリプトは、複数の測定先、時間帯別の重み付け、連続失敗数、外部通知、業務時間帯だけの切替など、標準グループだけでは表現しにくい条件を追加する場合に適しています。まずは標準のプロキシグループで挙動を確認し、それで不足する要件だけをAPI側へ移すのが保守しやすい方法です。

ノード自動切替は、単に最も小さい遅延値を選ぶ機能ではありません。APIの認証と公開範囲を適切に管理し、測定の誤差を考慮したしきい値を設定し、失敗時のログと復旧動作を設計することで、初めて安定した運用になります。小規模な環境では短いスクリプトから始め、記録した実測値をもとに測定間隔、連続失敗数、クールダウン時間を調整してください。MihomoやClash対応クライアントをすぐに試したい場合は、環境に合った設定を用意してから段階的に自動化を導入すると、既存の通信への影響を抑えながら運用を改善できます。