Threat model: what “open external-controller” actually means
Modern Clash derivatives ship a programmable control plane alongside the data plane that carries your browser tabs. The data plane listens on familiar proxy ports; the control plane is the external-controller listener that speaks HTTP and powers dashboards, hot reloads, and automation. When tutorials say “open the panel,” they mean “make a browser talk to that listener.” The listener does not care whether the browser tab is yours, your roommate’s laptop on the same subnet, or a script kiddie who port-scanned your public IP after you forwarded 9090 by mistake.
People underestimate the blast radius because the UI looks friendly. A dashboard is still a thin skin over administrative verbs: fetch running configuration, patch rule payloads, trigger reloads, inspect connections, and sometimes dump enough state to reconstruct provider URLs you assumed were hidden inside the GUI. None of that is “magic hacking”—it is the product working as designed with zero authentication boundary.
Your goal in 2026 is not paranoia; it is aligning exposure with intent. A home user who only opens Yacd on the same machine should not publish the controller on all interfaces. A homelab operator who manages headless mihomo from a phone may still avoid raw 0.0.0.0 by tunneling over SSH or VPN. A developer who runs Clash in Docker on a shared build server owes teammates explicit firewall rules, not a shrug and “we trust VLAN 3.”
secret is empty or still the sample string from a gist, anyone who reaches the port can issue the same requests your panel does. Treat that like leaving admin/admin on a router—except the attacker can also flip your traffic to an exit you never audited.
Listener addresses: 127.0.0.1 versus 0.0.0.0 (and IPv6 gotchas)
The external-controller field accepts a host:port string. The host half decides which local sockets Clash binds. 127.0.0.1:9090 means “only loopback IPv4 on this machine.” Other devices cannot complete a TCP handshake to that address—even if your firewall is permissive—because the OS stack never attached the listener to a routable interface.
0.0.0.0:9090 (or an empty host that defaults to all interfaces in some wrappers) means “every IPv4 address the machine owns,” including LAN IPs such as 192.168.1.42 and sometimes VPN tunnel interfaces, depending on bind order and OS behavior. That is the configuration people paste when they want a phone on the same Wi‑Fi to reach the panel without understanding they also invited every other guest on the coffee-shop SSID.
IPv6 adds parallel stories. A literal [::]:9090 style bind may accept connections from unexpected scopes if you enabled IPv6 on the LAN and your distro treats “dual stack” generously. If you do not actively need IPv6 for the controller, prefer explicit IPv4 loopback in documentation you hand to beginners. When you do need cross-host access, document the exact address family, not a vague “open the port.”
Finally, distinguish binding from publishing. Binding to a LAN IP without host firewall rules still exposes the API to anyone who can route to that IP. Binding to loopback plus an SSH local forward (-L 9090:127.0.0.1:9090) keeps the sensitive listener off the LAN while letting you administer remotely through an encrypted channel you already trust.
API secret: configuration, headers, and rotation hygiene
In Clash-family cores, secret defines a shared key for the management plane. Clients—including Yacd—attach it as an HTTP Authorization header using the Bearer scheme. Think of it as a static API token: simple, effective against casual abuse, but worthless if leaked into screenshots, screen recordings, or a checked-in YAML on GitHub.
Operational habits matter. Generate a long random string (dozens of bytes) from a password manager or openssl rand -base64 32; avoid poetry, birthdays, or the word “clash” with numbers. Rotate the secret when you rotate nodes or when anyone who saw your config leaves a shared flat. Rotation implies updating both the YAML (or GUI field) and every dashboard bookmark that cached the old token.
Some community dashboards also support entering the secret in a web form stored in localStorage. That is convenient until someone borrows your browser profile. Prefer short-lived access: close the tab, clear storage, and keep disk encryption on laptops you travel with.
If you automate against the API with shell scripts, resist echoing the secret into process lists. On shared machines, prefer files with 0600 permissions or OS keychains. The threat is not only remote attackers; it is shoulder surfing and shared CI logs—places where “but the port is localhost” does not help.
Verify with curl: negative and positive tests
After you set secret and restart the core, prove the behavior instead of trusting the GUI badge. A negative test should fail: curl without a header should receive 401 Unauthorized (or connection refusal if you also bound loopback-only from another host). A positive test should succeed with the header:
curl -fsS -H "Authorization: Bearer YOUR_SECRET_HERE" http://127.0.0.1:9090/version
Swap /version for other diagnostic endpoints your build documents; the point is to exercise authentication, not to enumerate every route in this article. When a negative test accidentally returns 200, you still have an open controller—maybe the secret is empty, maybe you hit a different process on that port, or maybe a wrapper disabled auth for “convenience.”
From a second device on the LAN, repeat the negative test against http://<your-LAN-IP>:9090/version. If the connection succeeds when you intended loopback-only, you learned something valuable before an adversary did. If it fails with “connection refused,” your bind address and firewall are at least consistent with a localhost-only policy.
Host firewall: belt-and-suspenders when you must bind broadly
Loopback binding is the first prize. When business requirements force a LAN-visible controller—maybe you cannot run SSH from a managed phone—pair interface binding with explicit allowlists in Windows Defender Firewall, pf on macOS, or nftables/iptables/ufw on Linux. The rule should permit only the administrator’s subnet or static IP, not “any TCP from the internet.”
On Windows, inbound rules are easy to create poorly: marking a rule “Public” when you only needed “Private” accidentally exposes laptops on airport Wi‑Fi. Rehearse the rule on a VM before touching production. On Linux headless boxes, default-deny inbound plus a single ACCEPT for your jump host beats a dozen “temporary” opens you forget to delete.
Remember that containers and WSL introduce extra NAT hairpins. Docker Desktop on Windows might publish ports on the host interface even when your YAML said loopback—because -p 9090:9090 means “publish,” not “loopback.” Our Docker host proxy guide walks adjacent networking pitfalls; combine that mental model with this article when docker ps shows surprises.
For sharing proxy ports (not the controller) to phones, see the complementary walkthrough on Windows LAN sharing, ports, and firewall rules—it is easy to harden the controller while accidentally leaving mixed ports wide open.
Dashboards: Yacd, metacubexd, and browser origin stories
Static dashboards are served out of the core or loaded from CDNs depending on packaging. Either way, the JavaScript ultimately calls your external-controller origin. If you open the dashboard from file:// or an HTTPS site while the API is HTTP-only on loopback, mixed-content rules may push people toward tunnels—good—but some users “fix” it by exposing HTTP on 0.0.0.0, which is the wrong cure.
Prefer serving the UI and API on the same trust boundary: localhost to localhost, or both behind a single reverse proxy with TLS and authentication if you operate remotely. When a guide says “disable CORS,” ask why the browser needed cross-origin access in the first place; often the answer is an architecture that should have stayed on loopback.
Bookmark hygiene helps. A saved Yacd URL that embeds the secret in a query parameter might show up in sync’d browser history, analytics, or referrer logs on misconfigured intermediaries. Use headers the UI stores privately, or re-enter secrets per session on shared machines.
Decouple “Allow LAN” from controller exposure
GUI toggles for Allow LAN target proxy listeners—the mixed port, SOCKS, maybe HTTP—not necessarily the management API. You can be “safe” on proxy sharing while dangerously wide open on the controller, or the reverse: loopback controller with LAN proxies for a game console. Inventory both.
When troubleshooting team members say “I turned off Allow LAN, why can they still hit my API?” the answer is often external-controller: 0.0.0.0 surviving a partial config merge. YAML structure and merge order matter: mixin files, provider overrides, and GUI “patches” can resurrect interfaces you thought you deleted.
Headless Linux, systemd, and Docker footguns
Servers running mihomo under systemd (see our headless Linux guide) often enable the controller for observability. That is fine if unit files drop privileges, configs live on ramdisk with correct permissions, and journald is not world-readable. Add socket activation or IP allowlists if the machine has multiple tenants.
Docker Compose snippets frequently contain ports: ["9090:9090"] for convenience. That publishes on 0.0.0.0 of the Docker host unless you pin host IP. Prefer 127.0.0.1:9090:9090 when only localhost consumers exist. For remote admins, expose the management port on an internal bridge VPN interface, not the public NIC.
Kubernetes sidecars and Nomad tasks repeat the same mistake at scale: broad Services pointing at Clash metrics or control ports. Namespace NetworkPolicies are your friend; “we rely on security by obscurity on port 9090” is not a policy.
What attackers actually try once they see port 9090
Automated scanners do not understand your politics; they fingerprint HTTP titles and move on. Targeted attackers chain small wins: call a version endpoint, pull a config snapshot if auth is missing, inject a rule that sends corporate traffic through an exit they control, exfiltrate a subscription URL to clone your node list, or simply crash the daemon with malformed payloads if a bug exists.
You cannot patch every unknown bug, but you can shrink who may attempt those payloads. Loopback bind removes entire classes of LAN attackers. Secret rotation limits how long a leaked token works. Firewall allowlists cap blast radius when you must expose management to a phone subnet.
Subscription URLs deserve explicit mention because they are credentials: rotating them after leakage is operationally painful, and providers may rate-limit you. Treat any API that can dump providers or downloaded configs as sensitive—even if it feels “internal.”
Incident response: quick containment checklist
If you realize the controller has been world-accessible for weeks, assume compromise of policy integrity until you disprove it. Rotate secrets, rotate subscription URLs at the provider, diff your YAML against a known-good backup, and inspect unusual policy-group targets. Reload from a clean file rather than incrementally “fixing” a live instance you no longer trust.
On Windows and macOS GUIs, toggling the core off stops exposure immediately; on servers, systemctl stop the unit, fix bind addresses, then restart with logging elevated briefly to confirm only expected clients connect. Document the timeline—future you will thank present you when reconciling odd routing decisions.
Configuration matrix (mental model)
| Pattern | LAN exposure | Typical use |
|---|---|---|
127.0.0.1 + strong secret |
None (same host only) | Personal laptop with local Yacd |
127.0.0.1 + SSH tunnel |
Tunnel endpoint only | Remote admin without raw exposure |
| LAN IP + host firewall allowlist | Controlled subset | Trusted phone admin on home SSID |
0.0.0.0 + blank secret |
Broad / often catastrophic | Mistake—fix immediately |
Use the table as a communication tool: when someone says “it only needs to work,” ask which row they are choosing and what compensating controls exist.
Frequently asked questions
Should external-controller listen on 0.0.0.0 or 127.0.0.1? Default personal setups to loopback. Expand deliberately with tunnels or firewalls, not because a README skipped security paragraphs.
How does secret interact with dashboards? Panels send Bearer tokens matching secret. Empty secret means unauthenticated management for anyone who can reach the port.
Is Allow LAN the same knob? No. Audit proxy listeners and the controller separately.
Can attackers steal subscription URLs? If they obtain API responses that include provider configuration, yes—treat that like credential leakage.
How do I verify bind mode? Use OS socket tools and curl from another host; refused connections to LAN IP indicate loopback-only success.
Closing thoughts
Clash power users live inside dashboards. That convenience should not trade away the guarantees of your rules and subscription URL privacy. Binding external-controller to 127.0.0.1, pairing it with a real API secret, and proving the posture with curl plus host firewalls closes the gap between “I can open Yacd” and “only I can administer this core.” When remote access is real, tunnel or reverse-proxy it—do not spray management HTTP across the LAN because a tutorial forgot to mention the stakes.
Compared with chasing mysterious slowdowns, spending ten minutes on listener inventory pays disproportionate dividends: fewer midnight panics about mystery policy flips, fewer awkward questions after a roommate clicked an old bookmark, and a cleaner story when someone asks why your laptop listens on 0.0.0.0 at all. Pair this discipline with current mihomo builds from our download page so parser and API semantics match the YAML you actually run.
For YAML structure and merge ordering, continue with the policy group tutorial; for LAN proxy sharing without opening the controller, read the Windows LAN firewall guide; browse the full tech column for DNS, TUN, and developer-proxy articles.