Trace an actual connection: did it enter Mihomo, which domain/IP was available, which rule matched, and which proxy carried it? Files, runtime configuration and existing connections may reflect different moments.
# Inspect ExecStart, working directory, and overridessystemctl cat mihomo.service
Identify the binary and -d/-f paths. For containers/OpenWrt plugins use their manager, not this systemd example against another configuration.
# Check the main process and recent startup resultsystemctl status mihomo.service --no-pager
Use journalctl for fuller logs; file existence does not prove this process uses it.
# Check the deployed core versionmihomo -v
Use the same binary as ExecStart; the PATH command may be another version.
# Validate the candidate configurationmihomo -t -f /opt/mihomo/mihomo_config/config.yaml
Adjust paths/workdir to the deployment, especially relative provider paths. Fix errors first; runtime APIs, subscriptions, and application access still need checking.
The path and 127.0.0.1:9999 are retained deployment examples, not defaults. Inspect ExecStart -d/-f, external-controller and secret. Do not expose an unauthenticated controller publicly. Validation does not establish live provider, DNS or upstream availability.
Load client authentication before querying
Run on a client that can reach the controller, here localhost:9999. Load MIHOMO_SECRET through your controlled secret mechanism or an interactive terminal read. Each request rejects an empty value.
# Read the controller secret without echoing itread-r-s MIHOMO_SECRET
Press Enter after input. This is an alternative to a secret manager, not an additional mandatory step. Unset the variable after the session.
# Verify controller identity and API accesscurl -fsS -H"Authorization: Bearer ${MIHOMO_SECRET:?Load the controller secret in this shell}" http://127.0.0.1:9999/version
For 401 inspect the secret; for refusal inspect the listener. Add -i when response headers/status are needed.
# Read runtime mode and inbound configurationcurl -fsS -H"Authorization: Bearer ${MIHOMO_SECRET:?Load the controller secret in this shell}" http://127.0.0.1:9999/configs|jq'{mode,port,"mixed-port": .["mixed-port"],tun}'
Requires jq. Compare mode, mixed-port, and tun; if files differ from runtime, verify controller identity first.
Back up the source and confirm reload paths are allowed by the process working directory/SAFE_PATHS. Runtime PATCH is not persistence to the source file; recheck after restart. A force query parameter is not permission to ignore invalid configuration.
Separate file reload from temporary PATCH
Preserve the old file and validate the new one first. The path below is on the Mihomo host, not the API client, and must satisfy workdir/SAFE_PATHS rules.
# Reload the validated file in the running instancecurl -fsS -i -X PUT "http://127.0.0.1:9999/configs?force=true"\
-H"Authorization: Bearer ${MIHOMO_SECRET:?Load the controller secret in this shell}"\
-H'Content-Type: application/json'\
--data'{"path":"/opt/mihomo/mihomo_config/config.yaml"}'
Check HTTP status, then GET configs/rules and test new connections. On failure restore the old file and validate/reload through the same flow; force does not bypass errors.
# Change runtime mode only for an intended temporary adjustmentcurl -fsS -i -X PATCH http://127.0.0.1:9999/configs \
-H"Authorization: Bearer ${MIHOMO_SECRET:?Load the controller secret in this shell}"\
-H'Content-Type: application/json'\
--data'{"mode":"rule"}'
This is an alternative task, not mandatory after reload. Record old mode, GET to verify, persist through managed source configuration if needed, and PATCH the recorded value to undo.
A 204 completes the request; inspect runtime config, rules and new connections. Reload can reconfigure listeners/tunnels and is not a guarantee of uninterrupted existing sessions.
The first matching rule selects the action
Preparing the visual
Does an exception precede the broader rule?
Rules use the first match. api.example.com matches both exact and suffix rules; putting the DIRECT suffix first shadows the REJECT exception.
# Read rules and their runtime ordercurl -fsS -H"Authorization: Bearer ${MIHOMO_SECRET:?Load the controller secret in this shell}" http://127.0.0.1:9999/rules|jq'.rules'
Compare type, payload, and target; first match wins. If this differs from the edited file, inspect reload status.
# Find the request and inspect its actual selected chaincurl -fsS -H"Authorization: Bearer ${MIHOMO_SECRET:?Load the controller secret in this shell}" http://127.0.0.1:9999/connections|jq'.connections[] | {id,source: .metadata.sourceIP,host: .metadata.host,destination: .metadata.destinationIP,port: .metadata.destinationPort,rule,rulePayload,chains}'
Correlate source, destination, port, and time. Use id for precise actions later. Short-lived connections may disappear; reproduce with logs or a sustained request.
# Read policy-group selection and available nodescurl -fsS -H"Authorization: Bearer ${MIHOMO_SECRET:?Load the controller secret in this shell}" http://127.0.0.1:9999/proxies|jq'.proxies'
Obtain group PROXY and node NODE from the response. URL-encode path names with spaces/non-ASCII and escape JSON values correctly.
Change only manually selectable groups
# Select a verified member of a Selector groupcurl -fsS -i -X PUT http://127.0.0.1:9999/proxies/PROXY \
-H"Authorization: Bearer ${MIHOMO_SECRET:?Load the controller secret in this shell}"\
-H'Content-Type: application/json'\
--data'{"name":"NODE"}'
Record old now, GET to verify, and inspect a new connection’s chains. Do not treat automatic groups as Selectors; persistence depends on configuration such as profile.store-selected.
# Close only the just-verified connectioncurl -fsS -i -X DELETE -H"Authorization: Bearer ${MIHOMO_SECRET:?Load the controller secret in this shell}" http://127.0.0.1:9999/connections/CONNECTION_ID
Use its ID from connections. This interrupts that request. Verify removal and let the application reconnect; omitting the ID targets all connections.
Distinguish selecting a group from that group selecting a node; inspect chains and now. Domain rules need domain metadata, which can come from DNS mapping or sniffing; redir-host is not sniffing-only. A fake IP identifies a mapping, not the remote server’s public address.
Test new connections because old sessions can retain earlier choices. If disconnection is necessary, select a specific ID. DELETE /connections disconnects everything and is not a routine refresh button.
Test explicit proxy and transparent ingress separately
Test the explicit proxy first
# Make an explicit proxy request on the proxy hostcurl -v --connect-timeout 5 --proxy http://127.0.0.1:7890 https://example.com/
Replace 7890 with the runtime port and correlate the connection rule/chains. A remote client’s localhost is not the proxy host.
# Inspect policy routing for transparent trafficip rule show
Run on the actual gateway and inspect sources/marks; query table contents separately.
# Inspect proxy, DNS, and controller listenersss -lntup
Match ports to configuration; loopback listeners serve only the local host directly.
# Read transparent-interception rules and ownershipsudo nft -a list ruleset
Correlate ingress interfaces, ports, marks, and routing tables. Edit plugin-managed rules through that plugin; do not flush the whole ruleset.
Observation
Next check
Explicit proxy works, LAN fails
Inspect client gateway/DNS and interception/routing
Request absent from connections
Confirm listener/process/ingress; short connections may have ended, correlate logs
Correct rule, failed destination
Check node, protocol, DNS, TLS and service; a delay probe is not universal health
Inspect actual Nikki/TUN/TProxy rules and ports, not a fixed 7891 assumption. Custom fail-open exists only if implemented and direct fallback may violate routing intent. After recovery verify DNS, TCP, UDP and LAN exceptions.