For users who can already connect to a server and are ready to fine-tune traffic routing. Using the core routing model in V2Ray and Xray, this guide explains domain suffixes, exact domain matches, IP ranges, and category data. It walks through rule snippets step by step, then shows how to verify the active results in v2rayN and v2rayNG. The goal isn’t to pile on rules, but to give every request a clear, verifiable route.
Routing Basics: Make Sure Traffic Reaches the Core
System proxy settings and routing rules handle different stages. The system proxy determines whether proxy-aware apps send requests to a local port; core routing selects an outbound for requests that have reached the core. If a browser isn’t using the system proxy, or an app connects directly, even a precisely written geosite rule won’t handle that connection. TUN can capture more traffic, but enabling it doesn’t change the basic top-to-bottom rule matching order.
v2rayN is a desktop client that generates and starts the core configuration. v2rayNG uses the Xray core, while v2flyNG uses the V2Fly core. Routing presets in a client’s interface are separate from the proxy protocol: VMess and VLESS describe the protocol used for the proxy connection, while domain, ip, and outboundTag define local traffic routing. Switching the server protocol won’t fix a misconfigured domain rule.
Domain Syntax: Suffix Matches Aren’t String Prefix Matches
The domain field in a rule is an array of strings; matching any one entry satisfies the domain condition. Prefixes such as domain:, full:, and regexp: specify how each string is matched. The most commonly misunderstood is domain:: it matches the specified domain and its subdomains, not any domain that starts with those characters.
| Rule string | What it matches | Example |
|---|---|---|
domain:example.test | The domain and its subdomains | Matches example.test and api.example.test; not notexample.test |
full:api.example.test | Exact domain match | Matches api.example.test; not img.api.example.test |
example | Substring match without a prefix | May also match another-example.test, so the match is broad |
regexp:^api[.]example[.]test$ | Regular expression match | This pattern matches one exact domain |
geosite:cn | Matches the cn category in the data file | The actual scope depends on the geosite data in use |
Use domain: to route a service and all its subdomains directly; use full: for an exception that applies to just one domain. Bare strings work for keyword rules when you know exactly what they’ll match, but they aren’t suitable for treating a short term as an exact domain. Regex syntax is also limited by the regex engine used by the core, so test complex expressions first rather than assuming every feature supported by other tools will work.
{
"type": "field",
"domain": [
"full:api.example.test",
"domain:assets.example.test"
],
"outboundTag": "direct"
}
This rule sends the specified API or static asset domain direct, without routing all of example.test directly. When placing a regex in JSON, remember to escape backslashes: to use \. for a dot, write \\. in the JSON string. The table uses [.] for a dot to avoid this extra layer of escaping.
IP and geoip: Address Ranges, Categories, and Combined Conditions
The ip array accepts individual addresses, CIDR ranges, and geoip: categories. The number after the slash in a CIDR is the network prefix length: 192.168.0.0/16 represents that entire private address range, not just one local network address. IPv6 ranges can be written the same way, such as fc00::/7. Category matching depends on GeoIP data available to the core.
These totals include every address in each range; they aren’t the number of addresses available to assign to hosts. geoip:private is often used to route local network and other special-purpose addresses, while geoip:cn matches IP ranges marked as China in the data file. It doesn’t identify Chinese-language websites, and its coverage won’t necessarily align with domain categories. A domain may use CDN addresses in multiple regions, and its IP category can change with DNS resolution.
{
"type": "field",
"ip": ["192.168.0.0/16"],
"port": "443",
"network": "tcp",
"outboundTag": "direct"
}
Key point: Different fields in one rule are combined with AND
The rule above sends traffic direct only when all three conditions are true: the destination IP is in the specified range, the destination port is 443, and the network is TCP. To send traffic direct when either a specified domain or IP matches, use two separate rules. Putting domain and ip in one rule does not create an either-or condition.
geosite and domainStrategy: Domain Categories and When Resolution Happens
geosite:cn is a category name in a data file, not an online lookup command. The core must be able to read a geosite.dat file containing that category. Different data sources may include different tags, and updating a server subscription doesn’t update the category file. If the startup log says a category is missing or a data file can’t be read, fix the resource first, then check rule order.
Match domains first; no additional resolution
- Field
routing.domainStrategy- Value
AsIs- Domain request
- Match domain rules directly
- IP condition
- Don’t resolve the domain just to match a route
Suitable when you want explicit domain rules to determine the route; the outbound connection may still need to resolve the domain.
No domain match; check the IP next
- Field
routing.domainStrategy- Value
IPIfNonMatch- Resolution trigger
- The first routing pass finds no matching rule
- Next step
- After resolution, match again using the IP address
Suitable for supplementing domain-based routing with IP categories, provided the first pass is allowed to find no match.
Another common value is IPOnDemand: resolution may be triggered during matching as soon as a rule requires an IP. As a result, an early IP rule may match before a later domain exception. The key difference from IPIfNonMatch is when resolution is triggered, not which option is “stronger.”
This refers to domainStrategy under routing. It’s separate from the DNS module’s query strategy and the domain strategy used by a direct outbound. Routing resolution only provides data for matching; it doesn’t guarantee that DNS requests use a particular outbound. Check DNS server reachability, the resolution path, and how the app handles DNS separately.
Rule Priority: Put Specific Exceptions Before Broad Matches
The core checks the rules array from top to bottom. The first matching rule determines the route; a later, more specific domain doesn’t override it automatically. In other words, “more specific” is a reason to arrange rules in a certain order, not an automatically calculated priority. Rule tags must point to defined outbounds. If no rule matches, the core usually uses the first outbound in the outbounds array; it doesn’t automatically select a tag named proxy.
- Service-specific exceptions: Start with exact domains, narrow suffix matches, and specific IP ranges that need to go through a proxy or connect directly.
- Protect local traffic: Handle private IP addresses next to keep local device requests from going through the proxy by default. If a corporate network needs a special route, put its more specific exception first.
- Category rules: Add broader routing conditions such as geosite and geoip categories.
- Catch-all: Specify where remaining connections should go, and check whether the rule changes when the resolution strategy is triggered.
Here’s a routing snippet: proxy api.example.test on its own, connect directly to other domains under the same parent, connect directly to private addresses and the China category, and proxy remaining TCP / UDP traffic. IPOnDemand lets requests not covered by domain categories get resolved when they reach the IP rules. Merge this snippet into an existing core configuration; it isn’t a complete configuration file and can’t be started on its own.
{
"routing": {
"domainStrategy": "IPOnDemand",
"rules": [
{
"type": "field",
"domain": ["full:api.example.test"],
"outboundTag": "proxy"
},
{
"type": "field",
"domain": ["domain:example.test"],
"outboundTag": "direct"
},
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "direct"
},
{
"type": "field",
"domain": ["geosite:cn"],
"outboundTag": "direct"
},
{
"type": "field",
"ip": ["geoip:cn"],
"outboundTag": "direct"
},
{
"type": "field",
"network": "tcp,udp",
"outboundTag": "proxy"
}
]
}
}
| Input condition | Matching rule | Route |
|---|---|---|
| Domain: api.example.test | Rule 1: exact domain match | proxy |
| Domain: img.example.test | Rule 2: domain suffix match | direct |
| Destination IP: 192.168.1.10 | Rule 3: private address match | direct |
| Domain isn’t in the categories above; resolved IP matches geoip:cn | Rule 5: IP category match | direct |
| Any other unmatched TCP request | Rule 6: catch-all | proxy |
If you move rule 2 ahead of rule 1, api.example.test matches the parent-domain rule first and goes direct instead. Move the catch-all to the top, and the rules below it will effectively never apply. When troubleshooting conflicts, trace the actual target from the first rule down instead of focusing only on the rule you expect to match.
Apply the Rules in Your Client: Check the Generated Config, Then Test
In v2rayN, check the active routing configuration under Settings → Routing Settings; the menu label may vary by version. Make sure you’re editing the configuration in use, not an unselected preset. Under Settings → Parameter Settings, check the local listening port. The test below assumes SOCKS is listening on 127.0.0.1:10808. Substitute your actual port if it differs; the example isn’t a fixed default for every version.
- Back up your current rules. Note the core, routing preset, domain strategy, and outbound tags in use. Start by adding just one easy-to-identify domain exception.
- Save and reapply. Follow the client’s current process to restart the core or reconnect so the configuration is regenerated. Saving in the editor alone may not switch the active configuration.
- Check the runtime logs. First rule out JSON parsing errors, missing categories, undefined outbound tags, and a port already in use. Temporarily increase the log level if needed, then restore it after testing.
- Check the actual destination. Look for the target domain, destination IP, and outbound tag in the connection or access logs. Log details depend on the core and log level.
- Expand the scope gradually. Only add geosite / geoip categories and a catch-all after the single exception is verified. This makes it easier to pinpoint problems than changing several things at once.
curl --socks5-hostname 127.0.0.1:10808 \
--connect-timeout 10 \
--max-time 20 \
-I https://v2rayworld.com/
--socks5-hostname sends the target domain to the SOCKS server for resolution, which helps test domain rules. With --socks5, the domain is usually resolved locally first, so the core may only see an IP. The 10-second connection timeout and 20-second total timeout limit how long the test waits; they aren’t latency or speed measurements. A successful HTTP request only confirms that this request worked. Check the outbound logs to verify which rule matched.
In v2rayNG, first check the active routing mode, then open the corresponding routing configuration under Settings; menu paths vary by version. Android VPN traffic capture may also depend on per-app proxy settings, so make sure the test browser is included. Confirm v2flyNG’s core capabilities and resource files against its actual V2Fly configuration; don’t copy Xray-specific fields over unchanged. For the basic field rules covered here, the current core’s startup result is the final authority.
Troubleshooting: Domain Rules Not Matching, Category Errors, and Overridden Rules
Why does traffic still match an IP rule when the domain rule is correct?
First check whether the inbound connection received a domain or an IP address. Use the --socks5-hostname test above as a comparison. If the app resolved the domain itself, the core may have no domain to match. Sniffing can extract domains from some HTTP / TLS traffic, but not every connection; don’t rely on it as a dependable fix for all traffic.
geosite:cn Prevents Startup After I Save. What Should I Do?
Check the core logs for a missing geosite file or cn category. Verify the resource file location, read permissions, and data source used by the client. Restore the previous rules so the core can start, then use the client’s resource update option to update the data. Don’t hide a resource error by deleting all routing rules.
Why did adding domain and ip make the direct-routing scope smaller?
Check whether both conditions were added to the same rule. To match when either the domain or address qualifies, split them into two rules and assign the same outbound to each. Fields in one rule must all match, and matching an IP condition for a domain request also depends on the resolution strategy.
Why do my core configuration changes disappear after reconnecting?
The client may regenerate a temporary core configuration at startup. Make changes in the active routing preset or the client’s supported custom configuration area, rather than editing only the temporary runtime file. Reapply the changes, then check the generated rule order to make sure preset rules haven’t overridden them.
Use this checklist in order: Did the request reach the core? Does the target include a domain? Can the category data be read? Are the rules in the expected order? Is the outbound tag valid? Browsers may reuse existing connections, so start a new request or restart the test app after making changes. Once you confirm a match, test a regular domain, a local network IP, and an uncategorized target separately. Don’t assume the entire routing setup works based on a single website.