This guide is for users who want to customize traffic routing. It explains domain, IP, and dataset match syntax, then uses routing examples to show rule order, how conditions combine, and when DNS resolution occurs. When troubleshooting, check the config generated for the active core; don't assume UI labels like “direct” or “proxy” are the same as outboundTag values.
1. Routing Match Targets and Evaluation Order
V2Ray's routing selects an outbound based on details such as the connection's destination address and port, network type, and inbound tag. It is not a web content filter and does not route by URL path. Two pages on the same domain generally won't match different domain rules just because their paths differ.
routing.rules are evaluated from top to bottom in array order. Once a rule matches, its specified outbound handles the connection; later rules aren't checked to see whether they're “more specific.” Put exact-domain exceptions before suffix rules, and specific rules before catch-all rules.
- Between rules: The first matching rule wins. Rules aren't automatically sorted by domain length, prefix type, or network size.
- Within one field: For example, any matching entry in a
domainarray is enough. - Across different condition fields: If you specify both
domainandport, both must match. Puttingdomainandipin the same rule does not mean “domain OR IP.” - If no rules match: In a standard config without other routing mechanisms, V2Ray uses the first outbound in the
outboundsarray. That may not be a direct connection.
2. Syntax for domain, full, regexp, and geosite
domain is a rule field; domain: is a match prefix used inside that field. Don't confuse the two. The examples below all use the reserved example domain example.com to illustrate syntax, not to recommend routing for a real service.
| Entry syntax | What it matches | Notes |
|---|---|---|
full:example.com | Matches this exact domain only | Doesn't also match www.example.com |
domain:example.com | Matches the domain and its subdomains | Matches api.example.com, but not notexample.com |
example.com | Without a prefix, matches a substring | May match other names containing this string |
regexp:^api[0-9]+\.example\.com$ | Matches using a regular expression | Escape backslashes again when writing JSON |
geosite:cn | References the cn category in the domain data file | Category contents depend on the data file actually loaded |
To match a site and its subdomains, use domain:example.com rather than relying on a bare string. Use full: when you want to match only one hostname. Domain entries should not include https://, a port, a path, or query parameters; those aren't valid input for this field.
{
"type": "field",
"domain": [
"full:status.example.com",
"domain:example.net",
"regexp:^api[0-9]+\\.example\\.com$"
],
"outboundTag": "proxy"
}
The three domain entries in this JSON are OR conditions. After JSON decoding, \\. becomes the \. required by the regular expression. A single backslash may cause the config to fail during JSON parsing. Use the Go regular expression syntax supported by the core; backreferences and lookarounds aren't supported.
Rule of thumb: use full or domain for a single site
Use regexp only when the domain structure genuinely calls for a pattern. After editing, test the base domain, a subdomain, and a similar but unrelated domain to make sure a substring match isn't giving a false impression that the rule works.
3. Combining ip, geoip, and destination ports
The ip field accepts individual IP addresses, CIDR ranges, and geoip: dataset references. For IPv4, /32 denotes a single address; 192.0.2.0/24 is a documentation-only example range. Don't put CIDR ranges in the domain array or add https:// to entries in the ip array.
{
"type": "field",
"ip": [
"192.0.2.0/24",
"2001:db8::/32"
],
"port": "443",
"network": "tcp",
"outboundTag": "direct"
}
This rule matches only connections whose destination falls within either of the two example ranges, whose destination port is 443, and whose network is TCP. It won't match TCP port 80 on the same address or UDP port 443. port means the destination port, not the client's local listening port. To distinguish local inbounds by tag, use inboundTag.
geoip:privateis commonly used to route private and local address ranges. Its exact coverage depends on the data file being loaded.geoip:cnreferences an IP category;geosite:cnreferences a domain category. They aren't interchangeable.- Domain and IP categories don't always produce the same result. A domain may resolve to CDN addresses in different regions.
- An IPv4 range alone won't match IPv6 connections. In dual-stack environments, check which address family is actually in use.
4. How domainStrategy Controls DNS Resolution for Routing
routing.domainStrategy controls how routing handles domain-name destinations. It is not the same as system DNS settings or a similarly named setting on an outbound. Even if routing doesn't resolve the domain, a later outbound may still need to resolve an address to connect to its server.
| Value | Routing behavior | Typical effect |
|---|---|---|
AsIs | Match the original destination without resolving the domain for routing | A domain-only destination won't get a resolved IP for IP rules to match |
IPIfNonMatch | If no domain-stage rule matches, try resolving the domain and matching again | An earlier domain match or catch-all may prevent resolution from being attempted |
IPOnDemand | Try resolving the domain when a rule requiring an IP check is reached | An earlier IP rule may determine the result before a later domain rule |
One easy-to-miss case: using IPIfNonMatch while adding a final rule that matches all TCP and UDP traffic. A domain request will match that catch-all directly, so you can't expect the core to resolve it and try again after an earlier geoip rule fails. If domain rules should take precedence, followed by routing based on the resolved IP, review the resolution strategy and catch-all rules together.
Domain rules first, resolve if none match
- domainStrategy
- IPIfNonMatch
- Start of rules
- Explicit domain exceptions
- Conditions that follow
- IP rules that need an additional check
- Catch-all check
- Avoid an early match by a general rule
Make sure the first outbound is the intended default when no rule matches.
Resolve on demand as rules are evaluated
- domainStrategy
- IPOnDemand
- Start of rules
- Domain exceptions to keep ahead of other rules
- Conditions that follow
- IP or geoip conditions
- Catch-all check
- Add an explicit catch-all after IP checks if needed
First check that DNS returns an address, then verify that the IP category is what you expect.
If the application already gives the core an IP destination, routing doesn't need this setting to obtain that address. However, the domain name may no longer be available. Inbound sniffing can recover domain information for some protocols, but this depends on the traffic type and configuration; enabling it does not restore domain names for every connection.
5. A routing Example to Check Rule Order
This is a config snippet, not a standalone, complete config. It assumes you already have outbound tags proxy, direct, and block, pointing to proxy, direct, and blocking outbounds, respectively. These tags are user-defined, not reserved names created by the core. This example also requires a loaded data file that includes the private category.
{
"routing": {
"domainStrategy": "IPOnDemand",
"rules": [
{
"type": "field",
"domain": ["full:status.example.com"],
"outboundTag": "direct"
},
{
"type": "field",
"domain": ["domain:example.com"],
"outboundTag": "proxy"
},
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "direct"
},
{
"type": "field",
"network": "tcp,udp",
"port": "6881-6889",
"outboundTag": "block"
},
{
"type": "field",
"network": "tcp,udp",
"outboundTag": "proxy"
}
]
}
}
- Exact exception first:
status.example.commatches rule 1 and goes direct; it won't fall through to rule 2's domain-suffix match. - Site rule next:
www.example.commatches rule 2 and usesproxy. - Private addresses next: If neither of the first two rules matches, a private address can match rule 3. A domain destination may be resolved on demand here.
- The port rule isn't a global block: Rule 4 handles only connections not matched earlier. Connections already matched by a site or private-address rule won't reach it.
- Explicit catch-all last: Remaining TCP and UDP connections match rule 5.
IPOnDemandgives the earlier IP checks a chance to run first.
Action: Move global restrictions to the top
If ports 6881–6889 must always be blocked, move the port rule ahead of the domain and private-address rules. Change its position in the array; there is no “priority number” to add to a rule.
You can add a domain rule using geosite:cn if it fits your needs, but don't route every match just because the category name looks familiar. First confirm that the data file loads, then decide whether this rule belongs after exact-domain exceptions, before IP rules, or elsewhere. Data updates can also change which domains match.
6. Checking the Effective Config in the Client
UI labels and routing import methods vary across versions of v2rayN, v2rayNG, and v2flyNG. When troubleshooting, note the client version, core name, and routing mode, then find the rule set currently in use. Don't mistake VMess or VLESS connection settings in a subscription node for routing rules: nodes define outbound connections; routing selects which one to use.
- Back up your current rules: Save the routing config before editing. Subscription updates are generally for updating nodes, not a universal way to distribute custom local routing rules.
- Confirm traffic reaches the core: Check whether the app uses the system proxy or a specified local proxy. Requests that don't enter an inbound aren't controlled by these rules.
- Check the generated config: Verify the actual order of
routing.rulesand confirm thatoutboundTagexactly matches a value inoutbounds[].tag. - Apply the changes and test again: After saving the rules, reload the config or restart the core as required by the client, then create a new connection. Browser connection reuse may keep existing connections on their original outbound.
- Check the logs: Temporarily set the log level to
infoif needed, then inspect the inbound, destination, and selected outbound. Log formats vary by core and may not show the rule index.
Change one condition at a time. First use full: to test routing for a specific domain, then switch to domain: to expand the match, and finally add a geosite or geoip category. For context on config objects, use the configuration reference to check inbound, outbound, and DNS fields.
7. Common Questions About Routing Rules
Why is a domain rule still using the old outbound?
First check whether an earlier, broader rule is already matching. Temporarily move your test full: rule to the top, reload the core, and create a new connection. If it still doesn't work, check whether the destination is a domain or an IP and whether the traffic is actually passing through this client.
What should I change if geosite or geoip reports a missing category?
Check which data file the active core actually uses and verify the category name in that file. Put geosite: in domain and geoip: in ip. First verify the rule structure with a single domain or a known range, then investigate missing data files. Repeatedly changing the node password won't help.
Why does a rule match less often when it has both domain and ip?
Both fields must match; they aren't alternatives. If you want traffic to go direct when either a domain or a network range matches, create two rules and set the same outboundTag on each. Then check that the domain resolution strategy allows IP conditions to be evaluated.
Why isn't geoip working with IPIfNonMatch?
Check whether there's a general match such as network: "tcp,udp" at the end. It may already match during the domain stage, preventing the “resolve if no match” flow. Depending on your needs, switch to on-demand resolution or remove the early catch-all and verify the default outbound. Retest both domain and IP destinations after either change.