config.json · Xray / V2Fly core
V2Ray Config File Reference
A section-by-section walkthrough of one config.json: top-level structure, inbounds and outbounds, routing rules, DNS resolution, policy tuning, and how these fields are generated in v2rayN and v2rayNG. The snippets work as a lookup reference.
How to use this page and reading path
How this page and the tutorial split the work
This page is the systematic reference manual for the V2Ray config file: it walks through one config.json field by field, from the top-level structure down to each functional section. There is also a quick start tutorial on the site, and that track does exactly one thing: import a subscription into the client, pick a mode, connect, and confirm it works. The split is clear — the tutorial answers “what do I click next”, while this page answers “what does this field mean, what values are valid, and what happens if I get it wrong”. If you are already connecting fine and just want to use it day to day, you do not need to read this page end to end.
Where the config file sits in the chain
All three clients are graphical shells; the component that actually opens connections is the core bundled inside them. The v2rayN desktop edition ships with the Xray core, v2rayNG uses Xray, and v2flyNG uses the V2Fly core. Every checkbox in the GUI — transport, TLS, multiplexing, routing switches — is ultimately translated into a JSON config. The core reads that JSON at startup, listens on local ports according to its inbounds, and sends traffic out according to its outbounds. Once you understand that translation layer, troubleshooting starts with a simpler question: was an option in the GUI set wrong, or is the generated config itself the problem?
Hand-editing support in the three clients
v2rayN offers a full JSON editing entry point in its node editing window; nodes imported from a subscription are first parsed into an internal structure, then regenerated into a config by the client using the current settings. v2rayNG supports custom configs and subscription imports, but editing long JSON on mobile is awkward, so the more common approach is to prepare it on desktop and import it. v2flyNG shares the same config format as v2rayNG, with the difference being the core family. Download entry points for all three clients are on the Get the clients page, and a side-by-side comparison is in the comparison review.
Suggested reading order
If this is your first time with a config file, start with the structure overview in chapter two to get the big picture of which top-level sections exist and which two are required; after that, jump to specific chapters as needed. The three sections you will deal with most in daily use are outbounds, routing and dns: outbounds decide how traffic leaves, routing decides which traffic takes which outbound, and dns decides where domain names get resolved. policy is a tuning section — the defaults are enough for most setups, so only dig into it when you need to control memory use or connection reclamation time.
Fields and core versions
The config format evolves gradually between core versions, and new transports and security types usually land on the Xray side first, with V2Fly following. The examples on this page stick to common fields and are not tied to a specific version number. To tell whether a field works in your current client, the reliable method is the runtime log: fields the core does not recognize produce an error or an ignore notice in the startup log rather than silently taking effect.
About the example values
Every domain, UUID and public key in the config snippets on this page is an obviously replaceable placeholder — do not copy them straight into a real config. Node parameters should come from whatever your service provider gives you. The section bar at the top of the page jumps to any chapter, and each chapter is broken into subsections: tables for quick field lookup, code blocks for complete snippets, and tinted callout blocks for the easy-to-miss pitfalls.
JSON structure overview
Which sections sit at the top level
A complete config file is a single JSON object. Nine sections commonly appear at the top level: log handles logging, inbounds defines the local listening entry points, outbounds defines where traffic exits, routing defines the split rules, dns defines the resolution policy, policy defines connection and buffer behavior, stats and api let the graphical client read runtime state, and reverse is for reverse proxy setups. Of these, inbounds and outbounds are the two required sections: without an inbound, application traffic cannot get in; without an outbound, traffic cannot get out.
The minimal config below keeps only the required sections plus a log section. It is useful for understanding the structure, but it only does direct connections and has no proxy capability:
{
"log": { "loglevel": "warning" },
"inbounds": [
{
"tag": "socks-in",
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": { "udp": true }
}
],
"outbounds": [
{ "tag": "direct", "protocol": "freedom" }
]
}
Field naming and syntax rules
JSON is strict about syntax, and the most common startup failures in config files come from the points below. Field names are case-sensitive: writing outbounds as outBounds or streamSettings as streamsettings will fail to parse. Standard JSON does not allow comments, so when you copy a config from a web page or a note, any inline double-slash comments and block comments must be removed. Trailing commas are not allowed — one extra comma after the last item in an array or object throws an error. Strings always use double quotes, never single quotes. Numeric values such as ports and timeouts are written as bare numbers, without quotes. Booleans are only the lowercase true and false.
What tag does
Every inbound and outbound can carry a tag field. A tag is a custom string, and routing rules reference a specific inbound or outbound through it — for example, “traffic from socks-in goes to the proxy outbound”. Use readable names such as socks-in, http-in, proxy, direct and block, so the config still makes sense when you come back to it months later. Do not repeat a tag within one config; duplicates make rule targets ambiguous.
| Top-level field | Purpose | Required | Typical value |
|---|---|---|---|
| log | Log level and output location | Optional | {"loglevel":"warning"} |
| inbounds | Local listening entry points | Required | Array, at least one entry |
| outbounds | Traffic exit points | Required | Array, first entry is the default outbound |
| routing | Split routing rules | Optional | {"domainStrategy":"IPIfNonMatch","rules":[]} |
| dns | Domain resolution policy | Optional | {"servers":[]} |
| policy | Connection and buffer policy | Optional | {"levels":{"0":{}}} |
| stats / api | Statistics and local API | Optional | Generated by the graphical client as needed |
How configs load and take effect
The core reads the config once at startup. After you change it, the core needs a restart or a reload trigger, which the graphical client usually handles automatically once you save the node. Config errors show up in two ways. One is a parse failure: the core exits immediately and the log points to the offending location. The other is a config that is syntactically valid but semantically conflicting: the core starts, yet connections behave differently from what you expect — for example, routing rules written in the wrong order so the split never applies. The first kind is easy to pin down; the second takes a section-by-section comparison against the log.
How this relates to subscriptions
Every node in a subscription eventually expands into a config like this. The client generates a separate outbounds entry for each node, then adds the inbound, routing and DNS sections to assemble a complete file the core can read. That means when a subscription updates, the client regenerates the whole config, and any field you edited by hand gets overwritten — a point covered in more detail in the client chapter later.
Different clients store the config file in different places. On desktop it is usually in the program directory or the user config directory; on Android it is managed inside the app. You do not need to care about the exact path for daily use — you only need to find it when exporting a config for backup or migration.
inbounds
What inbounds do
Inbounds describe what the core listens on locally; they are the entry points through which application traffic reaches the core. Graphical clients usually generate two automatically: a socks inbound for browsers and system proxy use, and an http inbound for programs that only support HTTP proxies. They listen on different ports and can coexist. Below is a two-inbound config that includes sniffing:
"inbounds": [
{
"tag": "socks-in",
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": { "auth": "noauth", "udp": true },
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls"],
"routeOnly": false
}
},
{
"tag": "http-in",
"listen": "127.0.0.1",
"port": 10809,
"protocol": "http"
}
]
Field-by-field notes
- tag
- The inbound's identifier, referenced by routing rules. Do not repeat it within one config.
- listen
- Listen address. 127.0.0.1 allows only the local machine; 0.0.0.0 lets other devices on the same network connect.
- port
- Listen port. If it conflicts with another program on the system, the core fails to start and the log reports a listen failure.
- protocol
- Inbound protocol; common values are socks, http and dokodemo-door.
- settings
- Protocol-specific settings. For socks, udp decides whether UDP traffic is forwarded; an http inbound usually needs no extra settings.
- sniffing
- Extracts the real destination domain from the traffic. destOverride lists the protocol types allowed to be overridden; when routeOnly is true the extracted domain is used only for routing decisions and the destination address is not rewritten.
Why sniffing matters
When an application hands the destination to the socks inbound, it may pass an IP rather than a domain. Once the destination arrives as an IP, every domain-based rule in routing stops matching and the split is bound to be wrong. With sniffing enabled, the core pulls the real domain out of the SNI in the TLS handshake or the Host header of an HTTP request, then matches routing rules against that domain. destOverride lists the protocol types allowed to be overridden, commonly http and tls; when routeOnly is true, the extracted domain is used only for routing decisions and the actual destination address is left untouched, which suits setups that need to preserve the original request shape. Desktop and mobile clients both enable sniffing by default.
About dokodemo-door
dokodemo-door is another kind of inbound: it forwards traffic received on a local port straight to a specified destination, and is often used to bring LAN devices or requests on a fixed port into the core. It has no matching switch in the graphical clients, so it belongs to hand-written configs, where you set address and port yourself.
| Inbound protocol | Use | Where it typically appears |
|---|---|---|
| socks | The standard entry point for browsers and system proxies | Generated by default in v2rayN and v2rayNG |
| http | Programs that only support HTTP proxies | Generated by default in v2rayN |
| dokodemo-door | Port forwarding and LAN access | Manual configuration |
The security boundary of the listen address
With listen set to 127.0.0.1, only the local machine can reach the port, and that is the default. Changing it to 0.0.0.0 lets other devices on the same network point their proxy at this machine, which suits cases where you deliberately want to share the proxy, but it requires a network you trust. The matching option in graphical clients is usually labelled “Allow connections from the LAN” — confirm that before turning it on. When the proxy port itself performs no authentication, any device that can reach the port can use it directly.
Port already in use
When an inbound port is already in use, the core fails to start and the log reports a listen failure. There are three ways out: change the inbound port; find the process holding the port and end it; or, if the holder is a leftover core process that did not exit cleanly, restart the client. After changing the port, remember to update the system proxy or browser proxy settings too, otherwise applications keep sending requests to the old port.
outbounds
What outbounds do
Outbounds decide where traffic goes after it leaves the core. outbounds is an array that can hold several entries, each with its own tag. The first entry is the default outbound, used by traffic that matches no routing rule. Below is a VLESS outbound example with a TCP transport paired with REALITY:
"outbounds": [
{
"tag": "proxy",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "node.example.com",
"port": 443,
"users": [
{
"id": "00000000-0000-0000-0000-000000000000",
"encryption": "none",
"flow": "xtls-rprx-vision"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "node.example.com",
"fingerprint": "chrome",
"publicKey": "your-public-key",
"shortId": "your-short-id"
}
},
"mux": { "enabled": false }
},
{ "tag": "direct", "protocol": "freedom" },
{ "tag": "block", "protocol": "blackhole" }
]
Server settings
The vnext array describes the remote server; each entry contains address, port and users. address can be a domain or an IP; port is the port the server listens on. The id inside users is the identity, written as a UUID for both VMess and VLESS. VLESS always sets encryption to none, leaving encryption to the transport layer; VMess alterId defaults to 0 in newer versions, and if an older node still requires a non-zero value, getting it wrong means the connection fails outright. flow is the VLESS flow-control option, used together with the transport security type; with a tcp transport plus TLS or REALITY, the common value is xtls-rprx-vision.
Transport settings
- network
- Transport method. Common values are tcp, ws, grpc and httpupgrade, and it must match the server.
- security
- Transport security type. none means no encryption, tls means standard TLS, and reality means REALITY.
- wsSettings
- WebSocket-specific settings: path is the request path, and headers.Host is the host name in the request header.
- tlsSettings
- TLS-specific settings: serverName is the domain the certificate covers, and allowInsecure skips certificate verification — not recommended to leave on long term.
- realitySettings
- REALITY-specific settings: serverName, publicKey and shortId must match the server exactly, and fingerprint determines how the client fingerprint is presented.
The snippet below is a WebSocket plus TLS transport, at the same level as the REALITY example above — just replace the whole streamSettings block:
"streamSettings": {
"network": "ws",
"security": "tls",
"wsSettings": {
"path": "/your-path",
"headers": { "Host": "node.example.com" }
},
"tlsSettings": {
"serverName": "node.example.com",
"allowInsecure": false
}
}
How a VMess outbound differs
A VMess outbound has the same structure as VLESS; the difference is in the users fields. VMess describes encryption with alterId and security — the former defaults to 0 in newer versions, the latter is commonly auto. Transport settings are fully interchangeable with VLESS, so if one server offers both protocols, switching only means changing protocol and users. The four differences between VMess and VLESS in authentication and transport dependency are laid out in Protocol basics.
mux multiplexing
mux is the multiplexing switch. When enabled, the core merges several connections onto one underlying connection, cutting repeated handshakes and lowering latency on a stable link; the trade-off is that the quality of individual connections affects the others, so sustained high-throughput work such as large downloads can actually get slower. It is off by default in the clients — turn it on only when you need it.
freedom and blackhole
freedom is the direct outbound: whatever it receives is sent out as is, and it is commonly paired with routing rules to handle domains in mainland China and private addresses. It has a sendThrough field that specifies which local address to send from. blackhole is the discard outbound, used to block specific traffic, and it can be configured to return content. Neither needs server settings — a tag is enough to use them.
| Outbound protocol | Use | Notes |
|---|---|---|
| vless | The recommended proxy outbound | No built-in encryption; relies on the transport security type |
| vmess | Compatible with older nodes | alterId defaults to 0 in newer versions |
| freedom | Direct connection | Can specify the local source address |
| blackhole | Blocking and discarding | Return content can be configured |
Transport settings must match item by item
Parameters such as path, Host, SNI, public key and shortId must match the server exactly. Any mismatch shows up as a connection that drops the moment it is established, rather than a clear error message. When troubleshooting, suspect a stray space or a missing slash from copying first.
routing rules
How rules take effect
routing decides which traffic takes which outbound. It has two parts: domainStrategy and rules. Rules are matched from top to bottom, and the first match wins — later rules are not evaluated, so order matters more than count. Each item in the rules array is an object with type always set to field, while the remaining fields describe the match conditions and the action taken on a match.
The three domainStrategy values
- AsIs
- Matches the incoming domain or IP as is, with no resolution. Fastest, but requests that arrive as bare IPs cannot hit domain rules.
- IPIfNonMatch
- When no domain rule matches, the domain is resolved to an IP and IP rules are tried once more. The most common value in everyday use.
- IPOnDemand
- Resolves immediately whenever a rule contains an IP condition. The most thorough matching, and the highest resolution cost.
Rule condition fields
| Condition field | Example value | Notes |
|---|---|---|
| domain | ["domain:example.com"] | Matches by domain; several prefix forms are supported |
| ip | ["geoip:private"] | Matches by destination IP or IP range |
| port | "443" or "0-65535" | Matches by destination port; ranges are supported |
| sourcePort | "1-65535" | Matches by source port |
| inboundTag | ["socks-in"] | Distinguishes traffic by which inbound it came from |
| network | "tcp" or "udp" | Distinguishes by transport protocol |
| protocol | ["http","tls"] | Depends on sniffing results; sniffing must be enabled first |
| outboundTag | "direct" | Action on match: which outbound to use |
| balancerTag | "auto" | Action on match: use a balancer |
The four domain match prefixes
- domain:
- Matches the domain and all its subdomains. domain:example.com matches both example.com and a.example.com.
- full:
- Exact match. full:example.com matches only example.com, with no subdomains.
- keyword:
- Matches whenever the keyword appears. The broadest option and easy to over-match, so use it only when the first two forms cannot express what you need.
- regexp:
- Regular expression match. Flexible, but every request runs the regex, so a long rule list slows matching down.
Below is a split-routing snippet you can use as is: private addresses and domains in mainland China go direct, UDP port 443 is blocked, and everything else takes the default outbound.
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"domain": ["geosite:private"],
"outboundTag": "direct"
},
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "direct"
},
{
"type": "field",
"domain": ["geosite:cn"],
"outboundTag": "direct"
},
{
"type": "field",
"network": "udp",
"port": "443",
"outboundTag": "block"
}
]
}
What happens when the order is reversed
Take a reversed-order example: the first rule says “all traffic goes to proxy” and the second says “domains in mainland China go direct”. Because the first rule already matches everything, the second is never evaluated and the split effectively does nothing. The correct approach is to put specific conditions first, broad conditions later, and a catch-all rule last. To diagnose ordering problems, temporarily set the log level to debug — the core prints the match result for every connection, so you can see exactly which rule was hit.
geosite and geoip data
Forms such as geosite:cn and geoip:private rely on rule data files that the client either bundles or downloads. That data is updated on a cycle, so newly appearing domains may not be included yet, which shows up as a few sites routing differently from what you expect. Graphical clients usually offer a data update entry point in the routing settings — updating periodically is enough. Private address ranges rarely change, so they need no frequent updates.
balancer in brief
balancer distributes traffic across several outbounds and is referenced from rules with balancerTag. You first define a balancer section, use selector to pick outbounds by tag prefix, then specify the distribution strategy. Ordinary users rarely need it; it is useful only when you maintain several equivalent nodes.
Check order when routing does not work
Check in this order: whether sniffing is on, since domain rules depend on it; whether the destination arrives as an IP, which cannot hit domain rules; whether a broader condition earlier in the list is shadowing your rule; and whether the rule data files need updating. If all four are fine, look at the actual match records in the log.
dns configuration
With and without a dns section
The dns section decides how the core resolves domain names. Without it, the core leaves resolution to the system; with it, the core sends queries itself according to the server list and policy in the config. Writing this section buys three things: it keeps resolution results from being tampered with along the way, makes domain-based routing more accurate, and saves one unnecessary resolution round trip. Below is a complete example of the common fields:
"dns": {
"hosts": {
"domain:node.example.com": "203.0.113.10"
},
"queryStrategy": "UseIPv4",
"servers": [
{
"address": "223.5.5.5",
"domains": ["geosite:cn"],
"expectIPs": ["geoip:cn"]
},
{
"address": "1.1.1.1",
"domains": ["geosite:geolocation-!cn"]
}
]
}
- servers
- The DNS server list. An entry can be a plain address string or an object, where domains specifies which domains that server handles and expectIPs verifies that returned results fall inside the expected ranges.
- hosts
- Static mappings that point a domain straight at a fixed IP, skipping resolution entirely. The domain:, full: and keyword: prefixes are supported, with the same syntax as routing rules.
- queryStrategy
- Controls which address family is preferred in resolution results. UseIP places no restriction, while UseIPv4 and UseIPv6 keep only the corresponding family.
- domains
- Written inside a server object to limit which domains that server handles. In the example above, the first server handles domains in mainland China and the second handles the rest.
- expectIPs
- Validates returned results, discarding any resolved address that falls outside the specified ranges. Used to guard against tampered resolution results.
What hosts is for
hosts holds static mappings that point a domain straight at a fixed IP, skipping the resolution step. It suits two situations: the node domain is known and stable and you want to skip resolution, or a test environment needs a domain pinned to a specific address. It supports the domain:, full: and keyword: prefixes, exactly the same as routing rule domains, so there is nothing extra to memorize.
Choosing queryStrategy
queryStrategy controls which address family is preferred in resolution results. UseIP means no restriction, taking whatever order the server returns; UseIPv4 and UseIPv6 keep only the corresponding family. On a network with only IPv4 connectivity, setting UseIPv4 avoids resolving an IPv6 address and then retrying because it cannot connect, saving one failed round trip. Conversely, if the local network is already IPv6-first, UseIPv6 cuts unnecessary dual-stack queries.
How it works with routing
DNS queries are sent by the core itself and do not travel through the proxy chain in outbounds, so you do not need separate routing rules for resolution. What does matter is that resolution results affect routing: with IPIfNonMatch enabled, a domain is resolved to an IP before IP rules are matched, so an inaccurate result makes the IP rules inaccurate too. That is why the dns and routing sections are best read together.
Who resolves in each mode
On desktop with the system proxy, domain resolution is still done by the operating system, and the core's dns section only applies when the core needs to resolve something itself; with TUN mode enabled, the core takes over all traffic, DNS requests included. v2rayNG on Android works through VpnService and likewise takes over DNS queries. So the same dns config has a different scope of effect in different clients and modes — confirm which mode you are in before judging the result.
How resolution results affect routing
When routing looks wrong, first work out whether the domain rules or the IP rules are failing. The former usually comes down to sniffing, the latter to resolution results. Separating the two narrows the search a great deal.
policy
When you need a policy section
policy controls connection lifetime and buffer size, and is a tuning section. The defaults are enough for ordinary use, and there are three typical reasons to write it: cutting memory use, reclaiming idle connections sooner, and applying different limits to different inbounds. Below is a common example:
"policy": {
"levels": {
"0": {
"handshake": 4,
"connIdle": 300,
"uplinkOnly": 2,
"downlinkOnly": 5,
"bufferSize": 512
}
},
"system": {
"statsInboundUplink": true,
"statsInboundDownlink": true
}
}
| Field | Unit | Purpose |
|---|---|---|
| handshake | Seconds | Timeout for the handshake phase; exceeding it counts as a failure |
| connIdle | Seconds | How long a connection may sit idle before it is reclaimed |
| uplinkOnly | Seconds | How long the uplink is kept after the downlink closes |
| downlinkOnly | Seconds | How long the downlink is kept after the uplink closes |
| bufferSize | KB | Buffer size per connection; 0 means no buffer is used |
How levels works
The keys in levels are level numbers, and 0 is the default level. An inbound's settings or a user entry can specify a level field to assign that inbound or user to a particular level, which lets you apply separate limits per entry point. For everyday use, changing the 0 level alone is enough; assigning different levels to different entry points is a multi-user technique.
The system section
The system section controls the statistics switches. With statsInboundUplink and statsInboundDownlink on, the core counts inbound uplink and downlink traffic, and the graphical client's speed display depends on that data; statsOutboundUplink and statsOutboundDownlink cover the outbound direction. Turning statistics off saves a little overhead, at the cost of the traffic numbers in the interface no longer updating.
The trade-offs in tuning
Lowering connIdle reclaims idle connections sooner and reduces memory use, at the cost of rebuilding the connection next time, which shows up as one extra handshake delay. A larger bufferSize helps on high-bandwidth links, while a smaller one saves memory. handshake, uplinkOnly and downlinkOnly normally need no adjustment — only on a poor network where handshakes time out often is it worth loosening handshake.
The mobile special case
On mobile, background connections being reclaimed by the system usually has nothing to do with policy — it is the battery-saving strategy at work. The relevant handling is described in v2rayNG usage notes. Tuning only makes sense against a stable baseline, so keep a copy of the default config, compare changes one at a time, and keep a change only once you have confirmed the gain is real. Parameter combinations circulating online mostly target specific hardware and specific scenarios, and copying them wholesale does not necessarily help.
Config generation and hand-editing in the clients
How a subscription expands into a config
A subscription URL returns a block of encoded text. The client downloads it and parses it line by line, where each line describes the full parameters of one node, then translates those parameters into one entry in outbounds. The number of nodes in the subscription matches the length of the outbounds array, and the whole config is regenerated when the subscription updates. Once you follow that chain, it is clear why a node edited in the interface reverts after an update.
Share link parameters and their config fields
| Link parameter | Config field | Notes |
|---|---|---|
| add / address | vnext[].address | Server address |
| port | vnext[].port | Server port |
| id / uuid | users[].id | Identity |
| flow | users[].flow | Flow control option, VLESS only |
| net | streamSettings.network | Transport |
| tls | streamSettings.security | Security type |
| host | wsSettings.headers.Host | WebSocket request header host name |
| path | wsSettings.path | WebSocket request path |
| sni | tlsSettings.serverName | TLS domain |
| type | tlsSettings.fingerprint | Client fingerprint presentation |
Editing entry points in v2rayN
v2rayN has an edit entry point in the node list's right-click menu, and the window groups basic fields and transport fields. When you need to change a field the interface does not expose, the full config editor is available in the parameter settings. Note that a subscription update replaces the node list as a whole, so hand-edited nodes are overwritten on the next update; for adjustments you want to keep long term, save them as a standalone node or export a backup first.
Where v2rayNG gets its configs
v2rayNG gets nodes from three sources: scanning a QR code, importing a share link from the clipboard, and importing a subscription. Editing long JSON on mobile is awkward, so the usual approach is to prepare it on desktop and import it. Per-app proxy is selected app by app in the settings and only decides which apps' traffic enters VpnService — it has nothing to do with the JSON config itself, meaning per-app proxy rules never appear in config.json.
Where v2flyNG fits
v2flyNG and v2rayNG share the same config format, differing in core family. The same node parameters work on both, so when one core version handles a particular transport differently, the alternative client serves as a cross-check. Platform and version entry points for all three clients are on the Get the clients page.
When hand-editing a config is worth it
Three situations justify hand-editing: the client interface does not expose a field you need, such as custom routing rules or bufferSize; you need per-app or per-port routing; or you want to reproduce a problem with a minimal config while troubleshooting. Do the first two on desktop, then export and sync to mobile. In the third case, keep the hand-written config as short as possible, with only the sections needed to reproduce the problem.
More on subscription formats
Subscriptions come in three common forms: a base64-encoded list of links, a native JSON config, and a single share link. The differences between them and the fields most easily lost during conversion are compared item by item in Subscription format basics. When an import fails, first confirm which form the subscription returns, then decide which entry point to use.
Troubleshooting and logs
Turn on logging first
Before troubleshooting any config problem, confirm that logging is on. The log section looks like this:
"log": {
"loglevel": "warning",
"access": "",
"error": ""
}
loglevel runs from most to least verbose as debug, info, warning, error and none. Set it to debug temporarily while troubleshooting — the log then prints details such as routing match results and connection setup, which you can use to confirm whether a rule was hit; switch back to warning once the problem is solved to keep the log file from ballooning. Leaving access and error empty sends output to the console, and graphical clients redirect that output to the log panel in their interface.
Diagnose by symptom
| Symptom | Check first |
|---|---|
| Core fails to start, log reports a parse error | JSON syntax: comments, trailing commas, field name case |
| Core starts but the browser cannot open pages | Whether the inbound port and the system proxy setting match |
| Log reports a listen failure | Whether another program holds the port |
| Connection drops right after it is established | Whether transport settings match the server item by item |
| Only some applications go through the proxy | Per-app proxy settings and the scope of the system proxy |
| Domain-based routing does not take effect | Whether sniffing is on and whether rule data needs updating |
JSON syntax self-check order
On a parse failure the log gives the line number of the error. First check that line for full-width or smart punctuation — curly quotes, full-width commas and full-width brackets are the most common problem when copying a config from a web page. Next, look for a trailing comma. Finally, confirm the spelling and case of field names. If all three checks pass and it still fails, comment out half the config section by section and try again, using a binary search to narrow it down to the exact section.
Field names and versions
The core only recognizes the fields it defines. Extra fields are either ignored or throw an error, and different core versions may handle the same field differently. If a config that used to work starts erroring after a client upgrade, check the release notes first to see whether the field changed, rather than repeatedly tweaking parameters. When hand-writing a config, keep a working backup so you can roll back quickly if something breaks.
Read the log by failure stage
Errors in the log fall into stages. A dial-stage failure usually points to the address, port or transport settings; a handshake-stage failure points to the security type, certificate or public key; a resolution-stage failure points to the DNS config. The three stages produce different-looking messages, and telling them apart saves a lot of detours. Debug-level logs state the target and result of each connection attempt explicitly, so you can check them against the config item by item.
Change one parameter at a time
Change several parameters at once and test, and even if the problem goes away you will not know which change fixed it. Changing one thing at a time and verifying immediately is the fastest way to debug a config.
Subscription update failures are not a config problem
A failed subscription update has little to do with the config itself; it is more often the subscription URL, the update interval or the local network state, and the check order is listed item by item in Troubleshooting subscription update failures. The way to tell them apart is the log: a config problem errors out during core startup, while a subscription problem only affects refreshing the node list and does not stop already imported nodes from working.
Back to the main track
In the end, the log is the authority on how a config behaves. For a field this page does not cover, add it to a minimal config in the client one item at a time and watch the log change — far faster than writing a whole config and then debugging it. To walk through the basic flow again, go back to the quick start tutorial; to confirm client versions and platform entry points, see Get the clients; and for a side-by-side look at the three clients, see the comparison review.