DEPRECATED (§122). Clash API из L×Box выпилен: Debug API
/clash/*proxy и роутGET /state/clashудалены (commit2711f5b), UI/runtime-контроль переехали на libbox CommandClient. Запрос на/clash/*теперь вернёт404. Документ сохранён как историческая справка по поведению sing-box clash-api — для актуальных роутов см.debug-api-reference.md.
| Поле | Значение |
|---|---|
| Статус | Deprecated (§122 — Clash API dropped) |
| Дата | 2026-04-20 |
| Версия sing-box | 1.12.12 (libbox, JitPack) |
| Источник | live-разведка через 031 debug api + чтение sing-box/experimental/clashapi |
Документация сфокусирована на реальном поведении sing-box'овского clash-api (а не на upstream Clash.Meta — см. секцию Differences from upstream). Все ответы и curl-команды взяты с боевого устройства (OnePlus CPH2411, Android 15, VPN connected, 150+ nodes подписок).
Все примеры — через L×Box Debug API proxy на 127.0.0.1:9269. Сам Clash API слушает на рандомном порту (/state/clash → base_uri) и требует свой секрет — знать его не нужно, proxy подмешивает.
adb forward tcp:9269 tcp:9269
TOK="<debug_token from App Settings → Developer>"
curl -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/versionВажное свойство proxy: handler в debug/handlers/clash.dart читает upstream через streamed.stream.toBytes() — то есть буферит весь ответ до конца. Для streaming-эндпоинтов (/traffic, /memory, /logs) это значит, что через proxy они никогда не возвращаются — упираются в 25s timeout handler'а и падают UpstreamError. Смотри Streaming endpoints.
Список полный — регистрируется в server.go:117–131 + setupMetaAPI.
Path (под /clash) |
Method | Назначение | Streaming? |
|---|---|---|---|
/ |
GET | hello + (опц.) редирект на /ui/ |
нет |
/version |
GET | версия sing-box | нет |
/logs |
GET | поток логов (JSON lines / WebSocket) | да |
/traffic |
GET | поток скоростей {up,down} в секунду |
да |
/memory |
GET | поток RSS в секунду | да |
/configs |
GET / PUT / PATCH | schema + смена mode | нет |
/proxies |
GET | карта всех proxies + GLOBAL |
нет |
/proxies/{tag} |
GET | single proxy | нет |
/proxies/{tag} |
PUT | selector switch | нет |
/proxies/{tag}/delay |
GET | ping одного outbound | нет |
/group |
GET | список группы (только OutboundGroup'ы) | нет |
/group/{tag} |
GET | single группа | нет |
/group/{tag}/delay |
GET | ping всех членов группы | нет |
/rules |
GET | полный список route-правил | нет |
/connections |
GET | snapshot или WebSocket streaming | снапшот да, WS нет |
/connections |
DELETE | закрыть все + ResetNetwork() |
нет |
/connections/{id} |
DELETE | закрыть одно | нет |
/providers/proxies |
GET | {"providers":{}} — sing-box не поддерживает |
нет |
/providers/proxies/{name} |
GET/PUT | 404 всегда | нет |
/providers/proxies/{name}/healthcheck |
GET | 404 всегда | нет |
/providers/rules |
GET | {"providers":[]} — пусто (note: array, не object) |
нет |
/providers/rules/{name} |
GET/PUT | 404 всегда | нет |
/script |
POST / PATCH | {"message":"not implemented"} / 204 |
нет |
/profile/tracing |
GET | всегда 404 (закомменчено в upstream) | нет |
/cache/fakeip/flush |
POST | 204 если fakeip есть, 500 bucket not found если нет |
нет |
/dns/query |
GET | resolve через router DNS | нет |
/upgrade/ui |
POST | download external UI (404 если external_ui не в config'е) |
нет |
Proxy НЕ поддерживает WebSocket upgrade — это обычный http.Client.send, без ws. Если хочешь WS — ходи напрямую на base_uri из /state/clash, но там секрет замаскирован (***) в API (см. debug_storage_view.dart, секрет в /state/clash намеренно не светится). Для reverse-engineering WS — собирай local debug-build где секрет в debug_storage_view не маскируется.
Sanity-check. Отвечает всегда одинаково:
{"meta":true,"premium":true,"version":"sing-box 1.12.12"}meta: true, premium: true — захардкожено в server.go:428 для совместимости с Yacd-like dashboard'ами.
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/versionКорень clash-api. Захардкожено в server.go:292:
{"hello":"clash"}Если в конфиге задан experimental.clash_api.external_ui — редиректит на /ui/. В L×Box external_ui не используется → всегда JSON.
Quirk: GET /clash (без /) возвращает 400 {"error":{"code":"bad_request","message":"empty upstream path"}} — это защита Debug proxy, а не upstream. Всегда ставь / в конце.
curl -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/Полный dump всех outbound'ов в форме Clash-подобной карты. В L×Box при 150 nodes весит ~45 KB.
Top-level:
{"proxies": {"<tag>": {<proxy>}, ...}}Каждый proxy имеет минимальный набор полей (см. proxies.go:61 proxyInfo):
{
"type": "VLESS|Shadowsocks|Hysteria2|VMess|Trojan|WireGuard|Direct|Selector|URLTest|Fallback|Reject|...",
"name": "<tag>",
"udp": true,
"history": [{"time":"2026-04-20T13:57:50.886498532+03:00","delay":47}]
}history — массив из 0 или 1 элемента (только последний замер, sing-box хранит 1, не 10 как upstream).
Для групп (Selector / URLTest / GLOBAL) добавляется:
{
"now": "<current selected>",
"all": ["<member1>","<member2>",...]
}Пример ответа (сокращён) — найденные типы в боевом config'е:
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/proxies{
"proxies": {
"GLOBAL": {
"type": "Fallback",
"name": "GLOBAL",
"udp": true,
"history": [],
"all": ["<все proxies кроме Direct/Block/DNS>"],
"now": "vpn-1"
},
"direct-out": {
"type": "Direct",
"name": "direct-out",
"udp": true,
"history": [{"time":"2026-04-20T13:57:50.886498532+03:00","delay":47}]
},
"vpn-1": {
"type": "Selector", "name": "vpn-1", "udp": true,
"history": [...],
"now": "BL: 🇳🇱 The Netherlands, Eindhoven | [BL]",
"all": ["BL: 🇪🇪 Estonia, Tallinn | [BL]", ...]
},
"✨auto": {
"type": "URLTest", "name": "✨auto", "udp": true,
"history": [{"time":"2026-04-20T13:57:20.080014838+03:00","delay":228}],
"now": "BL: 🌐 Anycast-IP | [IPv6] | [BL]-7",
"all": [...]
},
"⚙ wg-parnas": {
"type": "WireGuard", "name": "⚙ wg-parnas", "udp": true,
"history": [{"time":"2026-04-20T13:57:50.833602916+03:00","delay":89}]
},
"BL: 🇪🇪 Estonia, Tallinn | [BL]": {
"type": "VLESS", "name": "BL: 🇪🇪 Estonia, Tallinn | [BL]", "udp": true, "history": []
},
"BL: 🇺🇸 United States, El Segundo | 🌐 | [IPv6] | [BL]": {
"type": "Shadowsocks", "name": "...", "udp": true, "history": []
},
"BL: 🇫🇷 France, Paris | [BL]-3": {
"type": "Hysteria2", "name": "...", "udp": true, "history": []
},
"BL: 🇳🇱 The Netherlands, Amsterdam | [BL]-3": {
"type": "VMess", "name": "...", "udp": true, "history": []
},
"BL: 🇫🇷 France, Paris | [BL]-4": {
"type": "Trojan", "name": "...", "udp": true, "history": []
}
}
}Важные свойства:
- Ни один протокол-специфичный параметр не отдаётся: ни адрес сервера, ни порт, ни cipher, ни UUID/password, ни ALPN, ни transport. Всё это — только в исходном конфиге через
GET /config. Dashboard'ы, которым нужны эти детали, должны парсить конфиг отдельно. - Direct / Block / DNS outbound'ы в
allсписке GLOBAL отфильтрованы (proxies.go:101), но в общем Map есть. GLOBAL.allсортируется stable так, чтоnowстоит первым — косметика для dashboard'ов.GLOBALотсутствует в outbound'ах sing-box'а — это синтетическая запись, подмешиваемая при формировании ответа (proxies.go:115). Отсюда quirk — см. ниже/proxies/GLOBAL.
Detail view single proxy. Шаблон:
{"type":"...","name":"...","udp":true,"history":[...]}Для группы — добавляются now + all.
URL-encoding обязателен для тегов с юникодом. Боевые теги в L×Box содержат эмодзи флагов + ✨ + ⚙:
# ✨auto
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/proxies/%E2%9C%A8auto"
# BL: 🇪🇪 Estonia, Tallinn | [BL]
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/proxies/BL%3A%20%F0%9F%87%AA%F0%9F%87%AA%20Estonia%2C%20Tallinn%20%7C%20%5BBL%5D"Pro-tip: python3 -c "import urllib.parse; print(urllib.parse.quote('$TAG'))".
Quirk — GLOBAL 404: GET /clash/proxies/GLOBAL возвращает 404 {"message":"Resource not found"}. Причина — GLOBAL только мёрджится в общий /proxies response, в реальном outbound registry его нет, а findProxyByName (proxies.go:49) идёт через server.outbound.Outbound(name). Значит GLOBAL списком получить можно, детально — нельзя.
Переключение selector на ребёнка. Body: {"name": "<child tag>"}. Ответ: 204 No Content при успехе.
# Switch vpn-1 → Paris
curl -X PUT -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d '{"name":"BL: 🇫🇷 France, Paris | [BL]"}' \
"http://127.0.0.1:9269/clash/proxies/vpn-1"
# → 204
# Verify switched
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/proxies/vpn-1" | jq '.now'
# → "BL: 🇫🇷 France, Paris | [BL]"Ошибки:
- Non-member в
.all:400 {"message":"Selector update error: not found"}. - Target не Selector (URLTest, VLESS, Direct, etc):
400 {"message":"Must be a Selector"}. - Bad JSON body:
400 {"message":"Bad Request"}(ErrBadRequest).
Critical: switch на URLTest группу запрещён. URLTest .now определяется автоматически по /delay + scheduled urltest_interval.
Ping одного outbound. Параметры:
url— HTTPS URL для теста;http://молча заменяется на пустую строку (proxies.go:191) и используется default изurltest-пакета (https://www.gstatic.com/generate_204). Если хочешь точный URL — шли HTTPS.timeout— миллисекунды, парсится как int16 (proxies.go:194) — максимум 32767 ms, любое значение > этого → 400 Bad Request. (Для/group/:tag/delayлимит int32 — там он в 2 млрд, но timeout тот же context deadline, ждать больше смысла нет.)
Успех:
{"delay": 152}Статусы:
200—{"delay": N}.504—{"message":"Request Timeout"}— ctx expired до завершения теста.503—{"message":"An error occurred in the delay test"}— дозвонились, но ошибка в L4/TLS/HTTP или delay=0 (sing-box считает 0 ms невалидным замером и возвращает 503).
Поведение по типу proxy:
| type | /delay | комментарий |
|---|---|---|
| Direct | работает | тестирует через host машину, игнорируя VPN |
| VLESS / SS / Trojan / Hysteria2 / VMess / WireGuard | работает но часто 503 | sing-box дозванивается до node независимо от того, выбрана ли она в активной цепочке. Для нод, которые давно down/банят / 503. |
| Selector | работает, тестирует .now |
внутри Selector делегирует тест в .now ребёнка. Если .now сейчас тормозит — 504. |
| URLTest | работает, тестирует .now |
аналогично Selector'у |
GLOBAL |
404 | см. выше |
# HTTPS, правильно
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/proxies/direct-out/delay?url=https%3A%2F%2Fcp.cloudflare.com%2Fgenerate_204&timeout=5000"
# → {"delay":152}
# int16 overflow — BadRequest
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/proxies/direct-out/delay?url=https%3A%2F%2Fcp.cloudflare.com%2Fgenerate_204&timeout=40000"
# → 400 {"message":"Bad Request"}Side-effect: успешный delay персистит в urlTestHistory — после вызова GET /proxies/{tag} покажет history: [{time, delay}]. Ошибка — DeleteURLTestHistory, т.е. history очищается. Это единственный путь заполнить history в /proxies ответе, иначе они пустые.
Список всех групповых outbound'ов (Selector / URLTest / Fallback / etc) в форме как /proxies, но отфильтрованное:
{
"proxies": [
{"type":"URLTest","name":"✨auto","udp":true,"history":[...],"now":"...","all":[...]},
{"type":"Selector","name":"vpn-1","udp":true,"history":[...],"now":"...","all":[...]}
]
}Обрати внимание: proxies — массив, не объект (в отличие от /proxies). Это Clash.Meta convention, sing-box следует (api_meta_group.go:33).
curl -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/groupСиноним /proxies/{tag}, но только для групп. Не-группа → 404 Resource not found.
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/group/%E2%9C%A8auto"Ping всех членов группы параллельно. Response — Map<memberTag, delayMs>:
{
"BL: 🇩🇪 Germany, Bad Soden am Taunus (Neuenhain) | [BL]": 275,
"BL: 🇫🇮 Finland, Helsinki | [BL]": 399,
"BL: 🇲🇩 Moldova, Chisinau | [BL]": 2113
}Quirks (из api_meta_group.go:59):
-
Только успешно протестированные ноды в результате. Failed (error / timeout) — ИСКЛЮЧАЮТСЯ из response +
DeleteURLTestHistoryу них. Если группа в 100 нод и в результате 3 записи — 97 нод провалились (не "97 не тестились"). Это критически важно понимать, интерпретируя response: отсутствие ноды в map ≠ "не проверялась", а "проверилась и упала". -
Для URLTest групп
urlquery игнорируется.URLTestGroup.URLTest(ctx)использует URL из конфига outbound'а (experimental.clash_api.default_urlили кастомныйurlв самой URLTest секции). Передаватьurlв query бессмысленно. Для Selector / других групп —urlиспользуется. -
http://→ пустая строка, как у/proxies/:tag/delay(тот же filter). -
Concurrency = 10 (
batch.WithConcurrencyNum[any](10)). При группе в 150 нод иtimeout=3000реальное время теста — up toceil(150/10) * 3000 = 45000 ms, но ctx deadline = 3000 ms обрывает всё через 3с. Ноды, которые не успели — пропущены. -
Дедупликация по realTag — sing-box выкидывает duplicates через
group.RealTag(detour). Если вallодна и та же node дважды под разными aliases — тестится один раз. -
Возврат 504 при ошибке в
urlTestGroup.URLTest(ctx)(только для URLTest-групп) — например если ctx deadline exceeded до того, как хоть одна нода ответила. Для Selector-групп batch.Wait() всегда возвращает — 504 не будет, просто пустой{}.
# Selector — честно тестит, возвращает только успешных
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/group/vpn-1/delay?url=https%3A%2F%2Fcp.cloudflare.com%2Fgenerate_204&timeout=3000"
# → {"BL: 🇫🇷 France, Paris | [BL]":169, ...}
# URLTest — url игнорируется, использует config'овый
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/group/%E2%9C%A8auto/delay?url=https%3A%2F%2Fignored.example%2F&timeout=3000".now у URLTest после /group/:tag/delay: observed behavior — .now не обновляется этим вызовом. URLTest group имеет internal urltest_interval tick; /delay обновляет individual node histories, но group'а перевыбирает .now по своему расписанию. Эмпирическая проверка: now оставался старым после forced /delay, новый .now появился после urltest_interval ticka.
Snapshot активных TCP/UDP трекеров. WebSocket upgrade не поддерживается нашим proxy (см. Streaming note) — только snapshot.
Top-level структура:
{
"downloadTotal": 6249733,
"uploadTotal": 450203,
"memory": 14368768,
"connections": [...]
}| Поле | Тип | Semantic |
|---|---|---|
uploadTotal |
int64 | Кумулятивный upload с момента запуска sing-box (байты). Не сбрасывается reset'ом. |
downloadTotal |
int64 | Аналогично для download. |
memory |
uint64 | runtime.MemStats.HeapInuse в момент snapshot'а. Не RSS процесса — а Go heap. Может быть в разы меньше реального RSS. |
connections |
array | Снимок живых трекеров. Dead connections пропадают. |
Элемент connections[]:
{
"id": "d38af8ef-0ec0-4832-baf0-4e041134ac4c",
"chains": ["BL: 🇳🇱 The Netherlands, Eindhoven | [BL]", "vpn-1"],
"rule": "final",
"rulePayload": "",
"start": "2026-04-20T14:09:17.929085419+03:00",
"upload": 3772,
"download": 17196,
"metadata": {
"network": "tcp",
"type": "tun/tun-in",
"sourceIP": "172.16.0.1",
"sourcePort": "40844",
"destinationIP": "64.233.163.101",
"destinationPort": "443",
"host": "clients4.google.com",
"dnsMode": "normal",
"processPath": "com.android.chrome (10140)"
}
}Все поля — см. Field reference: connections[].
Закрыть все активные connections и вызвать router.ResetNetwork(). Возвращает 204.
ResetNetwork() не просто закрывает соединения — он ещё сбрасывает DNS-кэш и состояние TUN'а. В момент вызова есть короткая пауза в трафике. Делать без нужды — не стоит.
curl -X DELETE -H "Authorization: Bearer $TOK" \
http://127.0.0.1:9269/clash/connections
# → 204Закрыть конкретное соединение по UUID.
curl -X DELETE -H "Authorization: Bearer $TOK" \
http://127.0.0.1:9269/clash/connections/d38af8ef-0ec0-4832-baf0-4e041134ac4c
# → 204Quirk — невалидный UUID молча 204: id парсится через uuid.FromStringOrNil(id) → любая мусорная строка даёт zero-UUID → линейный поиск в snapshot'е не находит совпадения → render.NoContent. Соответственно, 204 не означает "закрыто". Если нужно удостовериться — перезапроси /connections и убедись что id там нет.
curl -X DELETE -H "Authorization: Bearer $TOK" \
http://127.0.0.1:9269/clash/connections/not-a-uuid
# → 204 (no error, no action)Полный dump routing rules.
{
"rules": [
{"type":"default","payload":"inbound=tun-in","proxy":"resolve(prefer_ipv4)"},
{"type":"default","payload":"inbound=tun-in","proxy":"sniff(1s)"},
{"type":"default","payload":"protocol=dns","proxy":"hijack-dns"},
...
]
}Поля: type (default / logical), payload (stringified matcher), proxy (action — может быть outbound tag или псевдо-action вроде resolve(...) / sniff(...) / hijack-dns / reject / route-options(...)).
Порядок — исходный из route.rules[], plus implicit rules (resolve/sniff/hijack-dns добавляет сам sing-box в начале). Action отображается через rule.Action().String() — для route-action {"outbound":"X"} вернёт просто "X".
curl -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/rules{
"port": 0,
"socks-port": 0,
"redir-port": 0,
"tproxy-port": 0,
"mixed-port": 0,
"allow-lan": false,
"bind-address": "*",
"mode": "Rule",
"mode-list": ["Rule"],
"log-level": "warn",
"ipv6": false,
"tun": null
}Всё кроме mode / mode-list / log-level / bind-address — захардкожено в нулях/дефолтах (configs.go:36), не отражает реальный конфиг (в sing-box нет понятия mixed-port / redir-port — это Clash-legacy). ipv6: false и tun: null — тоже заглушки; настоящее состояние TUN живёт в /config на Debug API (не в clash-api!).
mode-list в L×Box = ["Rule"] (один элемент). Clash.Meta обычно имеет ["Rule","Global","Direct"], но sing-box experimental.clash_api.mode_list в конфиге пустой → default mode приклеивается один.
log-level — нормализуется: trace → debug, всё ниже error → error (configs.go:38). В ответе увидишь только debug|info|warn|error, не оригинальное значение из config'а.
Поменять mode. Body:
{"mode":"Global"}Response: 204 No Content.
Side-effects: dnsRouter.ClearCache() + сохранение в cache_file (если включён). Если mode не в mode-list — молча игнорирует (предварительно пробует case-insensitive match). mode == current mode — тоже молча игнорирует.
В L×Box mode_list = ["Rule"] → смена mode эффективно NO-OP, любые {"mode":"Global"} / {"mode":"Direct"} не пройдут фильтр. PATCH всегда вернёт 204, но GET /configs покажет прежний mode. Проверено live.
# NoOp — mode остаётся Rule
curl -X PATCH -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d '{"mode":"Global"}' \
"http://127.0.0.1:9269/clash/configs"
# → 204
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/configs | jq .mode
# → "Rule"PUT — просто возвращает 204. Никакой логики (configs.go:69). В upstream Clash это reload конфига из файла; в sing-box — заглушка.
Выполнить DNS запрос через sing-box DNS router (с применением dns.rules, strategy, etc).
Параметры:
name— запрашиваемое имя (FQDN или host; sing-box добавит trailing dot).type— тип записи:A|AAAA|MX|TXT|SOA|NS|PTR|CNAME|SRV|CAA|.... DefaultA. Regex-less — если тип неизвестенdns.StringToType→ 400{"message":"invalid query type"}.
Response (упрощённый):
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": false,
"CD": false,
"Server": "internal",
"Question": [{"Name":"cloudflare.com.","Qtype":1,"Qclass":1}],
"Answer": [
{"name":"cloudflare.com.","type":1,"TTL":300,"data":"104.16.133.229"},
{"name":"cloudflare.com.","type":1,"TTL":300,"data":"104.16.132.229"}
]
}Дополнительные секции при наличии: Authority[] (SOA/NS), Additional[]. Формат полей внутри RR:
name— строка (FQDN с точкой).type— числовой DNS type (A=1,AAAA=28,SOA=6, etc — decimal uint16).TTL— секунды.data— stringified RDATA без заголовка. ДляA— dotted IP. ДляSOA—mname rname serial refresh retry expire minimumчерез пробел. ДляMX—preference exchange. И так далее (формат:miekg/dns.RR.String()минусHeader.String()).
Не путать с dnsMode в connections metadata — он захардкожен "normal", а запросы здесь проходят через настоящий router'ский resolver.
Timeout — hardcoded C.DNSTimeout (10s).
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/dns/query?name=cloudflare.com&type=A"
# AAAA
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/dns/query?name=example.com&type=AAAA"
# SOA root
curl -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/dns/query?name=.&type=SOA"sing-box не поддерживает proxy providers. Response — пустая карта:
{"providers":{}}Любой GET/PUT /clash/providers/proxies/{name} → 404 {"message":"Resource not found"}. В коде тело handler'ов закомменчено (provider.go:61).
То же что и proxy providers, но массив вместо объекта — upstream-inconsistency, не наша:
{"providers":[]}{"message":"not implemented"}Status 400. Весь код starlark-инжекции закомменчен.
204, NoOp.
405 Method Not Allowed.
Всегда 404. Тело WS-стриминга закомменчено (profile.go:17) — функциональность деактивирована.
Reset FakeIP mapping table. Успех — 204. Если fakeip не включён в DNS config'е (как в L×Box) — 500 {"message":"bucket not found"}.
curl -X POST -H "Authorization: Bearer $TOK" \
http://127.0.0.1:9269/clash/cache/fakeip/flush
# → 500 {"message":"bucket not found"}Скачать (обновить) external UI через sing-box'овский downloader. В L×Box external_ui не задан → 404 {"message":"external UI not enabled"}.
Все три построены одинаково: infinite-loop с time.NewTicker(1 sec) + json.Encoder.Encode(...) + w.(http.Flusher).Flush(). Поддерживают WebSocket upgrade (?token=...&level=... для WS auth).
Через L×Box Debug API proxy не работают. Handler (clash.dart:52) читает upstream через streamed.stream.toBytes() — это блокирующий full-body read, возврата не будет пока upstream не закроет соединение. sing-box их никогда не закрывает → 25s timeout → Upstream: Clash API timeout.
Три пути, если тебе нужны эти данные:
-
Use Debug API alternatives (что уже есть):
/stateвозвращаетtraffic: {up_total, down_total, active_connections}— снапшот, обновляется при каждом запросе./logs?limit=N— AppLog (включает Dart-логи + некоторые core-события)./clash/connections— top-levelmemoryполе — это replacement для/memory.
-
Direct access to clash-api port — но secret'а нет в
/state/clash(замаскирован***), нужен local-build. Не делать в production. -
Переписать proxy handler на streaming — если понадобится, заменить
stream.toBytes()наpipe()+req.response.add(chunk); req.response.flush(). Требует API surgery вBytesResponse.
| Endpoint | Ticker | Body / entry |
|---|---|---|
/traffic |
1s | {"up":N,"down":N} — delta за последнюю секунду (не cumulative). |
/memory |
1s | {"inuse":N,"oslimit":0} — N = heap inuse. Первый tick всегда inuse:0 (захардкожено в api_meta.go:67 — "make chat.js begin with zero"). |
/logs |
event-driven | `{"type":"info |
Формат — NDJSON без разделителя: {...}{...}{...} подряд (не \n-separated). Это ломает наивный json.NewDecoder().Decode() — на большинстве парсеров работает (readers eager), но json.loads(full_body) не пройдёт.
Единый shape выдачи proxyInfo() (proxies.go:61):
| Поле | Тип | Пример | Semantic |
|---|---|---|---|
type |
string | VLESS, Shadowsocks, Selector, URLTest, Fallback, Direct, Reject |
ProxyDisplayName(type), с переименованием Block → Reject для Clash-compat. Регистр — CamelCase (но см. quirk про lowercase urltest в старых 1.8.x). |
name |
string | ✨auto, BL: 🇪🇪 Estonia, Tallinn | [BL] |
Тег outbound'а. Может содержать любой unicode включая эмодзи. |
udp |
bool | true |
UDP support. Для VLESS/SS/Trojan/Hysteria/Wireguard обычно true. Для TCP-only (rare) — false. |
history |
[{time,delay}] или [] |
[{"time":"2026-04-20T13:57:50.886498532+03:00","delay":47}] |
Массив из 0 или 1 элемента (последний замер). time — RFC3339 с нано-precision. delay — ms uint16. Очищается при первой ошибке в /delay. |
now |
string | BL: 🇳🇱 The Netherlands, Eindhoven | [BL] |
Только для групп (Selector / URLTest / Fallback / GLOBAL). Текущий выбранный child. Для URLTest до первого успешного test cycle — пустая строка. |
all |
string[] | ["child1","child2",...] |
Только для групп. Список всех members в порядке config'а. Для GLOBAL отсортировано так, чтобы now стоял первым (stable sort). |
Чего НЕТ (а в upstream Clash.Meta иногда есть):
server/port/cipher/password/uuid/alpn/sni— никогда.testUrl/intervalдля URLTest — никогда (только в config).alive/dead/lastAlive— никогда (только через/delayhistory).xudp— никогда.
Если нужны detail'ы — парси /config.
Выдаётся MarshalJSON() в trafficontrol/tracker.go:31. Всё ниже — именно так, как приходит, не upstream-конвенция.
| Поле | Тип | Пример | Semantic |
|---|---|---|---|
id |
string (uuid-v4) | d38af8ef-0ec0-4832-baf0-4e041134ac4c |
UUID трекера, генерится при создании коннекта. Стабилен до close. |
chains |
string[] | ["BL: 🇳🇱 The Netherlands, Eindhoven | [BL]", "vpn-1"] |
Reverse: [terminal, ..., outermost_group]. Первый элемент — реальный outbound через который идёт трафик, далее вложенные группы снаружи внутрь. Если цепочка ещё не раскрутилась (например URLTest без выбранного .now) — может оборваться на имени группы. |
rule |
string | final, default => route(vpn-1), logical(...) => route-options(...) |
rule.String() + " => " + rule.Action().String() либо "final" если правило не matched (default route). В формат входит "стрелка" — для простого route это просто <matcher> => <outbound_tag>. |
rulePayload |
string | "" |
Всегда пустая строка. Поле присутствует для совместимости с Clash.Meta, но sing-box его не заполняет (tracker.go:85). Ранее семантика была rule.RawPayload(); в sing-box убрано. |
start |
string (RFC3339) | 2026-04-20T14:09:17.929085419+03:00 |
Время создания трекера. Nano-precision, с offset'ом. |
upload |
int64 | 3772 |
Кумулятивный upload этого коннекта в байтах. |
download |
int64 | 17196 |
Аналогично. |
metadata |
object | — | См. ниже. |
| Поле | Тип | Пример | Semantic |
|---|---|---|---|
network |
string | tcp, udp |
L4 протокол. Нижний регистр. |
type |
string | tun/tun-in, socks/mixed-in, http |
Формат: <inboundType>/<inboundTag> если tag задан, иначе просто <inboundType>. Для L×Box TUN inbound всегда tun/tun-in (у нас tag = tun-in). Upstream Clash.Meta кладёт сюда часто только inbound tag → ломает dashboard'ы, ожидающие просто tun или Mixed. |
sourceIP |
string | 172.16.0.1 |
IP source. Для TUN — всегда IP из tun-subnet (172.16.0.1 у нас). Для IPv6 — без квадратных скобок (fe80::1). |
sourcePort |
string (!) | 40844 |
STRING, не number! Легко пропустить при парсинге. F.ToString(uint16). |
destinationIP |
string | 64.233.163.101 |
Destination IP после sniff'а и маппинга, но до dial'а. |
destinationPort |
string (!) | 443 |
STRING. |
host |
string | clients4.google.com, "" |
FQDN after sniff (Metadata.Domain или Metadata.Destination.Fqdn). Если приложение пошло на raw IP без SNI/Host header — пустая строка (не null). В L×Box видел пустой host для: прямых VoIP конект (com.grandstream.wave), push mtalk, некоторые background-services типа Oplus. |
dnsMode |
string | normal |
ЗАХАРДКОЖЕНО "normal" всегда (tracker.go:77). Upstream Clash имеет `normal |
processPath |
string | com.android.chrome (10140), 1000, "" |
Process hint от Android. Формат приоритетов:<package> (<uid>) если и package и uid известны<package> (<user>) если user задан (редко)<package> если только package<uid> (просто число) если только uid"" если ничего нет (root system processes). UID 1000 = system. |
-
destinationIP!= original target — после sniff'а TCP через TUN идёт rewrite. Еслиhostпустой — destinationIP это raw сокет target. Еслиhostесть — destinationIP то, куда sing-box резолвнулhost. -
processPathс суффиксом(uid)— суффикс стабильно есть для app-инстанцированных процессов; отсутствует для system услуг. Dart-код_extractPackage()вclash_api_client.dart:269режет по' ('— корректно. Если в будущем появится user-фаза (com.x (user)) — сломается; не критично, заменятся наcom.x. -
Нет
remoteDestinationилиsniffedDomainотдельно — всё вhost. -
Нет
inboundIP/inboundPort— inbound bind неизвестен, только tag+type. -
Нет
mode/outboundна уровне metadata — но естьchains[0]который эффективно тот же outbound tag.
Собрано в один bullet-list для быстрой reference'и.
http://URL в/delayмолча пропадает (proxies.go:191).if strings.HasPrefix(url, "http://") { url = "" }— используется defaulthttps://www.gstatic.com/generate_204. Пиши HTTPS.timeoutдля/proxies/:tag/delay— int16 (max 32767); для/group/:tag/delay— int32. Overflow → 400 Bad Request.- URL в
/group/:tag/delayдля URLTest групп игнорируется — URLTest.URLTest(ctx) использует свой config'овый URL.
- URLTest
.nowпосле/group/:tag/delayне обновляется — тесты сохраняют individual histories, но.nowпереизбирается на периодическомurltest_intervaltick'е (default 3m, мы ставим свой). .nowURLTest пустой до первого tick'а (если приложение только запустилось, VPN ещё не prewarm'илась).- chains[] в reverse —
[terminal, intermediate, outermost]; первый элемент — реальный outbound. - chains может обрываться на tag'е группы если URLTest ещё не выбрал child.
- PUT
/proxies/:tagработает ТОЛЬКО на Selector —Must be a Selector400 для URLTest и всех остальных. - GLOBAL 404 —
GET /proxies/GLOBALвозвращает404 Resource not found. Есть только в/proxiesaggregate.
rulePayloadвсегда пустая строка — sing-box не заполняет, не ждите.dnsModeвсегда"normal"— hardcoded.metadata.type = <inboundType>/<inboundTag>— не просто inbound tag. У нас этоtun/tun-in.processPathс(uid)суффиксом —com.foo (10042). Парсить регулярным\s*\(\d+\)$, а не трактовать весь processPath как package.sourcePortиdestinationPort— строки, не числа.destinationIP— после sniff'а (для host-known коннектов — это резолвнутый IP).- DELETE /connections/{id} с невалидным UUID молча возвращает 204 — no error.
- DELETE /connections закрывает всё + ResetNetwork() — не просто close, а clearDNS + TUN reset. Пауза в трафике.
memorytop-level — это Go heap inuse, не RSS процесса; обычно в разы меньше.
/traffic,/memory,/logsчерез Debug API proxy не работают — buffering. Используй/state+/logs?limit=N+/connections.memory./memoryпервый tick всегда 0 даже если heap не пустой — захардкожено "для chat.js" в upstream./trafficотдаёт delta в секунду, не cumulative —up = new - prevкаждый tick.- NDJSON stream без разделителя —
{...}{...}{...}подряд. Неjson.loads(body).
/configsGET отдаёт захардкоженные нули для port / socks-port / redir-port / tproxy-port / mixed-port / allow-lan / ipv6 / tun. Не смотри на них.PATCH /configsmode смена — приmode_list=["Rule"](наш случай) эффективно no-op для любых других значений. Возвращает 204 ноmodeостаётся прежним.PUT /configs— заглушка (упрощает миграцию dashboard'ов, реально ничего не делает).log-levelв/configs— нормализуется (trace→debug, <error→error). Не оригинал.
GET /providers/proxiesотдаёт{"providers":{}}(object),GET /providers/rulesотдаёт{"providers":[]}(array) — inconsistency в upstream, sing-box её дублирует.- Любой
/providers/{proxies,rules}/{name}— 404.
/profile/tracing— всегда 404 (закомменчен код WS-стриминга)./scriptPOST — 400not implemented./scriptPATCH — 204 no-op./upgrade/ui— 404 еслиexternal_uiне в config'е (всегда у нас).
- Теги с юникодом (эмодзи, кириллица) — URL-encode обязательно. sing-box использует
getEscapeParam(common.go) для parsing — не-encoded характеры с пробелами ломают path routing.
GET /clash(без/) →400 empty upstream path(наш handler, не sing-box). Всегда с/./state/clashотдаётsecret: "***"— прямой доступ кbase_uriневозможен через внешний curl. Всё через/clash/*.
Собрано по результатам разведки sing-box 1.12.12 vs Clash.Meta docs.
| Область | Upstream Clash.Meta | sing-box 1.12.12 |
|---|---|---|
metadata.type |
Обычно строка-тип inbound'а: Mixed, Tun, HTTP. |
<inboundType>/<inboundTag> склеено — tun/tun-in. |
metadata.dnsMode |
Enum: `normal | fake-ip |
rulePayload |
Строка после => в правиле (actual matched payload). |
Всегда "". |
proxies[].type для rejected |
Reject |
Reject (совпадает; mapped from Block). |
proxies[] protocol fields |
Cipher, server, port, plugin-opts etc. | Ничего — только type/name/udp/history. |
proxies[].history |
Массив до 10 последних замеров. | Массив 0 или 1 элемента. |
GLOBAL в /proxies/{name} |
Работает (Clash.Meta подмешивает outbound). | 404 — GLOBAL только в /proxies aggregate. |
/proxies/:tag/delay с http:// |
Пропускает как есть (тестит HTTP 204). | Сбрасывает в пустую строку; использует default HTTPS URL. |
/group/:tag/delay результат |
Map<tag, delay> со всеми членами (failed → 0 или -1). | Только успешные. Failed полностью исключены из map. |
/group/:tag/delay url param для URLTest |
Используется. | Игнорируется — берётся URL из URLTest config. |
/logs, /traffic, /memory |
Streaming через SSE или WS. | Infinite NDJSON через raw HTTP, или WS с ?token=. |
/configs PATCH |
Может менять port, log-level, ipv6, allow-lan. | Только mode. Остальные поля в body игнорируются. |
/configs PUT |
Reload конфига с диска. | No-op 204. |
/providers/proxies |
Наполнен реальными providers'ами (если proxy-providers в config). |
Всегда пусто. Proxy providers не поддерживаются. |
/providers/rules |
Наполнен rule-providers. | Всегда пусто. Rule providers отдельно, sing-box их не exposes через этот endpoint. |
/script |
Starlark filtering (старый Premium). | Stub / not implemented. |
/profile/tracing |
WS-стрим трассировки matched rules. | Hardcoded 404. |
/cache/fakeip/flush |
Сбрасывает FakeIP pool. | То же, но 500 если FakeIP не активен. |
/connections DELETE |
Закрывает все + ничего больше. | Закрывает все + router.ResetNetwork() (clears DNS + TUN state). |
/version |
{"version":"...","premium":true} |
То же + meta: true (для совместимости с Yacd). |
PUT /proxies/:tag в URLTest |
Allowed в некоторых fork'ах. | 400 "Must be a Selector". |
Port fields в /configs |
Реальные. | Все захардкожены в 0. |
- WebSocket auth через
?token=— совпадает с Clash.Meta. - Bearer header — совпадает.
- CORS — enabled,
AllowedOrigins: ["*"]по умолчанию. - Host check — нет в clash-api (только в Debug API нашем).
- Secret клэша генерится рандомно при каждом rebuild config'а (см.
build_config.dart). Прямой доступ к base_uri бесполезен — secret меняется. Всегда через Debug proxy. mode_listвсегда["Rule"]— мы не конфигурим mode-switching (спеки 011 + 013 работают через другие механизмы).external_uiне задан → нет/ui, редиректов, upgrade'а.- TUN inbound tag =
tun-in→metadata.type = "tun/tun-in". - Source IP =
172.16.0.1(наш tun address).
Для копипаста: export TOK="<debug_token>".
# Sanity
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/ping
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/version
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/
# Proxies
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/proxies | jq '.proxies | keys | length'
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/proxies/direct-out
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/proxies/vpn-1 | jq '{now, "members": (.all|length)}'
curl -s -H "Authorization: Bearer $TOK" "http://127.0.0.1:9269/clash/proxies/%E2%9C%A8auto" | jq .now
# Ping
curl -s -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/proxies/direct-out/delay?url=https%3A%2F%2Fcp.cloudflare.com%2Fgenerate_204&timeout=5000"
curl -s -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/group/vpn-1/delay?url=https%3A%2F%2Fcp.cloudflare.com%2Fgenerate_204&timeout=3000" | jq 'to_entries | sort_by(.value) | .[0:5]'
# Selector switch (see tag via .now first, restore after!)
BEFORE=$(curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/proxies/vpn-1 | jq -r .now)
curl -s -X PUT -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d '{"name":"BL: 🇫🇷 France, Paris | [BL]"}' \
http://127.0.0.1:9269/clash/proxies/vpn-1
# ... revert ...
curl -s -X PUT -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-d "{\"name\":\"$BEFORE\"}" http://127.0.0.1:9269/clash/proxies/vpn-1
# Groups
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/group | jq '.proxies | map({type,name,now})'
curl -s -H "Authorization: Bearer $TOK" "http://127.0.0.1:9269/clash/group/%E2%9C%A8auto" | jq .now
# Rules
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/rules | jq '.rules[:5]'
# Connections
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/connections | \
jq '{total:(.connections|length), up:.uploadTotal, down:.downloadTotal, mem:.memory}'
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/connections | \
jq '.connections | map(.metadata.processPath) | unique'
# Close one (safe no-op with bogus UUID)
curl -s -X DELETE -H "Authorization: Bearer $TOK" \
http://127.0.0.1:9269/clash/connections/00000000-0000-0000-0000-000000000000
# DNS
curl -s -H "Authorization: Bearer $TOK" \
"http://127.0.0.1:9269/clash/dns/query?name=cloudflare.com&type=A" | jq
# Configs
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/configs
# Stubs (expect stub responses)
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/providers/proxies
curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/providers/rules
# Streaming — will timeout through proxy, for reference only
curl -sN --max-time 3 -H "Authorization: Bearer $TOK" http://127.0.0.1:9269/clash/traffic
curl -sN --max-time 3 -H "Authorization: Bearer $TOK" "http://127.0.0.1:9269/clash/logs?level=debug"python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "BL: 🇫🇷 France, Paris | [BL]"
# → BL%3A%20%F0%9F%87%AB%F0%9F%87%B7%20France%2C%20Paris%20%7C%20%5BBL%5D- Фикстуры для unit-тестов
test/services/clash_api_client_test.dartдолжны следовать реальной shape описанной тут, особенно:connections[].metadata.type = "tun/tun-in"(не просто"tun")rulePayload: ""всегдаdnsMode: "normal"всегдаsourcePort/destinationPort— stringhistory: []для большинства нод (не trivially populated)now: ""для свежезапущенного URLTest
- Если sing-box обновляется — перепроверь
tracker.go,proxies.go,api_meta_group.go. Main breaking-change vectors — форматtypeи исчезновение/добавление полей в metadata. - Streaming поддержка в debug proxy — задача на отдельный spec, если понадобится real-time dashboard.