Clash Custom Rule Syntax and Priority: DOMAIN-SUFFIX, IP-CIDR and MATCH Order
A rule-by-rule walkthrough of the rules section and its match order: from DOMAIN, DOMAIN-SUFFIX and IP-CIDR through to GEOIP and the MATCH fallback, covering how top-down short-circuit matching works and the ways people most often get it wrong.
The three-part rule format and top-down short-circuit matching
In Clash and mihomo, all rules live in the top-level rules: array of the config. Each rule is a YAML list item with a fixed three-part structure: rule type, match value, policy name. The first field decides how the match is performed, the second is the match target, and the third is the outbound used once the rule hits. The outbound can be DIRECT (direct connection), REJECT (block), or any proxy group name defined under proxy-groups. Some types also accept a fourth field; the most commonly used one today is no-resolve.
rules:
- DOMAIN,api.github.com,Proxy
- DOMAIN-SUFFIX,github.com,Proxy
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- GEOIP,CN,DIRECT
- MATCH,Proxy
Matching runs top-down and short-circuits: the kernel compares rules one by one from the first, and the moment one hits it stops — the rules below are never evaluated. In other words, the order of the rules is the priority, and the kernel will not move a more specific rule up on its own. Put DOMAIN-SUFFIX,github.com,Proxy before DOMAIN,api.github.com,DIRECT and the latter will never take effect.
MATCH carries no match value, so any traffic will hit it. Once it sits in the middle of the rule list, every rule after it becomes dead code that never runs. Check this one first after changing rules.
Policy names must match the names defined under proxy-groups exactly, letter case included; the only names you can use directly are DIRECT and REJECT — everything else is a custom group name.
Domain rules: the boundaries of DOMAIN, DOMAIN-SUFFIX and DOMAIN-KEYWORD
Domain rules come in three tiers, each wider than the last. Before writing one, decide whether you need to cover a single host or a whole site:
| Syntax | Matches | Non-matches |
|---|---|---|
DOMAIN,api.github.com |
api.github.com | github.com、cdn.api.github.com |
DOMAIN-SUFFIX,github.com |
github.com、api.github.com | raw.githubusercontent.com、github.com.cn |
DOMAIN-KEYWORD,github |
github.com、githubassets.com、mygithub.io | gitlab.com |
DOMAIN is an exact match: only an identical domain name qualifies. DOMAIN-SUFFIX matches on domain label boundaries — github.com itself and any hostname ending in .github.com count as a match, but github.com.cn does not, since it is neither exactly equal nor ends with .github.com. Many people read it as a plain string suffix and assume github.com.cn or notgithub.com would be caught by the rule; in fact neither is.
DOMAIN-KEYWORD is a pure substring match: github will match both githubusercontent.com and mygithub.io. It suits services with many domain variants and inconsistent suffixes, but the shorter the keyword, the greater the collateral damage — words like app, api and cloud are best avoided as keywords.
mihomo also supports DOMAIN-REGEX for complex patterns written as regular expressions. Every connection has to run the regex once, which gets expensive when there are many such rules, so it is usually reserved for a few cases where nothing else works.
IP rules: IP-CIDR, GEOIP and the resolution cost of no-resolve
IP-based rules include IP-CIDR, IP-CIDR6, SRC-IP-CIDR and GEOIP, and they match against IP addresses. Connections started by browsers and clients usually carry only a domain name, so when the kernel sees a domain request it performs a DNS lookup first, then compares the resulting IP.
That lookup has two side effects: one extra query per connection, and a result that depends on the dns section — which may not be what you expected when you wrote the rule.
no-resolve turns this behaviour off. With it in place, if the current request carries a domain name, the kernel will not resolve it just to match this rule; it treats the rule as a non-match and moves on.
rules:
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
- IP-CIDR6,fc00::/7,DIRECT,no-resolve
- GEOIP,CN,DIRECT
- MATCH,Proxy
Private ranges, loopback addresses and carrier-reserved blocks are almost always handled by IP rules, and adding no-resolve keeps them from triggering extra lookups. Rules such as GEOIP,CN,DIRECT need the real IP to determine the country, so they normally omit no-resolve and let resolution happen.
Under dns.enhanced-mode: fake-ip there is an extra caveat: the application receives an address faked by the kernel (by default inside 198.18.0.1/16). These addresses do not represent real location, so do not match them with IP rules. Putting domain rules ahead of IP rules saves a round of resolution.
If the GeoIP data file is missing or fails to load, no GEOIP rule will match and traffic falls all the way through to MATCH — which shows up as sites in mainland China going through the proxy. Confirm the data file is in place before you start debugging rules.
Ports, processes and source subnets: where fine-grained rules belong
DST-PORT and SRC-PORT match on port, PROCESS-NAME and PROCESS-PATH match the process that opened the connection, and SRC-IP-CIDR matches the local address the connection came from. These rules generally go after the domain rules and before MATCH.
rules:
- PROCESS-NAME,Telegram.exe,Proxy
- PROCESS-PATH,/usr/bin/curl,Proxy
- SRC-IP-CIDR,192.168.1.0/24,DIRECT,no-resolve
- DST-PORT,22,DIRECT
- MATCH,Proxy
Process rules first have to work out which process a connection belongs to, so a single match costs more than a plain domain comparison — placing them near the top of the list pays off. PROCESS-NAME works out of the box on Windows and macOS; on Linux process information is limited by permissions, so PROCESS-PATH is more reliable in most setups.
SRC-IP-CIDR is often used to send traffic from virtual machine adapters or Docker bridges straight to a direct connection, usually together with no-resolve. Port rules have a very wide reach: DST-PORT,22,DIRECT affects every connection whose destination port is 22, so make sure it will not cut off traffic further down that should have gone through the proxy.
RULE-SET and rule-providers: moving rules out of the main config
Once the rule count runs into the hundreds, keeping everything in the main config is hard to read and harder to update. rule-providers splits rules into separate files, which the rules section references with RULE-SET:
rule-providers:
ad-block:
type: file
behavior: domain
path: ./ruleset/ad-block.yaml
rules:
- RULE-SET,ad-block,REJECT
- MATCH,Proxy
behavior determines the file format: domain is a plain domain list, ipcidr is a list of subnets, and classical is full rule syntax. The name after RULE-SET must match a key under rule-providers exactly; a typo will make the config fail to load.
If a rule set needs to be refreshed from a remote source, change type to http and add the url and interval fields — interval: 86400 means it is fetched once every 24 hours. If the rule set file is corrupted or badly formatted, the whole RULE-SET stops working, so keep a solid MATCH fallback in place: at least the failure behaviour is then predictable.
Six common mistakes and how to trace them in the logs
Listed from most to least frequent — work through them in order when a rule is not taking effect:
MATCHplaced in the middle: every rule after it stops working, and it is the easiest thing to overlook while troubleshooting.- Treating
DOMAIN-SUFFIXas a string suffix:github.com.cnandnotgithub.comwill not match; to cover variants, switch toDOMAIN-KEYWORDorDOMAIN-REGEX. DOMAIN-KEYWORDused too broadly: a short keyword drags unrelated domains in with it and pushes the whole chain through the proxy.- Missing
no-resolveon IP rules: every domain connection costs an extra lookup, raising both latency and DNS load. - Policy name case mismatch: the group is called
Proxyunderproxy-groupsbut written asproxyin the rule, and config validation fails. - GeoIP data file missing: every
GEOIPrule falls through and traffic drops toMATCH.
Tracing: enable external-controller: 127.0.0.1:9090 in the config and use a dashboard to see which rule each connection actually hit; the kernel log prints entries such as match DomainSuffix(github.com) using Proxy, naming the rule that took effect. After editing rules, reload the config and reproduce the same connection to confirm.
A rule list is essentially a checklist that runs in order. Read it from top to bottom once it is written, confirm that no rule is pre-empted by a broader one above it, and put MATCH last — the rules section will then rarely cause trouble.