Clash APIでノード自動切替|遅延監視と実践スクリプト設定

ノード障害や急な遅延を検知して、Clashのプロキシグループを自動変更するための技術ガイドです。API呼び出しの流れから実運用向けの監視、定期実行、トラブルシューティングまで確認できます。

Clash のプロキシグループには、複数のノードから手動で1つを選ぶ Selector、遅延を測定して自動選択する URLTest、フォールバック先を順番に試す Fallback などがあります。標準の自動選択だけで十分な場合もありますが、業務時間帯だけ特定のノードを優先したい、連続して失敗したノードを一時的に除外したい、切替後の結果をログに残したいという場合は、外部コントローラー API を使った監視スクリプトが役立ちます。

この記事では、mihomo 系クライアントで動作する外部コントローラーを前提に、現在のグループとノードを API から取得し、各ノードの遅延を測定して、選択グループの出口を変更する流れを説明します。Clash Verge Rev、Clash for Windows、ClashX、Clash for Android などでは画面上の名称や API の公開方法が異なる場合がありますが、コアが mihomo で、external-controller が有効なら基本的な仕組みは共通です。

外部コントローラーと API 認証を準備する

Clash API は、カーネルが待ち受ける外部コントローラーに HTTP リクエストを送って操作します。代表的な設定は 127.0.0.1:9090 で、API の入口は http://127.0.0.1:9090 です。クライアントの「設定」または「プロファイル編集」画面で、現在有効な設定に次のような項目があるか確認してください。

external-controller: 127.0.0.1:9090
secret: "change-this-secret"

external-controller が未設定の場合、API は利用できません。また、設定を変更しても既存のコアプロセスが再起動されるまで反映されないことがあります。ポートが別のアプリケーションに使われているとコアの起動自体に失敗するため、クライアントのログで「external controller listening」などの待ち受けメッセージを確認します。

コントローラーを LAN に公開しない

0.0.0.0:9090 で待ち受けると、同じネットワーク上の端末から API を操作できる状態になります。ノード切替だけでなく、設定取得やプロファイル変更まで許可される実装もあるため、通常は 127.0.0.1 に限定してください。どうしても遠隔監視が必要な場合は、ファイアウォール、強い secret、VPN または SSH トンネルを組み合わせます。

認証が設定されている場合、すべてのリクエストに次の HTTP ヘッダーを付けます。secret が空の環境ではヘッダーを省略できますが、運用スクリプトでは環境変数から読み込む形にして、ソースコードへ直接書かない方が安全です。

Authorization: Bearer change-this-secret
API用途方法
/version コアのバージョンと API 接続確認 GET
/proxies ノード、グループ、現在の選択状態を取得 GET
/proxies/<名前>/delay 指定したノードの遅延を測定 GET
/proxies/<グループ名> Selector の現在の出口を変更 PUT

遅延測定からノード切替までの API の流れ

自動切替は、単に「遅延の数字が最小のノードを選ぶ」処理ではありません。まず API に接続できることを確認し、次に対象グループの種類と子ノードを取得します。その後、同じ測定 URL とタイムアウト値で各ノードを検査し、成功した候補だけを比較します。最後に現在の選択先と新しい候補を比較して、条件を満たしたときだけ PUT で切り替えます。

# API の稼働確認
curl -s \
  -H "Authorization: Bearer change-this-secret" \
  http://127.0.0.1:9090/version

# 現在のグループとノードを取得
curl -s \
  -H "Authorization: Bearer change-this-secret" \
  http://127.0.0.1:9090/proxies

# 1つのノードを測定
curl -s \
  -H "Authorization: Bearer change-this-secret" \
  "http://127.0.0.1:9090/proxies/Node%20A/delay?url=http%3A%2F%2Fwww.gstatic.com%2Fgenerate_204&timeout=5000&expected=204"

# Selector の出口を変更
curl -s -X PUT \
  -H "Authorization: Bearer change-this-secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"Node A"}' \
  "http://127.0.0.1:9090/proxies/Proxy"

delay エンドポイントのノード名とグループ名は URL パスの一部です。日本語、空白、スラッシュ、記号を含む名前をそのまま連結すると 404 や不正な URL になるため、スクリプトでは必ず URL エンコードします。測定 URL は HTTP 204 を返す軽量なエンドポイントを使い、expected=204 と一致した場合だけ成功と判定します。対象サイトが 301 や 200 を返す場合は、実際のレスポンスに合わせて expected を変更してください。

遅延値はネットワーク全体の品質を表す絶対値ではありません。測定先までの DNS、TLS、経路、相手側の混雑に影響されるため、1つの URL だけで判断すると偏りが出ます。実用上は同じ URL を使って比較条件を統一し、短時間の一時的な変動で切り替わらないようヒステリシスを入れます。たとえば現在のノードが 180ms、新候補が 175ms であれば切り替えず、差が 30ms 以上ある場合だけ変更する方が安定します。

Selector と URLTest の役割を分ける

Selector は API から選択先を明示的に変更できるため、外部スクリプトとの相性が良いグループです。一方、URLTest はカーネル自身が定期的にテストして選択します。URLTest に対して外部スクリプトが頻繁に PUT を送ると、カーネルの自動選択とスクリプトの判断が競合します。自作ロジックを使う場合は Selector を専用に用意し、そこへ候補ノードを登録する構成が分かりやすいでしょう。

proxy-groups:
  - name: Auto-Script
    type: select
    proxies:
      - Node-A
      - Node-B
      - Node-C
      - DIRECT

実際の購読設定では、ノード名やグループ名が更新のたびに変わることがあります。スクリプトに固定名を記述する場合は、購読更新後に /proxies のレスポンスを確認してください。名前が変わる可能性がある環境では、ノード名の完全一致ではなく、プロバイダー名や名前の接頭辞を使って候補を絞る方法もあります。ただし似た名前のノードを誤って選ぶ危険があるため、最初は許可リスト方式が安全です。

Python で実装する遅延監視と自動切替

次の例は外部ライブラリを使わず、Python の標準ライブラリだけで動作する簡易スクリプトです。環境変数 CLASH_APICLASH_SECRETCLASH_GROUP を指定し、対象グループに含まれるノードを順番に測定します。現在のノードより十分に速い候補が見つかった場合だけ切り替え、失敗したノードは候補から除外します。

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

API = os.getenv("CLASH_API", "http://127.0.0.1:9090").rstrip("/")
SECRET = os.getenv("CLASH_SECRET", "")
GROUP = os.getenv("CLASH_GROUP", "Auto-Script")
TEST_URL = os.getenv(
    "CLASH_TEST_URL",
    "http://www.gstatic.com/generate_204"
)
TIMEOUT_MS = int(os.getenv("CLASH_TIMEOUT_MS", "5000"))
MIN_IMPROVEMENT_MS = int(os.getenv("CLASH_MIN_IMPROVEMENT_MS", "30"))
REQUEST_TIMEOUT = max(3, TIMEOUT_MS / 1000 + 2)

def headers(content_type=False):
    result = {"Accept": "application/json"}
    if SECRET:
        result["Authorization"] = "Bearer " + SECRET
    if content_type:
        result["Content-Type"] = "application/json"
    return result

def request_json(path, method="GET", body=None):
    data = None
    if body is not None:
        data = json.dumps(body).encode("utf-8")
    request = Request(
        API + path,
        data=data,
        headers=headers(body is not None),
        method=method
    )
    with urlopen(request, timeout=REQUEST_TIMEOUT) as response:
        raw = response.read()
        return json.loads(raw.decode("utf-8")) if raw else {}

def get_proxies():
    return request_json("/proxies")

def measure(node):
    path = "/proxies/" + quote(node, safe="")
    query = urlencode({
        "url": TEST_URL,
        "timeout": str(TIMEOUT_MS),
        "expected": "204"
    })
    result = request_json(path + "/delay?" + query)
    return int(result["delay"])

def switch_to(node):
    path = "/proxies/" + quote(GROUP, safe="")
    request_json(path, method="PUT", body={"name": node})

def main():
    try:
        proxies = get_proxies()
        group = proxies["proxies"][GROUP]
        current = group.get("now", "")
        candidates = [
            name for name in group.get("all", [])
            if name not in {"DIRECT", "REJECT"}
        ]
    except (KeyError, HTTPError, URLError, ValueError) as error:
        print("API またはグループの取得に失敗:", error, file=sys.stderr)
        return 2

    results = []
    for node in candidates:
        try:
            delay = measure(node)
            results.append((delay, node))
            print(f"{node}: {delay} ms")
        except (HTTPError, URLError, KeyError, ValueError, TimeoutError) as error:
            print(f"{node}: failed ({error})", file=sys.stderr)

    if not results:
        print("利用可能なノードがありません", file=sys.stderr)
        return 3

    results.sort(key=lambda item: item[0])
    best_delay, best_node = results[0]

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

    if current == best_node:
        print(f"変更なし: {current} ({best_delay} ms)")
        return 0

    if current_delay is not None:
        improvement = current_delay - best_delay
        if improvement < MIN_IMPROVEMENT_MS:
            print(
                f"変更なし: 現在 {current_delay} ms / "
                f"最良 {best_delay} ms"
            )
            return 0

    switch_to(best_node)
    print(f"切替: {current or '(未選択)'} -> "
          f"{best_node} ({best_delay} ms)")
    return 0

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

実行前に、グループ名が本当に Auto-Script か確認します。環境変数を使う場合は、Linux なら次のように指定できます。

export CLASH_API="http://127.0.0.1:9090"
export CLASH_SECRET="change-this-secret"
export CLASH_GROUP="Auto-Script"
export CLASH_TIMEOUT_MS="5000"
export CLASH_MIN_IMPROVEMENT_MS="30"

python3 clash_switch.py

この例では全候補を順番に測定するため、ノード数が 30 個あると測定時間も長くなります。外部コントローラーへの API 呼び出しと各遅延テストは、対象ノードへの接続を発生させる処理です。1分ごとに大量の候補を検査すると、回線やプロバイダーの制限に触れる可能性があります。まずは 5〜15 分間隔で実行し、必要なら候補を 5〜10 個程度に絞ってください。

切替の安定性を高めるポイント

1回の失敗だけで現在のノードを捨てず、2回連続失敗または一定時間内に複数回失敗した場合に切り替えると、瞬間的なパケットロスに強くなります。また、切替後に最低 2〜5 分は同じノードを維持するクールダウンを設けると、ノードが頻繁に往復するフラッピングを防げます。

定期実行と実運用での設計

手動実行で期待どおりに動くことを確認したら、定期実行へ移します。Linux の systemd timer や cron、Windows のタスク スケジューラ、macOS の launchd などを使えます。どの環境でも、クライアントが起動して API が待ち受けた後にスクリプトを実行することが重要です。起動直後はプロファイル読み込み中で、/proxies がまだ完成していない場合があります。

Linux の cron 設定例

スクリプトを /opt/clash/clash_switch.py に保存し、実行権限を付与したうえで、5分ごとに起動する例です。secret を crontab に直接書くと、プロセス情報や設定バックアップから見える場合があるため、権限を制限した環境ファイルから読み込む構成を推奨します。

# /etc/clash-switch.env
CLASH_API=http://127.0.0.1:9090
CLASH_SECRET=change-this-secret
CLASH_GROUP=Auto-Script
CLASH_TIMEOUT_MS=5000
CLASH_MIN_IMPROVEMENT_MS=30

# root の crontab
*/5 * * * * set -a; . /etc/clash-switch.env; set +a; /usr/bin/python3 /opt/clash/clash_switch.py >> /var/log/clash-switch.log 2>&1

環境ファイルには他のユーザーが読めない権限を設定します。GUI クライアントを一般ユーザーで起動し、cron を root で動かす場合は、127.0.0.1 にアクセスできてもプロファイルやログの所有者が異なることがあります。まずは同じユーザーで手動実行し、その後にサービス化してください。

ログ、通知、切替条件を記録する

自動化では「切り替わった」という結果だけでなく、切替前のノード、測定 URL、各ノードの遅延、失敗理由、API の HTTP ステータスも残します。ノード名には購読由来の個人識別情報が含まれる場合があるため、外部のログサービスへ送る前に名前を匿名化するか、ローカル保存に限定してください。

  • API 接続エラー:コアが停止中、ポート違い、secret 不一致を確認します。
  • 全ノードが失敗:測定 URL の応答、DNS、ローカルネットワーク、購読の有効期限を確認します。
  • 一部ノードだけ失敗:対象ノードの停止、SNI、TLS、サーバー側の制限を確認します。
  • 切替後も通信できない:Selector の変更だけで、システムプロキシや TUN が有効とは限らない点に注意します。
  • 頻繁に切り替わる:改善幅、連続失敗回数、クールダウン時間を大きくします。

切替できないときの確認順序

最初に /version が返るかを確認します。ここで 401 や 403 が返るなら secret、接続できないならアドレスとポートが原因です。次に /proxies を取得し、指定したグループ名がレスポンス内に存在するかを調べます。画面上の表示名と YAML の name が一致していないケース、全角スペースや末尾の空白が混ざっているケースは珍しくありません。

グループが存在しても PUT が失敗する場合は、そのグループが Selector ではない可能性があります。/proxies のグループ情報に含まれる type を確認し、select に相当するグループだけを手動切替の対象にします。URLTest や Fallback に対して手動で選択先を指定する設計は、コアの自動制御と衝突しやすいため避けてください。

症状確認する場所対処
接続拒否になる external-controller とクライアントログ API のポートと待ち受けアドレスを修正する
401 Unauthorized secret と Authorization ヘッダー Bearer を含め、secret の再入力を確認する
グループが見つからない /proxies のキー名 画面名ではなく API に返った正確な名前を使う
delay が常に失敗する 測定 URL、expected、DNS とノード接続 204 を返す URL とタイムアウト値を見直す
切替後も直接接続のまま システムプロキシ、TUN、mode、rules API の選択変更と通信の入口を別々に確認する

最後に、API でグループの出口を変更しても、Clash クライアント全体の動作モードが自動で変わるわけではありません。mode: rule ならルールが指定するグループへ通信が送られ、スクリプトが変更した Selector がそのグループとして参照される必要があります。別のルールが別グループを指定している場合、正しい Selector を変更していてもブラウザの通信には影響しません。ルール、グループ、システムプロキシまたは TUN の3層を分けて確認することが、自動切替を安定して運用する最短ルートです。

調査の前にクライアントとカーネルのバージョンを確認する

本記事で扱う proxy-server-nameserverunified-delaytcp-concurrent の3つのフィールドは mihomo カーネルが必要です。オリジナルの Clash カーネルはメンテナンスが終了しており、フィールドが効かない場合はまずカーネルの種類を確認し、ログと照らし合わせて項目ごとに調べてください。

ダウンロードページへ インストールガイドを見る

Clash をダウンロード