Migrating from mihomo
Citrus uses the same configuration format as mihomo, and most configs run unchanged. Below is what may still need edits, and the behaviour that has become stricter. Check a config before restarting:
citrus -t -f config.yamlFor projects that embed the core as a library: the Go module path is still github.com/metacubex/mihomo, so pull Citrus in with a replace. Citrus itself replaces github.com/metacubex/utls, github.com/metacubex/tls and github.com/metacubex/http with its forks; copy those lines into the embedding project’s go.mod too. The module needs Go 1.26 or newer.
Removed protocols
Section titled “Removed protocols”VMess (with mKCP, mekya and TLSMirror), ShadowsocksR, Hysteria v1, TUIC v4, Shadowsocks stream ciphers and v2ray-plugin are gone. Replacements are in the compatibility table.
- Main config: such a proxy or listener is an error with a clear message, for example “VMess was removed from Citrus, use VLESS” or “TUIC v4 is no longer supported, use uuid/password (v5)”. The config does not load.
- Proxy providers: a node with a removed protocol is skipped with a warning, and the rest of the subscription loads.
- Links: the converter skips
vmess://,ssr://,hysteria://and TUIC v4 links. - Options:
network: mkcp,kcp,mekyaandtlsmirror-optsare now errors; VLESS and Trojan used to fall back to TCP silently. TheIN-TYPE,VMESSrule no longer parses, and there is no top-levelvmess-config. - The
relaygroup was already removed in mihomo; its error now points at atype: chaingroup, see Chains.
Controller
Section titled “Controller”Details are on the Controller security page.
- A network address needs a
secret.external-controllerandexternal-controller-tlson a non-loopback address (0.0.0.0:9090,:9090, a LAN address) without asecretdo not start: there is an error in the log, and the rest of the core runs. Without a secret you can use loopback,external-controller-unixandexternal-controller-pipe, and TLS with verified client certificates. To turn the check off:external-controller-allow-insecure: true(or-ext-ctl-allow-insecure,CLASH_OVERRIDE_EXTERNAL_CONTROLLER_ALLOW_INSECURE=true). This affects Docker setups that publish the controller port. - Browser guard without a secret. A tcp/tls controller without a secret answers only a
Hostthat is an IP orlocalhost, and refuses requests with a foreignOrigin. A dashboard on an external site needs asecretor its origin inexternal-controller-cors.allow-origins("*"does not count). Scripts and panels that send noOriginare not affected. PUT /configsapplies the controller settings. A newsecret, addresses, TLS, CORS andexternal-uitake effect at once: the old token gets 401 and open websockets are closed. A config whose controller would be refused gets 400, and nothing is applied.- The unix socket and the named pipe are owner-only (socket 0600; pipe: the core’s account, SYSTEM and Administrators). If a GUI runs as another user, set
CITRUS_UNIX_SOCKET_MODE=0660with a shared group, or your ownLISTEN_NAMEDPIPE_SDDL. When asecretis set, it is required on the socket and the pipe as well. - Controller connection limits: 10 seconds for the headers (and TLS), 2 minutes of keep-alive idle, 256 KiB of headers.
GET /configshides secrets of TUIC and Shadowsocks (******). Read them from the config file./debug(pprof) needs the secret like the rest of the API.
Config reload
Section titled “Config reload”- The access policy applies without
force=truetoo.PUT /configsandsystemctl reload citrusapplyallow-lan,bind-address,skip-auth-prefixes,lan-allowed-ips,lan-disallowed-ips, and the built-inss-configandtuic-serveron every reload.forcenow only decides whether the built-in ports (port,socks-port,mixed-portand the rest) are reopened on the values from the file. - Credential changes revoke sessions. When a reload removes a user from
authenticationor changes its password, that user’s open HTTP, SOCKS and mixed sessions are closed. Turningauthenticationon closes the sessions let in without it. - Empty user names are refused:
authentication: [":pass"]anduserswith an emptyusernameon socks, http and mixed are config errors.
Files and paths
Section titled “Files and paths”external-uicannot point at the config root. The home directory, the config file’s directory, aSAFE_PATHSroot or a parent of any of them is refused, even withSKIP_SAFE_PATH_CHECK: a UI update empties that folder. Use a subfolder such as./ui.- Symlinks. A path that leaves the home directory and
SAFE_PATHSthrough a symlink or junction is refused. For exampleui -> /usr/share/metacubexd: add the link target toSAFE_PATHS, or setSKIP_SAFE_PATH_CHECK=true. A provider file that was a symlink is replaced with a regular file on the next update. - Modes of new files: config directory 0700,
config.yaml0600, provider files 0600,cache.db0600. Existing files keep their modes.
Providers and downloads
Section titled “Providers and downloads”- Redirects into private networks are refused. A public provider, rule-set, geodata or dashboard URL cannot redirect to loopback, private, link-local, CGNAT and similar addresses. A URL that is private to begin with is not restricted.
size-limit: 0means 64 MiB, not unlimited;-1removes the limit. Going over is an update error and the previous copy stays (the file used to be truncated).- Size bounds: a provider takes at most 50,000
proxiesentries and builds at most 10,000 proxies;override-exprproduces at most 65,536 results; logic rules nest at most 32 levels deep. Ordinary subscriptions are well within these.
Inbounds
Section titled “Inbounds”-
TLS 1.2 minimum on every TLS inbound and on the controller’s TLS.
-
The HTTP proxy authenticates every request. On a keep-alive connection a request without credentials gets 407, one with wrong credentials gets 403 and the connection is closed. With authentication off,
Proxy-Authorizationno longer names the user forIN-USER. Request headers are capped at 64 KiB (431). -
SOCKS5 UDP is bound to the user and a port. With authentication on, datagrams are accepted only from a client that holds an open UDP ASSOCIATE, and only from that association’s one UDP port. If the client’s port changes (NAT rebinding), it has to associate again. SOCKS5 UDP sessions now count against the user’s limits and stats.
-
The Snell client ID no longer names the user. It is chosen by any client that knows the shared PSK, so it is ignored:
IN-USERrules, per-user limits and per-user stats see a Snell user only when ShadowTLS v3 or JLS verified one. A plain Snell inbound has no user. -
A 15-second handshake deadline before authentication: VLESS, Trojan, AnyTLS, Shadowsocks, Snell, HTTP, SOCKS, mixed and the HTTP transports. A client that has not sent its header by then is dropped; there is no deadline after authentication.
-
Hysteria2 Realm behind a reverse proxy believes
trusted-proxy-headeronly from addresses intrusted-proxies. An empty list means never: list the proxy, for exampletrusted-proxies: [loopback]. VLESS and Trojan have worked this way sincetrusted-proxy-headerarrived. -
The Hysteria2 HTTPS masquerade verifies the upstream certificate. For a self-signed local site set
masquerade-skip-cert-verify: true, otherwise the answer is 502. -
REALITY:
max-time-differenceis in milliseconds, like Xray’smaxTimeDiff(it used to be microseconds). -
The same address on different inbounds or as different users gets separate UDP sessions, each with its own rules and limits.
-
The users of the server inbounds cannot reach the server’s network through
DIRECT. This covers VLESS, Trojan, Shadowsocks, Snell, TUIC, Hysteria2, AnyTLS, Mieru, Sudoku, TrustTunnel and ShadowQUIC. They cannot reach loopback, private, link-local, CGNAT and ULA addresses, the cloud metadata services or this host’s addresses, whatever the rules say. If they must reach something there, list it inegress-guard.allow-ips, or setegress-guard: falseon the listener. Client inbounds are not affected. See the egress guard. -
TUIC and TrustTunnel (HTTP/3): server
zero-rttis disabled by default.zero-rtt: trueexplicitly enables replayable QUIC early data. ShadowQUIC already uses the same opt-in default. -
KCPTun: omitting
keypreserves the known shared legacy key for compatibility and now emits a warning. Set a unique random key;max-sessionsandmax-streamsbound server sessions and streams per session (256/256 by default). -
Sudoku fallback: at most 256 relays per listener and 64 per source (an IPv4 address or IPv6 /64), with a 60-second idle limit shared by both directions; activity extends the connection. The Sudoku handshake format and existing keys remain compatible.
-
Controller WebSockets: the main
secretin?token=is rejected by default. A dashboard must obtain a one-time ticket fromPOST /auth/ws-ticketwith a bearer header, then send?ticket=on connection. For an older dashboard, temporarily setexternal-controller-allow-legacy-websocket-token: true; this restores the main-secret-in-URL exposure. API details.
Outbounds
Section titled “Outbounds”- REALITY sends X25519MLKEM768 by default. For old servers set
reality-opts.support-x25519mlkem768: false. - XHTTP: without
reuse-settings, connections are shared as in Xray (except over HTTP/1.1);h-max-request-times: "1"opts out. With data in headers or cookies and nouplink-data-key, the client sendsX-Data-Nandx_data_N, as Xray does; if you mix versions, setuplink-data-keyexplicitly on both sides. - SSH without
host-keystill connects, but logs a warning and the server’s key in authorized_keys format, ready to pin.skip-host-key-check: trueaccepts any key explicitly and silences the warning. - OpenVPN without a server identity check logs a warning. The new
verify-x509-name,verify-x509-typeandverify-server-nameturn the check on. http(s)://andsocks5://links no longer turn onskip-cert-verifyby themselves; only when the link hasskip-cert-verify,allowInsecureorinsecureset to 1 or true.tcp-concurrentdials at most 8 addresses per connection and 4 at once, and cancels the losing attempts.- DIRECT connects to the address the IP rules checked instead of resolving the name again.
Core updates
Section titled “Core updates”/upgrade checks the package against checksums.txt from the same release. A release without that file, or without a line for the package, will not install; Citrus builds always publish it. version.txt is validated, file names inside the archive are ignored, and a zip must hold exactly one file.
Platforms and builds
Section titled “Platforms and builds”Citrus needs Go 1.26 or newer to build. mihomo also publishes builds on old Go versions (files ending in -go120, -go122, -go123, -go124, -go125); Citrus does not.
- Windows 7/8 and older macOS: Windows and macOS releases are built with the patched MetaCubeX Go toolchain, which keeps these systems working. Citrus does not test them separately.
- Linux kernels older than 3.2 are not supported: Go 1.24 and newer require kernel 3.2.
