Skip to content

Clash Controller

The CLI can expose a documented subset of the Clash API. The controller is disabled unless a listener is configured. It does not change existing groups’ round-robin behavior.

- clash_api_address: "0.0.0.0:9090"
secret: "replace-with-a-strong-secret"
cors_origins: ["https://dashboard.example.com"]
- clash_api_path: "/run/shoes/clash.sock"
secret: "replace-with-a-local-secret"
- cache_file_path: "./cache.db"

clash_api_address accepts one address or a list, using the ordinary server address syntax. Use either clash_api_address or clash_api_path per entry. Multiple entries share selectors, connections, counters, and resource budgets; they are not separate tenants. Each has its own secret and browser origins. Listener definitions and cache_file_path require a restart to change.

Every listener requires a nonempty secret. HTTP uses Authorization: Bearer <secret>; supported WebSocket upgrades also accept ?token=. Browser origins must exactly match cors_origins, unless cors_origins: ["*"] explicitly allows any origin, including opaque (null) origins. This applies to HTTP and WebSocket requests and does not disable authentication. Prefer exact origins when possible. With *, any website can attempt authenticated requests; use a long random secret. The default empty list permits authenticated clients without an Origin header but rejects requests carrying one. Only a standalone * acts as a wildcard; literal null entries are not accepted. Optional allow_private_network: true enables private-network preflight responses for allowed origins, not mixed-content bypass. Protect TCP with a firewall and external TLS termination. Unix sockets require an absolute path in an existing owner-only directory and are created with mode 0600. Browsers need an HTTP reverse proxy to use a Unix socket.

Opt into manual selection explicitly:

- client_group: outbound
type: selector
default: east
client_proxies:
- name: east
address: "east.example.com:1080"
protocol: {type: socks}
- name: west
address: "west.example.com:1080"
protocol: {type: socks}

Rules continue to use client_chain: outbound. A selector chooses an immediate named member or referenced group; inline selector members require name. The group discriminant is type: round-robin (the default), selector, or url-test. The removed strategy field and unknown types are errors. Nested round-robin weights, duplicate membership, and existing UDP filtering are preserved. A selected branch without UDP support fails new UDP flows instead of silently falling back. Selection changes affect new flows only; existing streams, UDP destinations, DNS connections, and WireGuard associations remain pinned until reopened. Optional client and server/TUN name fields provide stable dashboard identities. Generated anonymous names are stable only for unchanged definition sites.

Automatic selection does not require a controller listener or a cache file: see the URL-test example for a complete file.

- client_group: fastest
type: url-test
url: "https://probe.example.com/generate_204"
interval: 300 # seconds, 1..86400
timeout: 5000 # milliseconds, 1..60000, no longer than interval
tolerance: 50 # milliseconds, 0..65535
lazy: true
client_proxies:
- name: east
address: "east.example.com:1080"
protocol: {type: socks}
- name: west
address: "west.example.com:1080"
protocol: {type: socks}

Only url is required for url-test; the values above are the other defaults. Literal URL addresses denied by probe policy (including loopback, link-local, multicast, unspecified, and synthetic benchmark addresses) are configuration errors. Locally resolved hostnames are subject to the same destination policy at probe time. Literal URLs overlapping configured or retained fake-IP ranges are also rejected. default is selector-only. url, interval, timeout, tolerance, and lazy are url-test-only. Members must be nonempty and distinct; references to nested groups are supported. Explicit names are recommended for automatic inline members.

Selection uses the fastest fresh healthy result for the configured URL, among members eligible for that hop and transport. A healthy incumbent stays selected unless another member improves its delay by more than tolerance. TCP and UDP have separate incumbents; an automatic group can choose a UDP-capable alternative even when its fastest TCP member does not support UDP. Nested manual selectors still restrict eligibility, and intermediate chain hops only need TCP support. Changing selection affects new connections, not established streams or associations.

There is one initial check after activation, including for lazy groups. Thereafter lazy: true checks only groups used by new forwarding connections during the last interval. Probes, dashboard reads, and WireGuard carrier maintenance never count as use. New outbound connections through a WireGuard endpoint wake its carrier groups, including nested carriers, without changing their existing associations. Inbound-only endpoints and long-lived flows need lazy: false for continuous checks. lazy: false checks idle groups too. Samples older than 2 * interval + timeout are stale and cannot win on delay. Without fresh healthy results (startup, all dead, stale, or locally unavailable probes), retain the eligible incumbent, or choose the first eligible member in configuration order. This is best-effort routing, not fail-closed routing.

Unambiguous remote connection errors request a rate-limited recheck (at most once per second); they do not mark the configured URL dead based on a different user destination. Local overload and unsupported operations do not penalize health. HTTP/2 adapters preserve matching transport-error causes for this classification. This is best effort when a protocol replaces a failure with a generic cancellation: for example, XHTTP upload cancellation can lose the original cause and count as a remote failure. User payloads are never retried across candidates. Probes use HTTP HEAD and accept any HTTP response status, without redirects; HTTPS certificates are verified. Local probe lookups use guarded system DNS, not a server’s configured DNS group. API probes with the same normalized URL and wildcard/default expected status also update selection. Other URLs and explicit status expectations cannot overwrite it.

Periodic and API probes share a 16-probe limit. Each periodic round’s timeout includes admission/queue time; members not admitted before the deadline retain their previous health. A deadline expiry after queueing is inconclusive and also preserves prior health; successful probes and explicit remote errors still update it. Members with no full-budget attempt, then the oldest full-budget attempts, lead each round, independently of health. Local failures cannot monopolize these slots, and queued attempts can receive a full budget on a later round. Tied members rotate on admission. Due groups with the oldest unqueued admission get priority, with stable catalog order breaking ties. Under sustained saturation, reduce group size, increase timeout/interval, or expect unprobed/stale members. Scheduling has one-second granularity and does not overlap rounds within a group; other groups can run while a slow round is in flight. Activation/reload cancels old work and rejects obsolete results; an interrupted round is immediately eligible when its generation resumes. New committed generations start with unprobed health.

The controller reports type: URLTest, effective TCP now, optional udpNow, testUrl, and selected-member health (unprobed, healthy, dead, or stale). Its alive is true only for fresh healthy results. Manual selection PUTs for automatic groups are rejected; automatic choices are neither pinned nor persisted. Leaf histories remain bounded to ten samples per URL and eight URLs. Automatic selection retains its configured-URL samples and full-budget attempt timestamps independently of API eviction; other attempt URLs are capped at eight per member for each of filtered and unfiltered probes. Automatic checks test each immediate member as a standalone outbound, not every surrounding chain or destination. URL-test groups are not supported by FFI.

Only one cache_file_path is allowed across all input files. Its absence disables persistence; relative paths resolve against the process working directory. The file is Shoes-specific versioned JSON, not a sing-box/Mihomo database. Parent directories must exist, and Unix cache/lock files must be private. Explicit API choices take precedence over default, then the first member. Startup/reload never save defaults or rewrite YAML. Every accepted selection PUT saves before publishing the new choice. Failed saves leave routing unchanged. An error after atomic replacement can leave an unacknowledged choice on disk; logs report this durability uncertainty. Saved entries for removed groups are retained. Both reads and writes are limited to 1 MiB; remove obsolete entries only while Shoes is stopped.

Supported endpoints: /version, read-only /configs, /proxies, /group, selector PUTs, node/group /delay, read-only /rules, /connections snapshots and DELETE cancellation, plus WebSocket/NDJSON /traffic, /memory, and /logs. Provider collections return {"providers":{}}. Traffic counters measure outbound application payload, not headers, padding, or retransmissions. Probes, internal DNS/carrier traffic, and unauthenticated camouflage/fallback forwarding are excluded; synthetic fake-DNS answers and ICMP are not represented as connections. Connection speeds are derived by the dashboard from successive byte counters. Process/GeoIP data is not fabricated.

Optional connection_notes: true on a Clash API entry (clash_api_address or clash_api_path) adds setup notes to active /connections objects, over HTTP and WebSocket. It defaults to false; changing it requires a restart. If any controller enables it, all controllers expose notes. In zashboard, open a connection’s details and select Raw to inspect notes (elapsedUs, phase, text), alongside its existing generation, destination, selected chain, and rule metadata.

Notes describe preparation, routing choices, inbound-resolver calls, and outbound setup. They are best-effort: rejected/failed setup and closed connections have no retained history or separate debug endpoint. A dashboard may retain previously sampled connections, but short-lived connections can be missed. DNS notes cover existing routing/dial call boundaries, not resolver cache/upstream internals or remote DNS. Initial UDP prechecks and local-DNS handling are not observed. Notes contain hostnames and addresses, never payloads, credentials, or error messages; errors use only their kind. They may reveal outbound proxy server hostnames and IP addresses not otherwise exposed by the API.

At most 256 connections (including pending setup) collect notes at once. Each buffer holds 32 events with at most 128 UTF-8 bytes of text each, plus an overflow summary when read. New connections skip notes while all slots are occupied. Buffers reserve at most 8 KiB each, 2 MiB in aggregate, excluding allocator/RSS overhead and snapshot copies/JSON. Closing or abandoning a connection releases its buffer when its last reader finishes. The budget is shared across reloads. With notes disabled, no note buffers or resolver wrappers are allocated. When enabled, notes add setup work and snapshot copying/serialization: the controller samples every second even without subscribers; HTTP requests and custom-interval WebSocket subscriptions can request additional snapshots.

Delay tests send HTTP HEAD through actual outbounds, verify HTTPS certificates, and never follow redirects. A leaf test is standalone, not a test of every chain containing that leaf. Unused WireGuard nodes remain inactive and cannot be probed until referenced by a forwarding or DNS rule. Direct probe destinations cannot be loopback, link-local, unspecified, multicast, or synthetic fake-IP addresses. Remote proxies may resolve names differently: this is authenticated administrative network access, not an API for untrusted users.

The compatibility target is zashboard revision 6225aac84439425ae4f4493437b110a314753436, not full Mihomo parity. Only rule mode is offered, with no synthetic GLOBAL. Use disableUpgradeCore=1 in the dashboard backend settings. Unsupported controls can still be visible: config editing, API reload/restart, upgrades, provider updates, rule disabling, DNS/cache maintenance, and dashboard storage sync return explicit errors, never fake success. No UI download/hosting is included. Controller/cache entries are rejected by FFI.

See the complete example. Controller connections share a limit of 64, subscriptions 16, probes 16, and bound sockets 16. Request heads must finish within 15 seconds of accept or the preceding response; ordinary request bodies have a 10-second deadline and 64 KiB limit. Established subscriptions are exempt from header idle expiry and have bounded queues/write deadlines. Responses are limited to 32 MiB. An oversized connection snapshot closes its WebSocket with code 1009 and reason response exceeds 32 MiB; tracking continues without truncating the connection list. Configuring a controller, cache_file_path, or any selector/url-test group enables the catalog and its 16,384-node limit. Names must be nonempty, unique, at most 256 bytes, and contain no control characters. DIRECT, REJECT, and GLOBAL are reserved. Configuration preparation rejects /rules or base /proxies responses larger than 32 MiB. Group delay requests support at most 1,024 immediate members.