Network Guide Featured Tags: external controller bind-address secret

Clash External Controller Fails?Check Bind-Address, Secret, and the Web Panel

The Clash external controller exposes a REST API for logs, traffic, and rule toggles, and most web panel or dashboard apps talk to that API—not to your proxy port. When the browser spins, returns ERR_CONNECTION_REFUSED, times out, or shows 401 Unauthorized, the root cause is usually a straight line: the running core is listening on a different address:port than the URL you typed, the bind-address or host part of external-controller is too narrow for how you are connecting, the secret in your config.yaml does not match what the panel sends, or a firewall blocks inbound TCP to the controller. This article walks the stack in a deliberate order so you can fix the problem without re-importing the same subscription for the third time.

Approx. 19 min read
Clash Editorial

1. What the external controller is (and is not)

Modern Clash-family cores, including the widely deployed Mihomo line, use the external-controller field in config.yaml to bind an HTTP (or in some builds, HTTPS) listener that implements a REST surface: runtime version, mode, proxies, rules, logs, and more. A separate web dashboard, whether yacd-style, metacubexd, or a built-in panel that ships with your GUI client, typically loads in the browser and then issues requests to that API endpoint. The mixed HTTP/SOCKS proxy port (often 7890) and the controller port (commonly 9090) are different services; pasting the proxy port into a dashboard’s API field is one of the fastest ways to guarantee failure.

Users also confuse “Clash is running” with “the controller is reachable from this network path.” The tray icon can be green while the REST API listens only on 127.0.0.1, which is perfect for a local browser on the same machine and useless for a phone on the same Wi-Fi trying to use http://192.168.1.10:9090 unless you intentionally widen the bind. Understanding that split is the conceptual frame for everything that follows, including why bind-address matters and why bind-address: '*' or a LAN-facing IP is sometimes required.

If you are new to the overall YAML layout, start with the configuration overview on this site so the keys below line up with how your client merges profiles, overrides, and provider fragments.

2. Read the effective running config

Your disk file is not always what the core actually loaded. Many GUI programs merge a subscription download, a user patch, a geodata path, and runtime toggles, then start the core with a generated profile. The only reliable truth is the effective configuration your client displays after merge—often a “view running config” or “open working YAML” action—or the content you fetch from a diagnostic endpoint if you already have a working controller (which you might not, yet). Before editing blindly, open that merged view and search for the lines that control the API: external-controller, secret, and any bind-address (or the host part embedded in external-controller, depending on the core version and schema your build uses).

Version drift between documentation and the binary matters. A tutorial written for a two-year-old schema might show only external-controller: ':9090' (listen on all interfaces) while your build expects an explicit 127.0.0.1:9090 plus a separate bind-address for other listeners. If something looks unfamiliar, check the release notes of your exact core rather than copy-pasting from a random gist. The goal is a single, coherent picture: which TCP port, which IP families, and whether a secret is set.

Tip: Screenshot or save the “running config” snippet for external-controller and secret before you test from another device. It removes the “did the GUI revert my edit?” variable when the first curl fails.

3. The external-controller line: host, port, and format

The external-controller value is usually a string in the form HOST:PORT, for example 127.0.0.1:9090 or 0.0.0.0:9090. The port is the numeric part after the colon. Common defaults in examples are 9090 or 9091; your package might choose another. When you type a URL in the browser, the scheme is typically http:// unless you have explicitly set up TLS for the controller, which is uncommon on loopback and still rare on LAN for single-user home labs.

Align your mental model: if the YAML says 127.0.0.1:9090, you must use that host from the same machine. If you try http://0.0.0.0:9090 in a browser, most environments treat 0.0.0.0 as invalid for outbound client connections, so you would instead open http://127.0.0.1:9090 or the machine’s actual 192.168.x.x address after you rebind. Likewise, a hostname such as localhost should map to the same place your controller listens; if you run IPv6-only listeners or custom hosts file entries, verify with ping and netstat rather than assuming.

When multiple profiles exist, confirm you edited the one your client actually activated. A stray profile left on disk can make you think you “set 9090 everywhere” while the process still serves 9091 from yesterday’s file. The fastest sanity check, once something listens, is a local curl -v against http://127.0.0.1:CHOSEN_PORT/... to see the HTTP response code before involving the web panel at all.

4. bind-address and who may connect

Some configurations separate the listen host from the port using a bind-address at the top level of the YAML tree. Others fold the address into external-controller only. The semantics you care about are: does the REST API accept connections only from 127.0.0.1 (loopback), or from all interfaces (often shown as 0.0.0.0 or a wildcard) so that another device on the LAN can open TCP to the same port?

If you need a phone or another PC to use a browser-based dashboard against a desktop running the core, the controller must be reachable on the desktop’s LAN IP. That is the same class of “Allow LAN / bind all interfaces” story we describe for mixed ports in the Windows 11 mixed port and firewall guide—the controller is just another inbound tcp service with its own port number. Toggling a GUI’s “allow remote/ LAN management” may rewrite both the proxy bind and the API bind; if only one of them flips, you can browse the local machine fine while the phone still times out, or the opposite, depending on which port you are testing.

Security note

Binding the external controller to 0.0.0.0 on a public or untrusted network without a secret (or with a weak secret transmitted in cleartext over http) is a serious risk: anyone who can reach the port can query or change your proxy state. Home Wi-Fi is not public Wi-Fi, but guest networks, campus dorms, and shared flats blur that line. Prefer tight binding, a strong secret, and—where applicable—firewall rules scoped to your subnet only.

5. The secret: empty, set, and HTTP 401

When secret is empty or missing in a profile that expects none, the API may accept unauthenticated GET requests. When you set secret: 'your-long-random-string', the core expects the same value in the Authorization header, commonly as a Bearer token, depending on the client. If your web panel still sends no header, or a stale value from a previous profile import, the server returns 401 Unauthorized even when TCP to the port works perfectly.

Practical triage: first confirm the secret in the running YAML matches the field in the dashboard’s “API / secret / token” box character for character, including no trailing spaces from copy-paste. If you rotate the secret on disk but forget to click “apply” in the GUI, the process might still be serving the old value from memory. Restarting the client or service after changes is boring and effective.

Advanced tools and scripts that call the REST API with curl often use -H 'Authorization: Bearer ...'. Your browser will not add that for you on a random tab unless the web UI is built to store it. That is one reason a bare GET to an endpoint in the address bar “works in curl but not in the panel” or the reverse, depending on which side carries the token. When users say “401 on the Clash external controller,” almost always the next step is: align secret and the client’s Authorization path, not replace your entire node list.

Warning: Storing a powerful API token in a bookmark or plain-text config on a shared machine is the same as sharing remote control of your proxy stack. Use device-local storage and a reasonable rotation policy.

6. Web panels, external-ui, and URL paths

Some distributions enable an embedded external-ui that serves static files for a dashboard on the same port as the controller under a path prefix such as /ui or a similar segment. Other setups expect you to open a yacd or metacubexd instance separately and only point its “API base” or “backend” field at http://127.0.0.1:9090 (or your chosen host). If the UI you opened is still talking to a default of 127.0.0.1:9090 while you moved the API to 127.0.0.1:9092, the page loads and every chart stays empty, which feels like a broken proxy when it is only a API URL mismatch.

Cross-origin behavior matters when you host a static panel on a file:// or different origin than the API. Browsers may block fetches unless the server side enables CORS for the dashboard origin, which well-maintained embedded UIs or official builds usually handle, while random one-off github.io frontends do not. If the console shows blocked-by-CORS while your curl is fine, you are in front-end land, not YAML land, but the fix is still: align a supported UI with the core version you run.

7. LAN, loopback, and firewalls (Windows, macOS, Linux)

On the same computer, http://127.0.0.1:PORT only requires that the service listen on loopback. On a second device, you must use the host’s 192.168.x.x (or 10.x / 172.16-31) and the controller must be bound to an address that answers on that interface. Windows Defender and third-party firewall products often block inbound to unfamiliar programs until you add an allow rule, exactly like the mixed-port case in the firewall walkthrough for Windows 11. macOS can prompt for “accept incoming connections” the first time the binary build changes, and a silent block looks like a timeout from the other host.

On Linux headless boxes, ufw or iptables/nftables can deny the port even when ss -lntp shows LISTEN. Remember that docker or lxc networking can move the bind you think you have on the host into a namespace where the LAN never reaches it. If you are combining this guide with a container or bridge network, re-check the published port map and the interface list with ip addr on the namespace that actually runs the core.

Remote access over the public internet to an 0.0.0.0 controller is almost never a good idea with plain http and a short secret—if you must, put an authenticated reverse proxy with TLS in front, restrict source IPs, and consider a VPN instead. This article assumes you are on a trusted home or lab LAN when it discusses widening bind beyond 127.0.0.1.

8. Port conflicts and fast verification

Another process may already use 9090, which forces your Clash core to log a bind error and fall back, exit, or pick an alternate port depending on the build. The symptoms look like “worked yesterday, dead today” after you installed a dev stack or a telemetry agent. On Windows, netstat -ano | findstr :9090 (adjust the port) shows the owning PID. On macOS and Linux, ss -lntp is the first tool to reach for.

Once the listener row exists and shows the right address family, test from the same host with curl -v to a simple endpoint. If you see HTTP/1.1 401 without a token, the wire is good—go back to secret. If you see immediate connection refused, no program is listening on that socket. If the connection hangs, suspect firewall or a wrong IP on the client side.

IPv6 and dual stack

Some users type http://[::1]:PORT while the server listens only on 0.0.0.0 in an IPv4 sense, or the opposite. If your OS prefers one family first, a typo in the browser can send you to a black hole. When in doubt, use explicit 127.0.0.1 for local IPv4 until the path is green.

9. GUI clients and why settings drift

Frontends such as Clash Verge, Clash for Windows descendants, and platform-specific launchers add their own settings layer: toggles for “external control,” “allow LAN,” or “secret for API,” sometimes persisted separately from the raw config.yaml you edit in a text editor. After an update, the GUI might regenerate a default 127.0.0.1:9090 if you had briefly cleared a field, or the opposite—wipe a custom 0.0.0.0:9090 you needed for the phone. Treat the GUI’s advanced screen and the running YAML as a pair that must match.

Automation users who start the core with systemd and never open a GUI have fewer moving parts, but they must also ensure the service file does not override the config path or working directory, leaving an old API port in effect. A quick systemctl status and journal peek after edits saves hours of staring at a correct-on-disk YAML that the daemon never reloaded because Reload did not apply to bind changes.

10. Symptom-to-action table

What you see Likely cause First fix
ERR_CONNECTION_REFUSED in the browser Nothing listening, or wrong host/port Check external-controller and netstat / ss
Spinner then timeout (LAN) Bound to loopback only, or firewall Widen bind or add allow rule; see mixed-port LAN guide
HTTP 401 on API or empty UI secret mismatch or missing Authorization Align token in panel and YAML
UI loads, charts show no data Dashboard still points at default API URL Set base URL to your real HOST:PORT

11. Summary

The Clash external controller is the control plane: the external-controller string and optional bind-address decide where the REST API listens, the secret decides who is allowed to call it, and your web panel must use the same port, host, and token as the running core. Connection refused and timeouts usually come from address scope or firewall issues; 401 almost always means secret drift. When you work that list in order, you avoid the trap of re-downloading a subscription to fix a problem that was never the node list in the first place.

Compared with all-in-one VPN apps that hide their management channel, a Clash-based stack with an explicit API and readable YAML is easier to debug once you name each layer. That transparency is a feature: you can curl the controller, read the answer, and know whether the next step lives in the browser, the firewall, or a single line in config.yaml.

When you are ready to install or update a client with a first-class dashboard experience, get builds from the official download page on this site so your GUI and core stay in lockstep.

Download Clash for free and experience the difference