CH-01
Proxy Group Types & Practice
Proxy groups are the middle layer between routing rules and actual nodes: once a rule matches, it hands off to a proxy group, and the group decides which node to actually use based on its own logic. Understanding how each type makes that decision is the first step in turning a config from "working" into "working well." mihomo supports five common types, and their decision logic differs significantly — picking the wrong type is a common cause of jittery latency and frequent disconnects.
Decision Logic for the Five Types
Three Key Parameters of url-test
url is the speed-test target — usually an address that returns an empty 204 response, so bandwidth overhead is negligible. interval is the test period in seconds; too short amplifies the test traffic on subscription nodes, too long delays switching away from a degraded node — around 300 is a reasonable balance for everyday use. tolerance is the switch margin in milliseconds: a new node only takes over if it beats the current one by more than this margin. Without a tolerance set, two nodes at 60ms and 65ms will bounce back and forth, and every switch risks breaking an in-progress connection — set at least 50.
proxy-groups:
- name: Auto-Speedtest
type: url-test
proxies: [HK-01, HK-02, JP-01]
url: https://www.gstatic.com/generate_204
interval: 300
tolerance: 50
- name: Failover
type: fallback
proxies: [Primary-HK, Backup-JP, Fallback-US]
url: https://www.gstatic.com/generate_204
interval: 180
- name: Load-Balance-DL
type: load-balance
strategy: consistent-hashing
proxies: [SG-01, SG-02, SG-03]
The Two load-balance Strategies
consistent-hashing hashes by destination address, so requests to the same site consistently land on the same node — balancing spread with session stability, and the recommended default. round-robin rotates nodes connection by connection for the most thorough spread, but different requests to the same site come from different exit IPs, which can trip up strict session validation on some services. Only use it for pure download groups.
A Practical Grouping Pattern
In practice, a two-tier layout works well: build region-based url-test groups at the bottom (e.g. "HK Auto," "JP Auto"), then put a top-level select group that bundles those regional groups alongside a "manual pick" option, and point your rules at that top-level select group. Day to day it runs on auto speed-testing; when you need a fixed region, flip a switch in the client panel to that region's group without touching any rules. Proxy groups can also nest other proxy groups (just write the group name in the proxies list), but avoid circular references — the core will refuse to load a config like that. For a deeper comparison of the three auto types and parameter experiments, see the blog's Proxy Group Type Comparison; if you're unsure which nodes to put in a group, start with Four Dimensions for Picking Nodes.
Tip
Node names from a subscription can change (e.g. "HK-01" renamed to "Hong Kong 01"), and any group with hard-coded node names in its proxies list will break as a result. Switch to include-all: true combined with filter: "(?i)hk|hong" to collect nodes by regex instead — the group keeps up automatically after the subscription updates, with no manual list maintenance.
CH-02
Rule-Provider Subscriptions
Dumping thousands of routing rules straight into the rules section is the main reason a config becomes unmaintainable: every subscription refresh wipes out manually added rules, and adjusting how one category of sites routes means hunting through a long list line by line. rule-providers splits rules by purpose into independent files pulled in as subscriptions, leaving only a dozen or so "skeleton" lines in the rules section — readability and maintainability both improve noticeably.
Field Meanings in rule-providers
type is either http (remote subscription, auto-updated on interval) or file (local file you maintain yourself). behavior declares the content shape of the rule file — domain is a pure domain list, ipcidr is a pure IP-range list, and classical allows mixed rule types, the most versatile but with slightly higher matching overhead. format supports yaml, text, and mihomo's binary mrs format — mrs is smaller and loads faster, so prefer it for large rule sets. path is the local cache path; interval is the auto-update period in seconds — rule sets don't change often, so 86400 (one day) is plenty.
rule-providers:
streaming:
type: http
behavior: classical
format: yaml
url: https://example.com/rules/streaming.yaml
path: ./rule-sets/streaming.yaml
interval: 86400
cn-ip:
type: http
behavior: ipcidr
format: mrs
url: https://example.com/rules/cn-ip.mrs
path: ./rule-sets/cn-ip.mrs
interval: 86400
rules:
- RULE-SET,streaming,Streaming
- RULE-SET,cn-ip,DIRECT
- GEOIP,CN,DIRECT
- MATCH,Auto-Speedtest
Rule Order and Fallback
The rules section matches top-down and stops at the first hit, so order is priority: personal exceptions go first, large rule sets sit in the middle, geographic rules like GEOIP,CN,DIRECT come later, and the last line must be a MATCH catch-all — without one, unmatched traffic falls back to the core's default behavior, which makes troubleshooting confusing. Another common pitfall is putting IP-based rules (GEOIP/IP-CIDR) ahead of domain rules: IP rules trigger a DNS lookup to get the target IP, which slows matching and can cause unwanted resolution behavior. If you genuinely need one and don't want to trigger resolution, add the no-resolve parameter to the rule.
GeoIP and GeoSite Databases
GEOIP,CN relies on the GeoIP database loaded by the core, and GEOSITE rules rely on the GeoSite database — both go stale over time. When the database is too old, IPs that are actually located in mainland China may get misidentified as overseas and incorrectly routed through the proxy. Most actively maintained clients offer an "update Geo database" option in settings — click it once every month or two. mihomo also supports declaring geo-auto-update: true with an update interval, letting the core pull updates on its own. Database files live in the client's working directory alongside the config file, and need a config reload to take effect after updating.
Note
The name a RULE-SET references must exactly match a key defined in rule-providers, case-sensitive. Referencing an undefined rule set will fail to load the entire config, and the client log will name the exact offender — check this first when you hit an error.
CH-03
DNS Configuration Tuning
Half of routing accuracy comes down to DNS: if domain resolution gets tampered with, GEOIP rules end up working off the wrong IP and routing falls apart from there. Resolving through an unsuitable server can also land you on a CDN node far from your actual route, showing up as "the node latency looks fine but the page is just slow." The dns section is the part of the config most worth spending time tuning.
enhanced-mode: redir-host vs. fake-ip
As the default choice for a proxy client, fake-ip offers a better overall experience and is nearly mandatory under TUN mode (see Chapter 4). fake-ip-filter lists domains that must get a real IP — LAN device discovery, NTP time sync, and connectivity checks on some game platforms behave oddly with a fake IP, hence the common entries below. After switching enhanced-mode, it's worth clearing the client's DNS cache once (most clients have a button for this in settings, or just restart the core) to avoid mixing old and new mappings.
dns:
enable: true
listen: 0.0.0.0:1053
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter:
- "*.lan"
- "+.local"
- time.windows.com
- "+.ntp.org"
nameserver:
- https://223.5.5.5/dns-query
- https://doh.pub/dns-query
fallback:
- https://1.1.1.1/dns-query
fallback-filter:
geoip: true
geoip-code: CN
nameserver, fallback, and Per-Domain Resolution
nameserver is the primary resolver group — point it at DoH servers reachable directly from within mainland China, which keeps mainland-domain lookups fast and lands on the nearest CDN. fallback is the verification group — point it at overseas DoH servers: the core compares results from both groups, and if the IP returned by nameserver falls outside the range declared in fallback-filter (e.g. geoip-code CN), it's flagged as possibly tampered with and the fallback result is used instead. This setup balances fast in-region resolution with clean overseas resolution. For finer control, nameserver-policy can assign a resolver per domain — e.g. sending all domains in a given rule set to an overseas DoH server. It's the most precise option but also the most work to configure, so use it as needed.
Choosing a Resolution Protocol
DNS server addresses support several protocol prefixes: plain UDP (223.5.5.5) is fastest but sent in cleartext and can be tampered with; DoT (tls://) and DoH (https://) encrypt the transport and resist tampering much better — it's recommended to write every server in DoH form. Note that the DoH server's own hostname also needs resolving, and the core uses default-nameserver (which should be a plain-IP UDP server) to bootstrap that step — forgetting it causes every lookup to fail at startup. If you suspect slow speeds are DNS-related, work through the local-settings layer of the three-layer checklist item by item; more resolution-related issues are cataloged under the troubleshooting section of the FAQ.
CH-04
TUN and Fake-IP
System proxy settings only affect apps that "respect proxy settings" — command-line tools, game clients, and UDP traffic from some messaging apps all bypass it. TUN mode creates a virtual network adapter on the system and pulls all layer-3 traffic into the core for processing. It's the most comprehensive takeover method, and the correct answer for handling UDP and non-standard-port traffic.
Core Parameters
tun:
enable: true
stack: mixed
auto-route: true
auto-detect-interface: true
dns-hijack:
- any:53
auto-route automatically writes the route table, pointing the default route at the virtual adapter — turning it off means configuring routes by hand, so it's generally left on. auto-detect-interface lets the core automatically identify the real physical exit interface, avoiding proxy traffic looping back on itself — a must on multi-interface devices (wired + wireless, virtual machine adapters). dns-hijack intercepts plaintext DNS requests to port 53 and hands them to the core's DNS handler; combined with fake-ip, this is what guarantees accurate domain routing under TUN — which is why TUN and Fake-IP almost always come as a pair: DNS queries sent by apps through the virtual adapter are caught by the core, immediately answered with a fake IP, and when the resulting connection hits that fake-IP range, the core looks up the original domain and routes precisely by domain rule, all without relying on the system resolver.
The Three stack Options
Platform-Specific Authorization
TUN needs system-level permission to create a network adapter, and the path differs by platform: on Windows, run the client as administrator, or install the system service option in client settings so the service holds the permission and daily use needs no elevation; on macOS, the first activation pops a System Extension / Network Extension authorization prompt — allow it under "System Settings → Privacy & Security" and toggle it on again; on Linux you need root or must grant the core binary the cap_net_admin capability; on Android it's not called TUN but VpnService — the first toggle pops a VPN connection authorization prompt, just accept it, though manufacturer battery-saving policies often kill the background service and cause disconnects, so add the client to the battery whitelist (see Android Usage Essentials for the exact path); iOS clients (such as the App Store version of Clash Plus) are built on Network Extension, and the system handles authorization uniformly. Find clients for every platform on the download page.
Note
Running TUN and the system proxy at the same time doesn't conflict, but when troubleshooting it's best to keep only one takeover method active — otherwise it's hard to tell which path traffic is actually taking. If the network misbehaves after turning TUN off, it's usually leftover routes; restarting the core or the system's network once resolves it.
CH-05
Domain Sniffing
Not every connection naturally carries a domain: when an app runs its own DNS (e.g. a browser's built-in DoH), all the core sees is a destination IP, domain rules all miss, and it falls back to GEOIP judgment; domains allowed through fake-ip-filter likewise show up as real IPs. Domain sniffing recovers the domain from the traffic itself — the Host header of an HTTP request, the SNI field of a TLS handshake, and QUIC's initial packet all carry a plaintext domain. The core reads it out, rewrites the connection's metadata with it, and runs domain rules against it again, pulling these "IP-only" connections back into precise routing.
Configuration Example
sniffer:
enable: true
sniff:
HTTP:
ports: [80, 8080-8880]
override-destination: true
TLS:
ports: [443, 8443]
QUIC:
ports: [443]
skip-domain:
- "+.push.apple.com"
force-domain:
- "+.v2ex.com"
sniff declares the port range to sniff per protocol — the three protocols here cover the vast majority of cases. override-destination decides whether to overwrite the connection's target with the sniffed domain; when on, both routing and logs show up as domains. skip-domain excludes domains that actually break when sniffed — some push services and clients with strict certificate checks fail to connect once the target is rewritten, so add them here when it happens. force-domain does the opposite, forcing an override for domains it matches.
When You Need It and When You Don't
If fake-ip is already enabled and every app resolves through the core's DNS, connections already carry a domain and sniffing adds limited benefit. It really pays off for browser traffic under TUN that uses its own built-in DoH, mobile apps that bypass the system resolver, and as a routing-accuracy fallback under redir-host mode. Sniffing parses the first packet of every new connection, which is cheap on CPU, so leaving it on is safe. But if some app starts disconnecting oddly after enabling sniffing, suspect the destination-override behavior first and exclude that domain with skip-domain rather than disabling sniffing entirely. When log entries (Chapter 7) show domains instead of raw IPs, that's the sign sniffing is working.
CH-06
Local Overrides and Multi-Subscription Merging
Editing the config file a subscription pushes down is the most common maintenance mistake — every subscription refresh wipes out all manual changes. The right approach is to keep "nodes from the subscription" separate from "rules, DNS, and proxy groups you maintain yourself": the subscription handles nodes, local overrides handle everything else, and updating the subscription only ever touches nodes while personal config stays untouched.
Override Mechanisms at the Client Layer
Actively maintained desktop clients generally ship with built-in override support: Clash Verge Rev offers "Global Extended Config" and script overrides, letting you merge YAML or rewrite any field with a JavaScript function before the subscription config lands; Clash Plus and FlClash each provide their own override/merge entry points. Taking Verge Rev's Merge override as an example, the prefix semantics are: prepend- inserts at the front of the original list, append- adds to the end, and writing the field name directly replaces the whole section.
prepend-rules:
- DOMAIN-SUFFIX,internal.example.com,DIRECT
append-rules:
- MATCH,Auto-Speedtest
dns:
enable: true
enhanced-mode: fake-ip
The example above inserts a LAN-direct rule ahead of every subscription rule (guaranteeing it matches first), appends a catch-all rule, and fully replaces the dns section. For rule-type overrides, prefer prepend, since rules match in order and only inserting at the front guarantees it takes effect.
Merging Multiple Subscriptions with proxy-providers at the Core Layer
If you hold multiple subscriptions at once, there's no need to keep switching configs in the client — use proxy-providers to pull multiple subscriptions in as node sources within a single config:
proxy-providers:
sub-a:
type: http
url: https://example.com/sub-a
path: ./providers/sub-a.yaml
interval: 43200
health-check:
enable: true
url: https://www.gstatic.com/generate_204
interval: 600
sub-b:
type: http
url: https://example.com/sub-b
path: ./providers/sub-b.yaml
interval: 43200
proxy-groups:
- name: All Nodes
type: select
use: [sub-a, sub-b]
- name: HK Auto
type: url-test
use: [sub-a, sub-b]
filter: "(?i)hk|hong"
url: https://www.gstatic.com/generate_204
interval: 300
Proxy groups reference a provider with use instead of listing nodes one by one, and paired with a filter regex, the HK nodes from both subscriptions automatically flow into the same speed-test group; each subscription updates on its own interval independently. health-check lets nodes within a provider keep-alive on their own, keeping dead nodes from lingering in a group. If two subscriptions produce nodes with the same name, the core will report a conflict — use a provider's override.additional-prefix to prefix node names and tell them apart.
Suggestion
Make override files part of your regular backup routine: when switching devices, reinstalling, or changing clients, nodes are trivially restored from the subscription link, but the local rules and DNS tuning are what can't be regenerated. If a client stops being maintained and you need to migrate, the override files are also the first asset to export — see the blog's EOL Migration Roadmap for the full path.
CH-07
External Controller
The mihomo core exposes a RESTful control interface, generally called the external controller, and the client's own panel is simply one consumer of it. Once opened up, it can also connect to an independent web panel, or be queried and driven straight from the command line — this is how you remotely manage a core running on a router, or write automation scripts against it.
Enabling the Interface
external-controller: 127.0.0.1:9090
secret: "your-secret"
external-ui: ./ui
external-ui-url: https://example.com/panel.zip
external-controller declares the listen address: for local use only, write 127.0.0.1:9090; to access it from another device on the LAN (e.g. to manage a router), change it to 0.0.0.0:9090, and in that case secret must be set to a strong value — the interface can read every connection record and switch nodes, so leaving it open hands control to anyone on the LAN. external-ui points to a static panel directory that the core hosts under the same port's /ui path; external-ui-url instead has the core auto-download and unpack a panel archive into that directory, skipping manual deployment.
Common Endpoints Quick Reference
Verify the interface from the command line and manually switch a node:
curl -H "Authorization: Bearer your-secret" \
http://127.0.0.1:9090/proxies
curl -X PUT \
-H "Authorization: Bearer your-secret" \
-H "Content-Type: application/json" \
-d '{"name":"HK Auto"}' \
http://127.0.0.1:9090/proxies/Manual Select
Diagnosing Routing with the Connections View
The most useful everyday value of this interface is figuring out "which rule did this site actually go through": open the panel's connections page (or call GET /connections), filter by domain, and every connection shows the rule it matched and the exit node it ended on. When routing doesn't match expectations, check here first to see whether a rule was written wrong, ordered wrong, or the DNS/sniffing layer never got a domain in the first place (an entry showing a raw IP is the tell — go back to Chapters 3 and 5). That's far more efficient than blindly tweaking config. The log level can be adjusted in the config with log-level: info; switch it to debug temporarily while troubleshooting, and remember to switch it back afterward so logs don't flood and hurt performance.
Further Reading
Examples across this page's chapters can be freely combined into a single override file. For anything still not covered, check the category index in the FAQ first; readers starting completely from zero should go back to Getting Started, work through the main path once, then return here to go deeper chapter by chapter.