Controller security
The controller API (external-controller, external-controller-tls, external-controller-unix, external-controller-pipe) can change the config, the rules, the inbound users and restart or update the core. The secret is its only credential: every path except the health probes, the /ui files and the DoH path needs Authorization: Bearer <secret> (browser WebSockets use a one-time ticket; reusable ?token= secrets require an explicit compatibility option).
Secret on a network address
Section titled “Secret on a network address”Every TCP controller requires a credential, including loopback. Plain HTTP external-controller is restricted to loopback by default; a non-loopback bearer secret would travel in clear text. Refused listeners log an error, and PUT /configs answers 400 and applies nothing.
Without a secret these work:
external-controller-unixandexternal-controller-pipe, protected by the file mode and the pipe ACL;external-controller-tlswithtls.client-auth-type: require-and-verify(or withtls.client-auth-certset): the client certificate is the credential.
external-controller-allow-insecure: true (or -ext-ctl-allow-insecure, CLASH_OVERRIDE_EXTERNAL_CONTROLLER_ALLOW_INSECURE=true) is the explicit legacy opt-in for a secretless TCP controller or remote plaintext bearer HTTP. Use external-controller-tls for remote access.
Web pages and a controller without a secret
Section titled “Web pages and a controller without a secret”A TCP controller without a secret exists only in explicit insecure mode or with verified TLS client certificates. The browser guard remains defense in depth for the latter:
- answers only requests whose
Hostis an IP address orlocalhost; - refuses requests with an
Originheader (all cross-origin requests and websockets from a browser) unless the origin is the controller itself (the bundled/ui) or is listed literally inexternal-controller-cors.allow-origins;"*"does not count.
Programs (scripts, panels, curl) send no Origin. A hosted dashboard such as MetaCubeXD needs a secret or verified client certificate, plus its origin in allow-origins when applicable. Explicit insecure mode turns this guard off.
Reloading the config
Section titled “Reloading the config”PUT /configs, SIGHUP (systemctl reload citrus) and a restart apply the controller settings of the new config: secret, the addresses, tls, external-controller-cors, external-ui. When one of them changes, the controller listeners restart (the PUT /configs response is sent first) and the open websocket streams (/logs, /traffic, /memory, /connections, /events) are closed, so nothing authorized by a retired secret stays open. Command line overrides apply on every reload. Use -secret-file; literal -secret, -age-secret-key and -config are refused unless -allow-secret-arguments explicitly restores the process-list exposure. A reload that does not change the controller leaves its connections alone.
Core version
Section titled “Core version”GET /version answers as mihomo does, with one more field:
{"meta": true, "version": "v1.0.0-beta.3", "core": "citrus"}mihomo clients skip the extra field. An app that works with both cores can name the core by core, and take it for mihomo when the field is missing.
Limits
Section titled “Limits”Every controller listener cuts off a client that has not sent its request headers (or finished the TLS handshake) in 10 seconds, closes idle keep-alive connections after 2 minutes and accepts at most 256 KiB of headers. The streams are not limited in time.
GET /connections as a websocket takes interval in milliseconds from 1 to 3600000; any other value is answered with 400.
