ClashのAPIでノードを自動切替|死活監視と障害対策の実践設定

Clashのexternal-controllerを使い、通信不能なプロキシを検知して利用可能なノードへ自動的に切り替える方法を解説します。プロキシグループの操作、遅延測定、YAML設定、スクリプト実行、API認証、ログ確認まで実用的な構成で紹介します。

APIの役割と自動切替の全体像

Clashのノード障害に対処する方法は、大きく2つあります。ひとつはカーネルに組み込まれた url-testfallback を使い、複数ノードの状態に応じて自動選択させる方法です。もうひとつは、External Controller APIから現在のプロキシ状態を取得し、外部スクリプトで遅延測定、失敗回数の集計、プロキシグループの切り替えを行う方法です。

単純な遅延の比較だけなら url-test が扱いやすく、設定ファイルだけで完結します。一方で、「同じノードが3回連続で失敗したら切り替える」「特定の時間帯だけ予備ノードを優先する」「切り替えた理由をログに残す」といった条件は、APIを使った外部スクリプトのほうが柔軟です。FlClash、Clash Verge Rev、Mihomoを利用する場合でも、実際にAPIを提供しているのは画面ではなく、バックグラウンドで動作するClashまたはmihomoカーネルです。

自動切替の構成は、次の4層に分けて考えると分かりやすくなります。

External Controllerを安全に有効化する

APIを使うには、設定ファイルで external-controller を有効にします。ローカルのスクリプトだけからアクセスするなら、待ち受け先は 127.0.0.1 に固定してください。0.0.0.0 にするとLAN上の他の端末からも接続できるため、ファイアウォールと認証を適切に設定しない限り危険です。

mixed-port: 7890
allow-lan: false
mode: rule
log-level: info

external-controller: 127.0.0.1:9090
secret: change-this-to-a-long-random-secret

proxy-groups:
  - name: 自動選択
    type: url-test
    proxies:
      - ノードA
      - ノードB
      - ノードC
    url: https://www.gstatic.com/generate_204
    interval: 300
    tolerance: 50

external-controller9090 はWebアクセス用のプロキシポートではありません。ブラウザーやシステムプロキシに設定するのは mixed-port7890 であり、9090はクライアントやスクリプトがAPIリクエストを送る管理用ポートです。実際のポート番号は、使用中の設定ファイルまたはクライアントのカーネル設定画面で確認してください。

認証には secret を設定します。APIリクエストでは、通常次のようなHTTPヘッダーを使います。

Authorization: Bearer change-this-to-a-long-random-secret

secretはサブスクリプションURLと同じく、第三者に渡してはいけない情報です。公開リポジトリ、共有ログ、画面キャプチャ、チャットの貼り付けに含めないでください。FlClashなどの画面から設定を変更したあと、カーネルの再起動が必要になる場合があります。APIが応答しないときは、まずポートが待ち受け状態か、現在起動しているカーネルが編集した設定を読み込んでいるかを確認します。

APIの基本エンドポイントを確認する

最初は状態を変更せず、GETリクエストだけで接続を確認します。次のコマンドは、プロキシ一覧と現在の選択状態を取得する例です。secretは実際の値に置き換えてください。

curl -sS \
  -H "Authorization: Bearer change-this-to-a-long-random-secret" \
  http://127.0.0.1:9090/proxies

レスポンスには、個別ノードだけでなく、プロキシグループ、DIRECT、REJECTなども含まれます。グループ名を指定すると、現在選択されているプロキシを確認できます。

curl -sS \
  -H "Authorization: Bearer change-this-to-a-long-random-secret" \
  "http://127.0.0.1:9090/proxies/自動選択"

日本語のグループ名や記号を含むノード名はURLエンコードが必要です。スクリプトでリクエストを作る場合は、文字列を手動でURLへ連結せず、標準ライブラリのURLエンコード機能を使ってください。

死活監視とノード切替をスクリプト化する

APIには、指定したプロキシの遅延を測定するためのエンドポイントがあります。代表的な形式は /proxies/<プロキシ名>/delay で、クエリに測定URLとタイムアウトを指定します。カーネルの系列やバージョンによって利用できるパラメーターが異なることがあるため、実際には使用中のmihomoまたはClashのAPI仕様と応答形式を確認してください。

次のPython例は、ローカルのExternal Controllerへ接続し、「自動選択」グループに登録した候補を順番に測定します。タイムアウトは5000ミリ秒、許容する最大遅延は1500ミリ秒、連続失敗数が2回以上のノードは切り替え候補から外します。状態はJSONファイルに保存し、1回の失敗だけで頻繁に切り替わることを防ぎます。

import json
import logging
import os
import time
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen

API = "http://127.0.0.1:9090"
SECRET = os.environ["CLASH_SECRET"]
GROUP = "自動選択"
NODES = ["ノードA", "ノードB", "ノードC"]
TEST_URL = "https://www.gstatic.com/generate_204"
TIMEOUT_MS = 5000
MAX_DELAY_MS = 1500
STATE_FILE = "clash-node-state.json"

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s"
)

def api_request(path, method="GET", body=None):
    data = None
    headers = {"Authorization": f"Bearer {SECRET}"}
    if body is not None:
        data = json.dumps(body, ensure_ascii=False).encode("utf-8")
        headers["Content-Type"] = "application/json"

    request = Request(
        API + path,
        data=data,
        headers=headers,
        method=method
    )
    with urlopen(request, timeout=10) as response:
        raw = response.read().decode("utf-8")
        return json.loads(raw) if raw else {}

def measure(node):
    path = "/proxies/" + quote(node, safe="") + "/delay?" + urlencode({
        "timeout": TIMEOUT_MS,
        "url": TEST_URL
    })
    try:
        result = api_request(path)
        delay = int(result.get("delay", -1))
        if delay <= 0 or delay > MAX_DELAY_MS:
            return None
        return delay
    except (HTTPError, URLError, ValueError, TimeoutError) as error:
        logging.warning("測定失敗 node=%s error=%s", node, error)
        return None

def load_state():
    try:
        with open(STATE_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except (FileNotFoundError, json.JSONDecodeError):
        return {}

def save_state(state):
    temporary = STATE_FILE + ".tmp"
    with open(temporary, "w", encoding="utf-8") as file:
        json.dump(state, file, ensure_ascii=False, indent=2)
    os.replace(temporary, STATE_FILE)

def main():
    state = load_state()
    candidates = []

    for node in NODES:
        delay = measure(node)
        item = state.setdefault(node, {"failures": 0})
        if delay is None:
            item["failures"] += 1
            logging.warning(
                "node=%s failures=%s", node, item["failures"]
            )
            continue

        item["failures"] = 0
        item["delay"] = delay
        candidates.append((delay, node))
        logging.info("node=%s delay=%sms", node, delay)

    save_state(state)

    if not candidates:
        logging.error("利用可能なノードがありません")
        return

    candidates.sort()
    selected = candidates[0][1]
    current = api_request(
        "/proxies/" + quote(GROUP, safe="")
    ).get("now")

    if current != selected:
        api_request(
            "/proxies/" + quote(GROUP, safe=""),
            method="PUT",
            body={"name": selected}
        )
        logging.info("切替 group=%s from=%s to=%s", GROUP, current, selected)
    else:
        logging.info("変更なし group=%s node=%s", GROUP, current)

if __name__ == "__main__":
    main()

この例では、候補の中から測定遅延が最も小さいノードを選びます。実運用では、失敗回数が2回未満のノードを一時的に候補へ戻さない条件や、現在のノードに一定の優先時間を与える条件を追加すると、測定値の小さな揺れによる切り替えを抑えられます。また、測定用URLが特定の地域やネットワークで不安定だと、正常なノードまで障害と判定されます。実際の利用先に近いHTTPS URLを選び、1つのURLだけを絶対視しないことが重要です。

url-testとの使い分けと定期実行

設定ファイルの url-test は、カーネルが定期的にノードの遅延を測定し、グループ内の候補を自動選択する機能です。標準機能なので、外部プロセスが停止しても自動選択そのものは継続します。数分間隔での通常運用には、まずこちらを使うほうが安定します。

方法 適しているケース 注意点
url-test 遅延の小さいノードを定期的に自動選択したい 失敗回数、時間帯、通知などの細かい条件は限定的
fallback 現在のノードが利用できないとき予備へ移したい 測定URLと順序の設計が必要
APIスクリプト 連続失敗、ログ、通知、独自の優先順位を使いたい スクリプト、secret、実行環境を安全に管理する必要がある

定期実行の間隔は短すぎないようにします。30秒ごとに多数のノードを測定すると、測定先へのアクセスが増え、サブスクリプション事業者や接続先から過剰なアクセスと判断される可能性があります。一般的な家庭利用では、2~5分間隔から始め、ノード数、ネットワーク品質、切り替え頻度を見ながら調整してください。カーネル側の interval: 300 と外部スクリプトの実行間隔を同じにすると、測定が集中するため、数十秒ずらす設計も有効です。

WindowsではタスクスケジューラでPythonスクリプトを5分間隔で実行し、環境変数 CLASH_SECRET をユーザー環境に設定します。macOSやLinuxでは、secretをコマンドライン引数に直接書かず、権限を制限した環境ファイルから読み込む方法が安全です。cronの例は次のようになります。

*/5 * * * * CLASH_SECRET='secretをここに設定' /usr/bin/python3 /home/user/bin/check_clash.py >> /home/user/logs/clash-switch.log 2>&1

ログには時刻、測定対象、遅延、切り替え前後のノード名、APIエラーだけを残し、secret、サブスクリプションURL、ノードの接続情報は出力しないでください。ログを調べるときは、すべてのノードが同時に失敗しているか、特定のノードだけが失敗しているかを見ます。前者なら測定URL、DNS、ローカルカーネル、外部コントローラーの問題が疑われ、後者ならノード側の障害や経路品質を優先して確認できます。

APIによる自動切替は、障害を完全になくす仕組みではありません。測定URLの応答、DNS解決、ローカルポート、カーネルのログ、ノード側の状態がすべて影響します。設定ファイルには基本的な自動選択を残し、外部スクリプトでは連続失敗の判定や通知など、設定だけでは扱いにくい部分を補う構成が現実的です。最終的に重要なのは、切り替わったことだけでなく、どのノードがいつ、何回失敗し、どの条件で復旧したかを後から確認できることです。

FlClash ダウンロード 各プラットフォームのクライアントを見る