V2Ray config.json Explained: Inbounds, Outbounds, and Routing

A field-by-field guide to config.json, covering log, inbounds, outbounds, routing, and DNS, with explanations and common examples.

At a glance

For users who have imported a subscription and want to understand the core’s runtime configuration. First distinguish the app’s entry points, connection exits, and routing choices, then use a complete configuration with only local listeners, direct connections, and blocking to verify how the fields fit together. By the end, you’ll be able to track down port conflicts, mistyped tags, ineffective rule order, and DNS changes that don’t take effect.

Start with the scope: config.json is a core configuration file, not a subscription list

v2rayN is a desktop client; V2Ray and Xray are cores that handle connections. The client stores subscriptions, nodes, and UI settings, then generates a runtime configuration for the selected core. So a VMess or VLESS share link in a subscription is not a complete config.json: it usually contains only the remote connection details, without local inbounds, logging, DNS, or routing rules.

This guide uses the traditional V2Ray JSON configuration format and illustrates field relationships commonly found in V2Ray and Xray. V2Ray v5 also supports a different configuration format. Don’t infer the format from the file extension alone, and don’t mix fields from the two formats. Protocol and transport fields depend on the version of the core actually in use.

App request Local inbound Route matching Selected outbound Destination

Follow this chain when troubleshooting. A request must enter through an inbound before it can be routed; routing selects an outbound tag, not a subscription group name. DNS supports domain resolution and some routing decisions, but it is not a “proxy exit” that all traffic must pass through.

log: Make sure you can see what’s happening before changing connection settings

At the top level, log controls log output. For troubleshooting, start with info; once the issue is resolved, you can usually switch back to warning. Turning on debug right away can produce so much detail that a simple port conflict gets buried in the output.

{
  "log": {
    "loglevel": "info"
  }
}

This is a field snippet, not a complete runtime configuration. Common log levels are debug, info, warning, error, and none. No log file paths are set here, so you can first check startup output in the terminal or the client’s log panel. If you configure access or error log files, make sure the target directory exists and is writable by the account running the core.

inbounds: The address and port where app traffic enters

inbounds is an array, so one configuration can accept both SOCKS and HTTP proxy requests. listen sets the listening address, port sets the local port, and protocol determines which proxy protocol an app should use to connect. These are not the address and port of a remote node.

10808
SOCKS inbound port used in this guide
10809
HTTP inbound port used in this guide
127.0.0.1
Listening address accessible only from this machine

These are the ports explicitly chosen for this guide, not defaults shared by every client version. If v2rayN is already using one of them, the test core won’t be able to bind to it. Stop the process using the port, or change the port both in the configuration and in the test command. Don’t change just one.

SOCKS inbound

tag
local-socks
listen
127.0.0.1
port
10808
protocol
socks
settings
auth is noauth; udp is false

This example tests TCP only; disabling UDP forwarding won’t affect ordinary HTTPS connections.

HTTP inbound

tag
local-http
listen
127.0.0.1
port
10809
protocol
http
settings
Empty object; uses the inbound’s default settings

HTTPS websites typically establish a tunnel through an HTTP proxy using the CONNECT method.

Set the HTTP proxy in your local app to 127.0.0.1:10809, or set the SOCKS5 proxy to 127.0.0.1:10808. Don’t swap the two protocols based on port numbers alone. Changing the listen address to 0.0.0.0 makes the proxy reachable from more devices. Without access controls and firewall restrictions, don’t expose an unauthenticated proxy to a network other devices can reach.

outbounds: The outbound tag and outbound protocol are different things

outbounds is also an array. tag is a name used for references within the configuration; protocol determines how the outbound actually connects. Naming a tag proxy doesn’t make it a proxy: if its protocol is freedom, it still connects directly.

{
  "outbounds": [
    {
      "tag": "direct",
      "protocol": "freedom",
      "settings": {}
    },
    {
      "tag": "block",
      "protocol": "blackhole",
      "settings": {}
    }
  ]
}

Neither outbound needs a remote account: freedom uses the local network to connect to the destination, while blackhole drops matching traffic. In this configuration, which has no additional settings such as a load balancer, traffic uses the first outbound in the array if no routing rule matches. Here, the default outbound is direct.

Bottom line: Check the protocol behind a tag before changing the outbound

If a rule matches but traffic still goes out over the local network, first check the protocol of the corresponding outbound. If it’s freedom, fix the outbound configuration instead of repeatedly tweaking domain matching conditions.

routing: Rules are checked from top to bottom, and every tag must resolve

routing.rules are checked in order, and the first matching rule determines where traffic goes. Common conditions include the domain, domain; destination IP, ip; port, port; and inbound tag, inboundTag. A rule with different types of conditions generally requires all of them to match. Multiple entries in the same domain list are alternatives.

{
  "routing": {
    "domainStrategy": "AsIs",
    "rules": [
      {
        "type": "field",
        "domain": ["full:blocked.example"],
        "outboundTag": "block"
      },
      {
        "type": "field",
        "ip": ["127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
        "outboundTag": "direct"
      }
    ]
  }
}

full:blocked.example matches only that exact domain; domain:example.org matches that domain and its subdomains. This guide uses reserved example domains to test blocking without relying on real websites. The rule’s outboundTag must match the defined block or direct tag, including capitalization.

domainStrategyMatching behaviorWhat to look for in the configuration
AsIsMatches against destination information already available in the request; routing doesn’t resolve domains proactivelyDon’t expect a domain request to match a rule for its resolved IP automatically
IPIfNonMatchIf no domain rule matches, resolve the domain and then try matching by IPCheck DNS results and any additional resolution delay
IPOnDemandResolve the domain when routing needs IP informationThe position of an IP rule may cause resolution to happen earlier

This example uses AsIs, so a direct request to 192.168.1.1 can match the private IP rule. But a request to a domain that resolves to a private address won’t trigger routing resolution just because this rule exists. A direct outbound may still need to resolve the domain to connect; that happens at the outbound connection stage and is separate from the routing strategy.

dns: An inbuilt resolver doesn’t take over every DNS request

The top-level dns section configures the inbuilt resolver. It participates when routing needs the IP address for a domain or when an outbound is configured to use the inbuilt resolver’s results. Adding dns.servers alone won’t take over encrypted DNS in a browser or all system lookups, nor will it create a local DNS listening port.

{
  "dns": {
    "servers": ["localhost"]
  }
}

Here, localhost is a special value that uses the system resolver; it doesn’t mean a DNS server must be running at 127.0.0.1:53. This keeps the example simple, but it isn’t a DNS isolation strategy. To specify which outbound handles DNS queries, also check the core’s DNS behavior, related routing rules, and the outbound’s domain resolution strategy.

  1. First, check what the app sends: HTTP proxy CONNECT requests usually include a domain name. SOCKS requests can include either a domain name or an IP address the app has already resolved.
  2. Next, check whether routing resolves domains: routing.domainStrategy determines when routing needs to resolve a domain to an IP address.
  3. Finally, check how the outbound connects: The resolution strategy for freedom is an outbound setting. It’s separate from the similarly named concept at the routing level; don’t copy the field to the wrong level.

For example, with curl, socks5://127.0.0.1:10808 usually makes curl resolve the destination locally, while socks5h://127.0.0.1:10808 sends the domain to the SOCKS proxy for resolution. When testing domain-based routing, use the latter where possible; otherwise, the core sees only an IP address and domain rules may never get a chance to match.

Complete configuration: Verify all five top-level fields with direct connections and blocking

Here’s a complete configuration in the traditional JSON format. It provides only local HTTP and SOCKS inbounds, with direct and block outbounds. It includes no remote proxy nodes and won’t make websites unreachable from your local network accessible. Use it to verify the configuration structure, listening ports, and rule references before adding parameters for real nodes.

{
  "log": {
    "loglevel": "info"
  },
  "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": "block",
      "protocol": "blackhole",
      "settings": {}
    }
  ],
  "routing": {
    "domainStrategy": "AsIs",
    "rules": [
      {
        "type": "field",
        "domain": ["full:blocked.example"],
        "outboundTag": "block"
      },
      {
        "type": "field",
        "ip": [
          "127.0.0.0/8",
          "10.0.0.0/8",
          "172.16.0.0/12",
          "192.168.0.0/16"
        ],
        "outboundTag": "direct"
      }
    ]
  },
  "dns": {
    "servers": ["localhost"]
  }
}

The order of top-level fields in a JSON object doesn’t determine execution order; this guide arranges them for readability. What affects behavior is the order of the rules array, the position of the default outbound, and the references between fields. Save the file as UTF-8, use double quotes for keys and strings, omit the comma after the last item, and don’t copy explanatory comments into the file.

Validation order: Check syntax, startup, inbounds, and routing separately

Save the complete example as config.json in the current directory. The commands below use a Linux terminal: first validate the JSON syntax, then have the core you’re actually using validate the configuration. Passing the syntax check means only that the JSON can be parsed; it doesn’t guarantee that protocol fields, file dependencies, or network connections are correct.

python3 -m json.tool config.json > /dev/null

# Run this when using Xray
xray run -test -config config.json

# Run this with V2Ray v5 versions that support this command format
v2ray test -c config.json

Choose one of the two core validation commands based on the core you’re using; don’t run both services just for testing. Subcommands may differ in older executables, so first check with xray help or v2ray help. Once validation succeeds, load the same file using a run command supported by that version, such as Xray’s xray run -config config.json.

# Check TCP listeners from another terminal
ss -lntp 'sport = :10808 or sport = :10809'

# Test the direct outbound through the HTTP inbound
curl --noproxy "" --proxy http://127.0.0.1:10809 --max-time 15 -I https://example.org/

# Pass the domain to the SOCKS inbound to test the block rule
curl --noproxy "" --proxy socks5h://127.0.0.1:10808 --max-time 5 -I https://blocked.example/

Whether the first curl request succeeds still depends on your local network, system DNS, and the destination website; it isn’t a proxy-node speed test. The second request is expected to fail to get an HTTP response because the destination domain matches block. The exact error varies by core and connection stage, so check the logs; a single timeout doesn’t prove that routing worked.

Bottom line: Test one stage at a time

First confirm that the listener is available, then use explicit proxy options to verify that the request reaches the core, and finally check the logs to see which outbound was used. This separates “the app isn’t using the proxy” from “the request reached the proxy, but the rules are wrong” and avoids introducing new variables by changing the port, DNS, and node settings all at once.

  1. Connection refused: Check that the process is running and that the listening address and port match. Remote node settings aren’t the priority yet.
  2. Requests reach the inbound, but use the wrong outbound: Check the rule order, whether the request contains a domain or an IP address, and whether the default outbound is in the wrong position.
  3. Rules look right, but the connection fails: Next, check the resolution results, destination reachability, remote authentication, and transport security settings.
  4. Changes disappear after a restart: Confirm whether you edited a manually maintained configuration or a temporary runtime file generated by the client. If you use the client, return to the relevant settings in the user guide.

Once you understand this structure, configurations generated by v2rayN are much easier to read: find the inbound ports, then the default outbound, then the rules, and finally where DNS actually comes into play. For platform installation and core setup, see the complete documentation; there’s no need to pack every advanced field into one file.

Download v2rayN