14 KiB
sing-box Runtime Config In Zapret KVN
Scope
This document describes the sing-box JSON shape that Zapret KVN actually
builds today.
Current Runtime Model
The user-facing product model is now:
- one raw
sing-box.jsoneditor; - that raw config is the source of truth for routing, DNS, and the main sing-box-side graph in both proxy and TUN modes;
- runtime planning patches only app-owned service fragments;
- if outbound tag
proxyexists, the selected node may replace only that outbound; - if the selected node cannot be rendered as native sing-box outbound
(for example Xray
xhttp), the app keeps the same rawsing-box.jsonas the front config and automatically adds an Xray sidecar only for the proxy path.
For ordinary proxy mode the compiler removes source TUN/SOCKS/HTTP inbounds and
adds app-owned SOCKS 0.0.0.0:1390 and HTTP 0.0.0.0:1391 inbounds. Occupied
ports are moved together to the next free pair. For TUN mode it removes source
proxy inbounds and assigns a fresh TUN interface name.
This means the older RoutingSettings-driven full builder described below is
legacy architecture context, not the preferred runtime path for the current raw
editor flow.
It is intentionally narrower than the upstream manual. We only document the parts that matter for:
- TUN mode runtime generation;
- future text-editor support;
- compatibility with current
Node,RoutingSettings, andAppSettingsmodels.
Target baseline for future editor work:
sing-box 1.14.x
This matters because the local source baseline comes from the upstream
testing branch, but the app-side design can now intentionally target the
1.14.x feature set instead of treating 1.14 fields as speculative.
Upstream Root Shape
The upstream configuration root supports many top-level sections:
{
"log": {},
"dns": {},
"ntp": {},
"certificate": {},
"certificate_providers": [],
"endpoints": [],
"inbounds": [],
"outbounds": [],
"route": {},
"services": [],
"experimental": {}
}
Zapret KVN currently emits only this subset:
loginboundsoutboundsroutednsexperimental
Everything else is currently out of scope for app-generated sing-box runtime
configs.
Two Outbound Planner Outcomes
Native mode
Used when the selected node can be converted directly from Xray-style outbound
JSON into a sing-box outbound, or was imported as native sing-box JSON.
In ordinary proxy mode the inbounds section is compiled to:
[
{
"type": "socks",
"tag": "socks-in",
"listen": "0.0.0.0",
"listen_port": 1390
},
{
"type": "http",
"tag": "http-in",
"listen": "0.0.0.0",
"listen_port": 1391
}
]
In TUN mode the runtime shape is:
Runtime shape:
{
"log": {
"level": "warn",
"timestamp": true
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"interface_name": "xftun<random>",
"address": ["172.19.0.1/30"],
"auto_route": true,
"strict_route": false,
"stack": "mixed"
}
],
"outbounds": [
{
"type": "<converted node>",
"tag": "proxy",
"domain_resolver": "proxy-dns"
},
{
"type": "direct",
"tag": "direct",
"domain_resolver": "bootstrap-dns"
},
{
"type": "block",
"tag": "block"
}
],
"route": {
"auto_detect_interface": true,
"default_domain_resolver": "proxy-dns",
"final": "direct|proxy",
"rules": []
},
"dns": {
"servers": [
{
"tag": "bootstrap-dns",
"type": "<udp|tcp|tls|https>",
"server": "<bootstrap server>"
},
{
"tag": "proxy-dns",
"type": "<udp|tcp|tls|https>",
"server": "<proxy dns server>",
"detour": "proxy"
}
],
"final": "proxy-dns"
},
"experimental": {
"clash_api": {
"external_controller": "127.0.0.1:<port>"
}
}
}
Hybrid mode
Used when the selected node cannot be mapped directly by our current conversion
layer and the app must insert Xray as a sidecar. Today this happens when the
node uses Xray xhttp.
In hybrid mode, sing-box still owns the front proxy or TUN runtime, but
proxy is no longer the remote server. Instead:
sing-boxsends proxied traffic to local Xray over SOCKS;sing-boxexposes a local Shadowsocks protect inbound;- Xray uses
dialerProxyto send its own egress through that protect inbound.
The sing-box side looks like this:
{
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"interface_name": "xftun<random>",
"address": ["172.19.0.1/30"],
"auto_route": true,
"strict_route": false,
"stack": "mixed"
},
{
"type": "shadowsocks",
"tag": "tun-protect",
"listen": "127.0.0.1",
"listen_port": "<generated port>",
"method": "chacha20-ietf-poly1305",
"password": "<generated password>"
}
],
"outbounds": [
{
"type": "socks",
"tag": "proxy",
"server": "127.0.0.1",
"server_port": 11808,
"inet4_bind_address": "127.0.0.1"
},
{
"type": "direct",
"tag": "direct",
"domain_resolver": "bootstrap-dns"
},
{
"type": "block",
"tag": "block"
}
]
}
The important product consequence is that a future text editor cannot assume
that the on-screen sing-box JSON is the whole runtime truth. In hybrid mode,
the app also generates a second Xray config.
Route Pipeline
The most important behavior is not just the fields, but rule execution order.
Upstream semantics that matter
From upstream route/route.go, the router iterates rules in order. Some
actions are non-final and only mutate metadata for later rules. A final action
selects the route and stops the scan.
For our use case:
sniffis non-final;hijack-dnsis final;- classic
outboundrouting is final; rejectis final;bypassis final in the contexts where it applies.
This is why the first generated rule is sniff, not a plain route rule.
Rule order generated by the app
Native mode:
{"action": "sniff"}{"protocol": "dns", "action": "hijack-dns"}- protected-process bypass rule
- proxy-server-endpoint bypass rule
- optional LAN bypass rule
- grouped process rules
- service preset rules
- direct domain/IP rules
- block domain/IP rules
- proxy domain/IP rules
route.finalhandles the unmatched remainder
Hybrid mode:
{"action": "sniff"}{"protocol": "dns", "action": "hijack-dns"}- protected-process bypass rule
{"inbound": ["tun-protect"], "outbound": "direct"}- optional LAN bypass rule
- grouped process rules
- service preset rules
- direct domain/IP rules
- block domain/IP rules
- proxy domain/IP rules
route.finalhandles the unmatched remainder
Rule Fields We Already Use
The app already generates these upstream-supported match fields:
protocolinboundip_is_privateip_cidrdomaindomain_suffixdomain_keywordprocess_nameprocess_pathprocess_path_regex
The app already generates these actions:
- classic route action via
outboundshorthand sniffhijack-dns
Important note about classic route shorthand
Upstream route docs now describe rules with explicit action objects, for example:
{
"action": "route",
"outbound": "direct"
}
But the upstream option structs still accept the classic shorthand where the
rule only contains outbound. That is what the app generates today. This is
valid and intentionally simple.
Изменения в sing-box 1.14
Несколько полей, используемых текущим билдером, изменили статус в 1.14:
-
outboundкак shorthand в route rules (безaction) — deprecated с 1.11. Продолжает работать, но при переходе на текстовый редактор следует генерировать"action": "route", "outbound": "...". -
outboundкак match-условие в DNS rules — удалён в 1.14. Был deprecated с 1.12. Маршрутизация DNS теперь только черезdomain_resolverна outbound'ах. -
domain_resolverна outbound'ах с доменным server address — обязателен с 1.14. В текущем билдере уже проставлен, но при ручном редактировании пользователь может его пропустить. -
domain_strategyв Dial Fields — удалён в 1.14. Заменён наdomain_resolver.
Mapping From App Models To sing-box
RoutingSettings.mode and tun_default_outbound
route.final is derived as follows:
globalmode ->proxydirectmode ->direct- rule mode ->
directorproxydepending ontun_default_outbound
RoutingSettings.bypass_lan
When enabled:
{
"ip_is_private": true,
"outbound": "direct"
}
RoutingSettings.process_rules
Manual process rules are normalized into one of:
process_nameprocess_pathprocess_path_regex
The builder also groups values by action, so multiple entries may collapse into one rule containing an array of values.
RoutingSettings.process_preset_routes
Process presets are expanded to process_name arrays and grouped by action.
RoutingSettings.service_routes
Service presets are converted into domain_suffix, domain, domain_keyword,
or ip_cidr rules depending on the item prefix.
RoutingSettings.direct_domains, proxy_domains, block_domains
These lists support these local prefixes:
domain:->domain_suffixfull:->domainkeyword:->domain_keyword- plain CIDR ->
ip_cidr - plain domain ->
domain_suffix
Local compatibility rule:
geosite:*andgeoip:*are skipped because the current app intentionally aligns withsing-box >= 1.12, and the target baseline is1.14.x, where those old route fields are already gone.
Outbound Conversion Subset
The current app converts Node.outbound into a sing-box outbound only for
this subset:
vlessvmesstrojanshadowsockssockshttp
The parser stores hysteria://, hy2:// / hysteria2://, and tuic://
nodes. URI-backed Hysteria2 is relayed through the pinned official Hysteria
client; the exact source URI is its transport source of truth and is never
rebuilt from sing-box JSON. A native outbound JSON object with a top-level
type field is still passed through as-is, with only its runtime tag
replaced by the app. Consequently hysteria:// (v1), TUIC, and native
Hysteria2 JSON remain native sing-box outbounds.
The official Hysteria v2 URI parser recognizes obfs, obfs-password, sni,
insecure, pinSHA256, and ech. Unknown/vendor query parameters are retained
verbatim in the saved source and passed on unchanged; whether the pinned core
uses them is determined by that core. pinSHA256 pins the complete leaf
certificate, so it is deliberately not converted into sing-box's SPKI pin.
Because the sidecar uses lazy: true, local SOCKS readiness does not claim that
the first remote QUIC/TLS handshake has succeeded; the runtime log reports those
stages separately.
TLS support mapped today
The current conversion layer maps:
- plain
tls realityutls.fingerprintserver_namealpnallowInsecure->tls.insecure
Transport support mapped today
The current conversion layer maps:
wshttph2grpc
The current conversion layer does not map:
- Xray
xhttp
When xhttp is detected, the app switches to hybrid mode instead of producing
an invalid native outbound.
App-Managed Fields
These fields are runtime-owned today and should not silently become free-form user inputs:
inbounds[0].interface_nameinbounds[0].addressinbounds[0].auto_routeinbounds[0].strict_routeinbounds[0].stack- hybrid-only protect inbound port and password
- local loop-prevention rules
route.auto_detect_interfacedns.servers[].tagdns.servers[].detourexperimental.clash_api.external_controller- fixed outbound tags:
proxy,direct,block
This does not mean the future editor can never expose them. It means the app must treat them as managed settings with explicit ownership, not as accidental free text.
Validation Checklist For Future Editor Work
Before launching sing-box, the app should validate at least this subset:
- JSON parses successfully.
- Required top-level sections for our chosen mode exist.
- Exactly one TUN inbound exists when TUN mode is requested.
- Outbound tags required by our runtime pipeline exist:
proxydirectblock
route.finalpoints to a real outbound tag.- DNS server tags used by
default_domain_resolveranddns.finalexist. - Hybrid-only fragments are present only in hybrid mode.
- No unsupported local prefixes remain in domain lists after compilation.
- If the selected node requires hybrid mode, the native
proxyoutbound is not used by mistake. - After materialization, the final config passes upstream structural validation.
References
- Upstream docs:
docs/configuration/index.mddocs/configuration/inbound/tun.mddocs/configuration/route/index.mddocs/configuration/route/rule.mddocs/configuration/route/rule_action.mddocs/configuration/dns/index.mddocs/configuration/experimental/clash-api.mddocs/configuration/outbound/index.mddocs/configuration/outbound/vless.mddocs/configuration/outbound/vmess.mddocs/configuration/outbound/trojan.mddocs/configuration/outbound/shadowsocks.mddocs/configuration/outbound/socks.mddocs/configuration/outbound/http.mddocs/configuration/shared/tls.mddocs/configuration/shared/v2ray-transport.md
- Upstream source:
route/route.gooption/route.gooption/rule.gooption/rule_action.gooption/tun.gooption/dns.gooption/experimental.go
- Local app source:
xray_fluent/singbox_config_builder.pyxray_fluent/singbox_manager.py
- Field reference: