Clash APIでノード自動切替を実装する実践ガイド

手動でプロキシを選び直す運用から脱却し、Clash APIによるヘルスチェックと自動フェイルオーバーを構築します。実行可能なスクリプトを使って、遅延監視、再試行、ログ管理まで段階的に設定できます。

手動でプロキシを選び直す運用から脱却し、Clash APIによるヘルスチェックと自動フェイルオーバーを構築します。実行可能なスクリプトを使って、遅延監視、再試行、ログ管理まで段階的に設定できます。

Clash APIで実現できる自動切替の全体像

Clash系クライアントでノードを自動的に切り替える方法は、大きく2種類あります。1つ目は設定ファイルの url-testfallback を使い、mihomoコア自身に選択を任せる方法です。2つ目は外部コントローラーAPIをスクリプトから呼び出し、遅延、接続成功、連続失敗回数、切替履歴などの条件を細かく制御する方法です。

単純に最も速いノードを選びたいだけなら、設定ファイルの自動グループで十分です。しかし、特定のノードを優先したい、一定回数失敗した場合だけ切り替えたい、夜間だけ監視したい、切替時にログを残したいといった運用では、APIを使うほうが柔軟です。本記事では、mihomoを搭載したClash Verge Rev、Clash Plus、FlClashなどを想定し、ローカルで動作するPythonスクリプトからプロキシグループを操作します。

APIで行う処理の流れは次のとおりです。

  1. Clashの外部コントローラーへ接続する。
  2. 指定したプロキシグループと候補ノードを取得する。
  3. 各候補ノードに対してテストURLへの遅延測定を実行する。
  4. タイムアウトやHTTPエラーを除外し、条件を満たすノードを選ぶ。
  5. 現在の選択ノードと比較し、必要な場合だけグループを切り替える。
  6. 結果をログへ保存し、次回実行時の判定材料にする。

自動切替と負荷分散は別の機能

url-test は通常、定期的にテストURLへ接続して応答時間の短いノードを選びます。一方、fallback は登録順の優先度を維持し、現在のノードが利用できない場合に次の候補へ移ります。帯域を複数ノードへ分散する機能ではないため、目的に合うグループタイプを選択してください。

外部コントローラーと認証を安全に設定する

Clash APIの入口は、mihomo設定の external-controller です。多くのGUIでは「外部コントローラー」「External Controller」「APIポート」などの名前で表示されます。ローカルのスクリプトだけから操作するなら、外部公開を避けて 127.0.0.1 にバインドするのが基本です。

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

クライアントの設定画面で外部コントローラーを有効にした場合、実際のポート番号が9090とは限りません。Clash Verge RevなどではGUIの設定値が生成済み設定に反映されるため、スクリプトには現在表示されているアドレスを指定します。認証用の secret を設定している場合、HTTPリクエストには次のヘッダーを付けます。

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

外部コントローラーを 0.0.0.0:9090 で待ち受ける設定は、同じLAN上の別端末からAPIを操作できる状態を作ります。LAN共有が必要でない限り使用しないでください。どうしても別端末から操作する場合は、ファイアウォールで接続元を限定し、十分に長いランダムなシークレットを設定します。APIが漏れると、ノード選択だけでなく設定情報や接続状態も取得される可能性があります。

まずブラウザやターミナルから次のエンドポイントを確認します。JSONが返れば、アドレスとポートは正しく、401が返れば認証ヘッダーまたはシークレットが一致していません。

curl -H "Authorization: Bearer change-this-to-a-long-random-token" \
  http://127.0.0.1:9090/version

APIを有効にしても接続できない場合は、GUIが別のコアを起動していないか、設定反映後にコアを再起動したか、OSのファイアウォールがローカルポートを遮断していないかを確認します。設定ファイルを直接編集する場合は、YAMLのインデントと引用符にも注意してください。

ノード一覧とプロキシグループのAPIを理解する

自動切替で主に使うのは、プロキシ一覧を取得する GET /proxies、個別プロキシの遅延を測定する GET /proxies/{name}/delay、グループの現在値を変更する PUT /proxies/{group} の3つです。ノード名やグループ名には日本語、空白、スラッシュが含まれることがあるため、URLへ埋め込む際は必ずURLエンコードします。

用途HTTPメソッドエンドポイント主な戻り値
コアのバージョン確認GET/versionversion、meta
全プロキシ情報の取得GET/proxiesノードとグループの一覧
ノードの遅延測定GET/proxies/{name}/delaydelay
グループの選択変更PUT/proxies/{group}成功時は通常204

/proxies のレスポンスでは、各項目に typeallnow などが含まれます。SelectorURLTestFallback といったグループには候補ノードの配列が入り、通常のSS、VMess、VLESS、Trojanなどのノードにはグループ用の all がありません。したがって、すべてのプロキシ名を無条件にテストするのではなく、対象グループの all に含まれるものだけを候補にします。

# グループ「自動選択」の現在値を「東京-01」に変更する例
curl -X PUT \
  -H "Authorization: Bearer change-this-to-a-long-random-token" \
  -H "Content-Type: application/json" \
  -d '{"name":"東京-01"}' \
  "http://127.0.0.1:9090/proxies/自動選択"

遅延測定では、対象ノード名をパスに含め、テストURLとタイムアウトをクエリとして渡します。利用するURLは、普段の通信目的に近く、短いレスポンスを返すHTTPSエンドポイントが適しています。特定サービスだけを基準にすると、そのサービス側の混雑や地域制限をノード障害と誤認することがあるため、運用環境に合うURLを選びます。

実行可能なPythonスクリプトで遅延監視と切替を行う

以下はPython標準ライブラリだけで動作するサンプルです。外部パッケージを追加する必要はありません。対象グループ名、候補ノード、APIシークレット、最小許容遅延は環境に合わせて変更してください。スクリプトは各ノードを測定し、最も速いノードが現在値と異なる場合だけ切り替えます。

#!/usr/bin/env python3
import json
import logging
import os
import sys
import time
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError

API = os.getenv("CLASH_API", "http://127.0.0.1:9090")
SECRET = os.getenv("CLASH_SECRET", "")
GROUP = os.getenv("CLASH_GROUP", "自動選択")
TEST_URL = os.getenv("CLASH_TEST_URL", "https://www.gstatic.com/generate_204")
TIMEOUT_MS = int(os.getenv("CLASH_TIMEOUT_MS", "5000"))
MAX_DELAY_MS = int(os.getenv("CLASH_MAX_DELAY_MS", "2000"))
LOG_FILE = os.getenv("CLASH_LOG", "clash-auto-switch.log")

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

def request_json(path, method="GET", body=None, timeout=10):
    headers = {"Accept": "application/json"}
    if SECRET:
        headers["Authorization"] = "Bearer " + SECRET
    data = None
    if body is not None:
        headers["Content-Type"] = "application/json"
        data = json.dumps(body, ensure_ascii=False).encode("utf-8")
    req = Request(API.rstrip("/") + path, data=data,
                  headers=headers, method=method)
    with urlopen(req, timeout=timeout) as response:
        raw = response.read()
        return json.loads(raw.decode("utf-8")) if raw else {}

def measure(name):
    query = urlencode({"url": TEST_URL, "timeout": str(TIMEOUT_MS)})
    path = "/proxies/" + quote(name, safe="") + "/delay?" + query
    result = request_json(path, timeout=TIMEOUT_MS / 1000 + 2)
    delay = int(result["delay"])
    if delay <= 0 or delay > MAX_DELAY_MS:
        raise ValueError("delay over limit")
    return delay

def main():
    try:
        proxies = request_json("/proxies")
        group = proxies["proxies"][GROUP]
        candidates = group.get("all", [])
        current = group.get("now", "")
    except Exception as exc:
        logging.error("APIまたはグループ取得に失敗: %s", exc)
        return 2

    if not candidates:
        logging.error("候補ノードが見つかりません: %s", GROUP)
        return 3

    results = []
    for name in candidates:
        try:
            delay = measure(name)
            results.append((delay, name))
            logging.info("health name=%s delay=%dms", name, delay)
        except (HTTPError, URLError, TimeoutError, ValueError, KeyError) as exc:
            logging.warning("health name=%s failed=%s", name, exc)

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

    results.sort(key=lambda item: item[0])
    best_delay, best_name = results[0]
    if best_name == current:
        logging.info("keep group=%s node=%s delay=%dms",
                     GROUP, current, best_delay)
        return 0

    try:
        group_path = "/proxies/" + quote(GROUP, safe="")
        request_json(group_path, method="PUT", body={"name": best_name})
        logging.info("switch group=%s from=%s to=%s delay=%dms",
                     GROUP, current, best_name, best_delay)
        return 0
    except Exception as exc:
        logging.error("切替に失敗: %s", exc)
        return 5

if __name__ == "__main__":
    sys.exit(main())

LinuxやmacOSではファイルを保存して実行権限を付け、環境変数で認証情報を渡します。シェル履歴にシークレットを残したくない場合は、コマンドラインへ直接書かず、専用の環境ファイルやOSの資格情報管理機能を利用してください。

chmod 700 clash_auto_switch.py
export CLASH_API="http://127.0.0.1:9090"
export CLASH_SECRET="change-this-to-a-long-random-token"
export CLASH_GROUP="自動選択"
python3 clash_auto_switch.py

WindowsではPowerShellで次のように設定できます。日本語のグループ名を使う場合も、PowerShellの文字コード設定や保存ファイルのUTF-8形式を確認してください。

$env:CLASH_API = "http://127.0.0.1:9090"
$env:CLASH_SECRET = "change-this-to-a-long-random-token"
$env:CLASH_GROUP = "自動選択"
python .\clash_auto_switch.py

再試行とヒステリシスで頻繁な切替を防ぐ

遅延測定は一時的なパケットロスや接続先の混雑に影響されます。1回だけ測定して即座に切り替える設計では、数分おきにノードが入れ替わるフラッピングが起きます。実運用では、各候補を複数回測定して中央値を使う、現在のノードに一定の優先マージンを与える、連続失敗を条件にする、といった仕組みが必要です。

  • 再試行: 1回のタイムアウトを即障害と判断せず、同じノードを2〜3回測定する。
  • 中央値: 3回または5回の結果を並べ、最大値ではなく中央値で比較する。
  • 切替マージン: 現在ノードが300ms、新候補が290ms程度なら維持し、例えば100ms以上速い場合だけ切り替える。
  • 連続失敗数: 1回の失敗では切り替えず、同じノードが連続3回失敗した場合にフェイルオーバーする。
  • クールダウン: 切替後の数分間は再切替を抑制し、接続が安定する時間を確保する。

スクリプトへ簡単にマージンを追加する場合は、最速ノードと現在ノードの遅延を比較し、差が小さいときは切替処理を中止します。現在ノードの再測定に失敗した場合はマージン判定を行わず、候補の最良ノードへ切り替える設計が扱いやすいでしょう。

# 例:現在ノードが測定可能で、新候補が100ms以上速い場合だけ切替
SWITCH_MARGIN_MS = 100
if current_delay is not None:
    if best_delay + SWITCH_MARGIN_MS >= current_delay:
        logging.info("差が小さいため切替を見送り")
        return 0

ただし、APIの遅延測定が成功しても、実際のアプリ通信が正常とは限りません。テストURLに到達できることは、DNS、TLS、対象サービス、ルール設定のすべてが正常であることを意味しないためです。業務で使う場合は、低頻度の外部監視とClashログを組み合わせ、ノード選択の判断を一つのURLだけに依存させないようにします。

ログ管理、定期実行、手動復旧の設計

自動切替を常時動かす場合、ログには少なくとも測定時刻、グループ名、候補ノード、遅延、失敗理由、切替前後のノードを記録します。スクリプトのログにはサブスクリプションURL、認証トークン、ノードのパスワードなどを出力しないでください。ノード名に契約者情報が含まれる環境では、ログファイル自体のアクセス権も制限します。

Linuxではcronやsystemd timerで1〜5分間隔の実行を設定できます。cronの例は次のとおりです。

*/5 * * * * /usr/bin/python3 /opt/clash/clash_auto_switch.py

Windowsではタスクスケジューラで「ユーザーのログオン時」または「5分ごと」のタスクを作成し、プログラムにPythonの実行ファイル、引数にスクリプトの絶対パスを指定します。ノートPCでスリープ復帰後に複数のタスクが重複しないよう、「既に実行中の場合は新しいインスタンスを開始しない」を選ぶと安全です。

自動化の前に、まず手動実行で次の項目を確認します。

  1. APIの認証が通り、対象グループの名前が正確に取得できる。
  2. 候補ノードに実際のプロキシ名が入り、グループ名そのものを候補として誤認していない。
  3. 遅延測定が成功し、タイムアウトしたノードがログ上で判別できる。
  4. PUT後に /proxies を再取得すると、グループの now が変更されている。
  5. Clashの接続ログで新しいノードを通過していることを確認できる。

API障害時にプロキシを停止させない

スクリプトの目的はノードを切り替えることであり、APIに接続できない場合にシステムプロキシやTUNモードまで無効化することではありません。API障害時は現在の選択を維持し、ログへエラーを記録する設計が安全です。全候補が失敗した場合も、利用者が手動復旧できるように元のグループ設定を削除しないでください。

切替が不安定になった場合は、まず実行間隔を長くし、候補を信頼できるノードだけに絞ります。そのうえでテストURL、タイムアウト、再試行回数、切替マージンを調整してください。プロキシグループ自体の自動機能で足りるケースでは、外部スクリプトを常駐させず、mihomoの url-testfallback を使うほうが構成は単純です。API自動化は、複数の条件とログを含む運用ルールが必要な場合に採用するのが適切です。

DOWNLINK READY

Clashクライアントをダウンロード

API操作に対応したmihomo系クライアントを選び、外部コントローラーの設定とプロキシグループの動作を確認してください。

DOWNLINK READY

Clashクライアントをダウンロード

Windows、macOS、Linux、Android、iOS向けClashクライアントのダウンロード窓口を網羅。mihomoコア搭載版とGUI版を掲載。

Clash をダウンロード