For Mac users who have imported a subscription and want to understand config.json. Start with a complete example containing local inbounds, direct routing, and blocking, then review ports, outbound tags, rule order, and DNS. The example ports are specified for illustration and are not defaults for every client.
How config.json differs from v2rayN settings
config.json is the configuration file read by the core; v2rayN is a desktop client for managing servers, subscriptions, and runtime status. They operate at different levels: the client can translate its settings into a runtime configuration or manage a custom one. A file named config.json is not necessarily the file currently being read by the running process.
This article uses the traditional V2Ray JSON configuration format. Xray and V2Ray share many fields, but their protocol support and specific parameters are not identical. Before inspecting a configuration, check the client’s runtime logs to confirm which core is running, the configuration path, and any startup errors. Don’t paste protocol parameters intended for one core into another.
This flow also shows what the system proxy does: it directs requests from apps that honor the system proxy settings to a local inbound. Starting the core alone does not route traffic from every app through this path. Apps that ignore the system proxy need their own proxy settings or another explicit method of routing traffic.
inbounds: Accept requests and define the listening address, port, and local proxy protocol.outbounds: Handle requests using a direct connection, proxy server, or blocking action.routing: Select an outbound and send matching requests to its specified tag.loganddns: Control logging and the core’s DNS behavior, respectively; neither replaces the roles of the first three sections.
Complete example: two inbounds and two outbounds
Here’s a configuration for understanding the structure on your own Mac. It provides SOCKS and HTTP entry points, connects directly by default, and blocks the example domain example.com and its subdomains. It does not configure a remote proxy server, so it is not a subscription-node configuration. Before testing, make sure the ports aren’t already in use by a running client.
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"tag": "local-socks",
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": false
}
},
{
"tag": "local-http",
"listen": "127.0.0.1",
"port": 10809,
"protocol": "http",
"settings": {}
}
],
"outbounds": [
{
"tag": "direct",
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
}
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"domain": [
"domain:example.com"
],
"outboundTag": "blocked"
},
{
"type": "field",
"ip": [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
],
"outboundTag": "direct"
}
]
}
}
At the top level, inbounds, outbounds, and rules are arrays. Each array can contain multiple objects inside square brackets, separated by commas; don’t add a comma after the last item. For compatibility with standard JSON tools, use double quotes, omit comments, and enter ports as numbers rather than quoted strings.
inbounds: Listening address, port, and inbound tag
listen specifies the network interface that accepts connections. The example uses 127.0.0.1, which allows connections only from the same Mac. Changing it to 0.0.0.0 listens on all IPv4 interfaces, but doesn’t guarantee access from other devices; firewall rules, network isolation, and authentication still matter. For local use, keeping the listener on loopback makes access easier to control.
SOCKS inbound
- Tag
local-socks- Listen address
127.0.0.1:10808- Protocol
socks- Authentication
noauth; local use only- UDP
false; disabled in this example
Set the app to use a SOCKS proxy; don’t enter this port as an HTTP proxy port.
HTTP inbound
- Tag
local-http- Listen address
127.0.0.1:10809- Protocol
http- Setting
settings: {}
Accepts HTTP proxy requests and CONNECT tunnels; the inbound itself does not provide TLS-encrypted listening.
On macOS, go to System Settings → Network → your current connection → Details → Proxies to check the address and port. For the HTTP inbound in this example, set Web Proxy and Secure Web Proxy to 127.0.0.1:10809 as needed. For the SOCKS inbound, use port 10808. During troubleshooting, enable only the method you need to avoid conflicts between settings.
tag is an internal identifier in the core configuration, not a server address or a name that any app can recognize. In routing rules, inboundTag can distinguish where a request came from. For example, one rule can handle local-http and another local-socks, letting you set different policies for each entry point.
outbounds: Protocol determines behavior; tag links to routing
In this example, freedom is a direct outbound that connects to the destination from the current device; blackhole blocks requests. A remote proxy outbound also needs the server address, port, and identity parameters required by its protocol, as well as any transport settings. Changing only tag to proxy won’t turn a direct connection into a proxy.
| Field or protocol | Meaning | What to check |
|---|---|---|
tag | Internal name of the outbound | Must exactly match the rule’s outboundTag |
protocol | Selects the outbound implementation | Confirm the running core supports the protocol |
settings | Parameters specific to the protocol | Different protocols don’t share the same configuration structure |
streamSettings | Transport and transport security settings | Match each setting to the server’s requirements |
freedom | Connect directly to the destination | Uses the device’s actual network connection |
blackhole | Blocks matching requests | Make sure legitimate traffic isn’t matched by mistake |
In a typical configuration, requests that don’t match a routing rule use the first outbound in the outbounds array. Here, direct comes first, so unmatched requests connect directly. This order is especially important when reading a client-generated configuration: having a proxy outbound in the file doesn’t mean unmatched traffic will use it.
Takeaway: Check the default outbound before reviewing specific rules
If you’re troubleshooting why traffic still connects directly, check the first outbound, then verify the matching rule’s outboundTag. Don’t infer the actual route from a node name or a color in the interface.
routing: First-match order, combined conditions, and domain handling
routing.rules are checked from top to bottom. When a rule matches, its specified outbound is used and no later rules are checked, even if they’re more specific. Put explicit exceptions before broader rules. Array order is part of the configuration’s behavior; changing it can change the route without causing a syntax error.
- Request to
www.example.com: The first rule’sdomain:example.commatches the root domain and its subdomains, so the request goes toblocked. - Request to
192.168.1.20: The first rule doesn’t match; the second matches the private IP range and sends the request todirect. - Destination doesn’t meet either condition: No rule matches, so the first outbound,
direct, is used.
Within a rule, different condition fields generally all need to match. Multiple values for the same field are treated as alternatives. For example, the rule below blocks only TCP requests from the HTTP inbound to port 25. A request to the same port from the SOCKS inbound doesn’t meet all the conditions.
{
"type": "field",
"inboundTag": ["local-http"],
"network": "tcp",
"port": "25",
"outboundTag": "blocked"
}
Add this object to the existing routing.rules array and place it where needed; it isn’t a complete configuration. Note that field types differ: inbound port is a number, while a routing rule’s port is a string representing a port or range, such as "80,443".
AsIs: The routing stage doesn’t resolve domains just to check IP rules. If the request destination is already an IP address, IP rules can still match it.IPIfNonMatch: If a domain request doesn’t match the current conditions, try resolving it to an IP address, then run routing again.IPOnDemand: Resolve the domain when a rule requires an IP address during matching.
For conditions such as geosite: or geoip:, make sure the core can read the required data files and that the requested entries exist. This article uses explicit domain names and CIDR ranges to distinguish rule behavior from external data dependencies. Don’t confuse routing.domainStrategy with similarly named DNS strategy options in the direct outbound.
DNS and sniffing: What destinations can rules see?
Whether a domain rule matches depends on the destination information received by the inbound. HTTP proxy requests and CONNECT usually include a domain name; SOCKS requests can include either a domain name or an IP address already resolved by the app. If the app resolves the name locally, the core may receive only the IP address, so don’t assume it can always see the original domain.
curl --proxy socks5h://127.0.0.1:10808 --connect-timeout 5 https://example.com/
curl --proxy http://127.0.0.1:10809 --connect-timeout 5 https://example.com/
The first command uses socks5h so curl passes the domain to the SOCKS proxy for resolution. With socks5, curl resolves the destination locally. The domain request is expected to be blocked by the rules in this example, so a failed command is the intended result; the exact error depends on where the connection fails. Here, 5 is the connection timeout in seconds, not a performance measurement.
- The
dnsobject configures the core’s DNS functionality; it doesn’t automatically change macOS system DNS settings. sniffingcan identify information such as HTTP Host or TLS server names in some traffic, but it can’t recover a domain name from every connection.- Whether sniffing is enabled, whether it overrides the destination, and whether the app resolves the domain first can all affect the final match.
- When troubleshooting, keep the proxy type and test command fixed, then change one variable at a time so you can compare the logs.
A safe editing sequence and common troubleshooting checks
Back up the file first, confirm which file is active, and only then make changes. v2rayN may regenerate its runtime configuration when you switch nodes, update a subscription, or restart the core. Editing a generated file temporarily can help test an issue, but it’s not a reliable way to keep rules long term. Save persistent changes in the relevant client settings or a managed custom configuration.
- Confirm the core and file path. Check the startup logs for the core name, loaded file, and error lines. If the path contains spaces, put it in quotes in terminal commands.
- Check the JSON structure. Run the command below in macOS Terminal. It parses the JSON but doesn’t check whether the core supports every field.
- Check the listening ports. Make sure
10808and10809aren’t in use by another process, and confirm they match the app settings. - Check tag references. Verify that each
outboundTagpoints to an existing outbound and eachinboundTagmatches an inbound tag. - Reload and test. Restart or reload the core from the client, confirm it starts successfully, then test destinations known to match and not match.
plutil -lint -s -r -- config.json
lsof -nP -iTCP:10808 -sTCP:LISTEN
lsof -nP -iTCP:10809 -sTCP:LISTEN
plutil is used here to check JSON syntax. Even if the syntax is valid, a misspelled protocol, missing required parameters, or an incompatible core can still prevent startup. If the listener check returns no output, that alone doesn’t prove the configuration has a syntax error; also check whether the process started and whether it loaded a different file.
| Symptom | Check first | Next step |
|---|---|---|
| Core exits immediately on startup | JSON syntax, field compatibility, required parameters | Read the first actionable error in the startup log |
| Address already in use | Inbound port and duplicate processes | Identify the process listening on the port, then stop it or choose another port |
| Rule doesn’t match | Rule order, destination format, inbound tag | Reproduce with a fixed proxy command and compare the logs |
| Changes disappear after restart | Check whether you edited a generated file | Move the settings into the client’s managed configuration |
| Works in the browser but not in other apps | Check whether the app honors the system proxy | Check the app’s proxy type and port settings |
Takeaway: Change one layer at a time
First confirm the inbound can accept requests, then make sure the outbound works, and only then adjust routing. Temporarily increasing log verbosity can help with troubleshooting; restore the original level afterward. Before sharing logs or configuration, remove subscription URLs, credentials, and private destination details.