This guide is for anyone who needs to read a client-generated config or troubleshoot ports and routing. Start with a complete configuration containing local inbounds, direct and block outbounds, then learn how remote proxies, rule order, and DNS fit in. The example helps verify the configuration structure; it does not include a usable remote proxy. By the end, you’ll know where a request enters, which rule it matches, and which outbound handles it.
1. config.json Structure and Request Flow
config.json is a common configuration filename, not a fixed requirement. The kernel’s startup arguments determine which file it loads. This guide uses V2Ray’s traditional JSON configuration format. V2Ray and Xray share many core fields, but their extensions and version support differ; don’t assume a field added in one kernel can be copied directly to the other.
inbounds accept app traffic, outbounds define how connections are made, and routing selects an outbound. log records runtime information, while dns configures the built-in resolver. The order of these top-level keys in a JSON object usually doesn’t affect execution, but the order of rules in the routing array does.
| Top-level field | Data type | What to check first |
|---|---|---|
log | Object | Can you see startup errors and request logs? |
inbounds | Array | Do the app’s connection protocol, address, and port match the inbound? |
outbounds | Array | Are the outbound protocol and its settings complete? |
routing | Object | Do the rule order and outbound tags line up? |
dns | Object | Which steps actually use the built-in resolver? |
Use the configuration below as a standalone kernel structure test. It listens on two local ports, blocks example.org and its subdomains, and sends all other requests directly by default. It does not send traffic to a remote proxy or enable the system proxy automatically.
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"tag": "socks-in",
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": false
}
},
{
"tag": "http-in",
"listen": "127.0.0.1",
"port": 10809,
"protocol": "http",
"settings": {}
}
],
"outbounds": [
{
"tag": "direct",
"protocol": "freedom",
"settings": {}
},
{
"tag": "block",
"protocol": "blackhole",
"settings": {}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"domain": ["domain:example.org"],
"outboundTag": "block"
},
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "direct"
}
]
},
"dns": {
"servers": ["1.1.1.1"]
}
}
2. log: Make Configuration Errors Easier to Find
Common loglevel values are debug, info, warning, error, and none. This example uses warning to keep routine output manageable. For startup or connection issues, temporarily switch to info; use debug when you need more detailed internal logs.
{
"log": {
"access": "access.log",
"error": "error.log",
"loglevel": "info"
}
}
This is a top-level snippet: use it to replace log in the full configuration, not to add a duplicate key. Relative log paths are resolved from the process’s working directory, which may not be next to the config file. When a client launches the kernel, its working directory, file permissions, and log display may differ; start with the runtime logs provided by the client.
- Startup fails immediately: check for JSON parsing errors, unknown fields, or a port binding failure.
- No app requests are logged: first confirm the app is connecting to the right inbound. Don’t start by changing remote protocol settings.
- The connection fails after the request enters: check the outbound, destination address, DNS resolution, and network connectivity.
- Before sharing logs, remove sensitive details such as server addresses, user identifiers, subscription URLs, and visited domains.
3. inbounds: Listen Addresses, Ports, and Proxy Protocols
inbounds is an array, so you can define multiple entry points. Each inbound’s protocol determines how it interprets data from apps. SOCKS and HTTP proxy are different protocols; even when both listen locally, an app configured for the wrong type may fail to connect.
These are explicit example values, not defaults for every client version. For an HTTP proxy, configure the app to use address 127.0.0.1 and port 10809. For a SOCKS5 proxy, use 127.0.0.1:10808.
Local SOCKS inbound
- Tag
socks-in- Listen
127.0.0.1:10808- Protocol
socks- Authentication
noauth- UDP
false
This example handles local TCP requests only; SOCKS UDP forwarding is disabled.
Local HTTP inbound
- Tag
http-in- Listen
127.0.0.1:10809- Protocol
http- Settings
{}
For apps that support HTTP proxies; HTTPS requests usually establish a tunnel with CONNECT.
Setting listen to a loopback address limits access to the local machine. Changing it to 0.0.0.0 listens on all IPv4 interfaces and may expose the inbound to other devices on your LAN. If you need to share it, plan authentication, firewall rules, and access scope together—don’t just change the listen address.
tagidentifies the inbound and can be referenced byinboundTagin routing rules.- If a port is already in use, the kernel will fail to start. Stop any client using the same ports before running the test kernel.
- The system proxy is just one way for apps to connect. Apps that ignore it may still connect to destinations directly.
- Domain sniffing is a separate setting. It’s disabled in this example, so don’t assume the kernel can always recover a domain from a destination IP.
4. outbounds: Matching Protocols to Tags
outbounds define how traffic leaves the kernel. In this example, freedom is the direct outbound and blackhole blocks traffic. direct and block are user-defined tags; a tag alone doesn’t determine what an outbound does. Its behavior comes from protocol and the related settings.
| Outbound tag | Protocol | Behavior in this example |
|---|---|---|
direct | freedom | Connects directly to the destination and handles unmatched requests by default |
block | blackhole | Blocks matching requests without connecting to a remote proxy |
proxy | Depends on the actual proxy node | Not defined here; add a complete proxy outbound before referencing it |
In this type of configuration, requests that don’t match a routing rule use the first outbound in the outbounds array. This example puts direct first, so unmatched requests go direct. If you move a real proxy outbound to the first position, unmatched traffic will use it instead.
A VMess or VLESS outbound requires a real server address, port, user ID, and transport and security settings that match the server. You can’t infer settings such as a WebSocket path or TLS server name from the protocol name alone. For configurations that use Xray extensions, check which features the kernel supports; not every VLESS configuration is compatible with V2Ray.
Bottom line: Check the tags before changing node settings
Compare each rule’s outboundTag with the outbound’s tag, character for character, and make sure the first outbound matches your default traffic policy. Only after both checks pass should you troubleshoot the server and transport settings.
5. routing: Rule Order and outboundTag
routing.rules are checked in array order, and the first matching rule selects its specified outbound. A broad rule placed first may prevent more specific rules later in the list from running. Different condition fields in the same rule generally all need to match; multiple values in an array for one field generally match if any value does.
{
"type": "field",
"inboundTag": ["http-in"],
"domain": ["domain:example.org"],
"outboundTag": "block"
}
The snippet above shows combined conditions: a request must come from http-in and target example.org or one of its subdomains to use block. To match only the exact domain, use full:example.org instead of a domain suffix match.
- First, list the actual outbound tags, such as
directandblockin this example. - Put specific rules that need priority first, followed by broader rules.
- Check every
outboundTag: spelling, capitalization, and hyphens must match the outbound tag exactly. - Check whether the inbound receives a domain or an IP address. Different destination information can lead to different rule matches.
- Be clear that unmatched traffic uses the first outbound; “no match” does not mean traffic is blocked automatically.
| domainStrategy | Routing strategy | What to keep in mind |
|---|---|---|
AsIs | Use the destination information already available; don’t resolve domains just to match routing rules | If the destination is already an IP address, IP rules can still match it |
IPIfNonMatch | If a domain doesn’t match a rule, try resolving it before checking IP rules | May add a DNS lookup; results depend on the addresses returned by DNS |
IPOnDemand | Resolve the domain when a rule needs to check the IP address | Understand when resolution happens in the context of rule order |
This example uses AsIs. That means an app-provided private IP can match geoip:private, but a domain that ultimately resolves to a private address may not match this IP rule. Not resolving at the routing layer doesn’t mean resolution is never needed to establish the connection.
6. dns: The Built-in Resolver Is Not the System DNS
dns.servers specifies servers for the kernel’s built-in resolver. The 1.1.1.1 address here is an example of traditional DNS; it doesn’t enable encrypted DNS or guarantee that the server is reachable on your network. Choose a resolution method for your actual network and confirm that queries can reach the resolver.
- The operating system, browser, and kernel may each perform DNS resolution. Adding
dnsdoes not automatically replace every DNS request made by the system. - When routing uses a strategy that requires resolution, the built-in DNS can help match domains to IP addresses.
- The
freedomoutbound has its owndomainStrategy. Its default behavior differs from strategies that actively use the built-in DNS. - When a domain is handled by a remote proxy, where it gets resolved also depends on the outbound protocol and how the destination is represented.
For example, to have the direct outbound actively use the built-in DNS to obtain an IPv4 address, update its settings with the snippet below, if your kernel supports it. This is an outbound setting, not a routing setting. Both use domainStrategy, but the scope is different.
{
"tag": "direct",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv4"
}
}
7. Validate the Configuration and Understand Client Limitations
Validate the syntax first, then start the listeners, and finally test the request path. Standard JSON does not allow comments or trailing commas. When copying a snippet, replace the relevant object rather than leaving two top-level keys with the same name. A loader accepting extensions doesn’t mean other kernels or clients will support them.
- Back up the original configuration and confirm you’re editing the file that is actually loaded.
- Use the configuration test command for your current kernel, and fix field and resource-file errors first.
- Stop the processes using
10808and10809, then start the test kernel. - Send a request through an explicitly specified proxy port instead of relying on the system proxy being enabled.
- Check the results for the blocked domain and a normal destination, then add a real proxy outbound and more complex rules one at a time.
V2Ray 5 commonly uses the following command to test traditional JSON configurations. Executable paths may vary by distribution; run v2ray help first to confirm the available subcommands. Don’t mix the older -test -config argument format with the newer subcommands.
v2ray test -c config.json
v2ray run -c config.json
Once the kernel starts successfully, use a proxy-aware request tool to test the route. Both commands below request the domain blocked in this example, so the expected result is a failed request or a closed connection. This confirms the rule is working; it is not a connectivity test. The socks5h scheme in the second command sends the domain to the proxy to resolve, helping preserve domain-based matching.
curl --max-time 10 --proxy http://127.0.0.1:10809 http://example.org/
curl --max-time 10 --proxy socks5h://127.0.0.1:10808 http://example.org/
v2rayN is a desktop client that generates the kernel’s runtime configuration based on the selected node, routing, and settings. Generated files may be overwritten when you switch nodes or restart. v2rayNG uses the Xray kernel, while v2flyNG uses the V2Fly kernel. Reviewing their runtime configurations can help explain similar fields, but whether custom JSON imports work and how they interact with UI settings depends on the specific client version.
Why doesn’t changing config.json affect the traffic in my client?
First confirm you edited the file loaded by the current process, not an old exported copy. Then check whether the client regenerated the configuration, restarted the kernel, and routed the app through the relevant inbound. Subscription node settings, client routing presets, and full kernel configurations are separate layers; changing one doesn’t necessarily affect the others.
Does a successful configuration test mean the proxy node will work?
A configuration test mainly checks whether the config can be loaded. It doesn’t prove the remote server is online or confirm that the user ID, transport path, and TLS settings match the server. This example has no remote proxy outbound. To test a real node, add a complete outbound and check the connection with an actual request.
If only domain-based routing fails, where should I start?
First check whether the app sends the inbound a domain or an already-resolved IP. Then review the matching scope of domain: and full:, followed by rule order and domainStrategy. Don’t change DNS, sniffing, and every outbound setting at once before confirming the destination information.