Files
NeoTachyon/tests/sing_box_runtime.sh
T
Dushnilin 169e7f497d fix: restore mixed proxy DNS/LAN routing and eliminate service restart log flood
Fixes regression from 1.3.25 where section mixed proxy inbounds lost internet access due to missing DNS hijacking and missing private IP bypass, and suppresses noisy log warnings during service restart/update.
2026-09-12 21:51:05 +03:00

1529 lines
64 KiB
Bash

#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
CLI_UC="$TACHYON_BIN"
SING_BOX_RUNTIME_SH="$TACHYON_LIB/sing_box_runtime.sh"
LIFECYCLE_UC="$TACHYON_LIB/service/lifecycle.uc"
SINGBOX_RUNTIME_UC="$TACHYON_LIB/singbox/runtime.uc"
SINGBOX_GENERATOR_UC="$TACHYON_LIB/singbox/generator.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
[ ! -e "$SING_BOX_RUNTIME_SH" ] ||
fail "sing_box_runtime.sh shell owner must be removed"
grep -Fq '#!/usr/bin/ucode' "$TACHYON_BIN" ||
fail "tachyon entrypoint must be a direct ucode executable"
grep -Fq 'service/lifecycle.uc' "$CLI_UC" ||
fail "service/cli.uc must dispatch lifecycle orchestration through service/lifecycle.uc"
grep -Fq 'singbox/runtime.uc' "$LIFECYCLE_UC" ||
fail "service/lifecycle.uc must call singbox/runtime.uc for sing-box runtime operations"
if grep -R -n -E 'sing_box_runtime_ucode|rulesets_ucode|sing_box_configure_service|sing_box_init_config|get_service_listen_address|get_device_ipv4_address|get_download_detour_tag' "$TACHYON_BIN" "$TACHYON_LIB" --include='*.sh' >/dev/null 2>&1; then
fail "sing-box runtime shell symbols must not remain"
fi
grep -Fq 'mode == "configure-service"' "$SINGBOX_RUNTIME_UC" ||
fail "singbox/runtime.uc must own sing-box service configuration"
grep -Fq 'mode == "init-config"' "$SINGBOX_RUNTIME_UC" ||
fail "singbox/runtime.uc must own sing-box config initialization"
grep -Fq 'mode == "service-listen-address"' "$SINGBOX_RUNTIME_UC" ||
fail "singbox/runtime.uc must own service listen address detection"
grep -Fq 'mode == "service-proxy-address"' "$SINGBOX_RUNTIME_UC" ||
fail "singbox/runtime.uc must own service proxy address detection"
if grep -Fq 'log_file_lines(runtime_log, "debug", "sing-box config generator: ");' "$SINGBOX_RUNTIME_UC"; then
fail "singbox/runtime.uc must not duplicate generator failure output as debug log"
fi
if grep -Fq 'warn("unsupported: "' "$SINGBOX_GENERATOR_UC"; then
fail "singbox/generator.uc must emit concise generator failure reasons"
fi
if grep -n -E 'require\("uci"\)\.cursor|uci -q|uci", "-q"' "$SINGBOX_RUNTIME_UC" >/dev/null 2>&1; then
fail "singbox/runtime.uc must use core.uci instead of owning direct UCI cursor or CLI calls"
fi
if grep -n -E 'require\("uci"\)\.cursor|uci -q|uci", "-q"' "$SINGBOX_GENERATOR_UC" >/dev/null 2>&1; then
fail "singbox/generator.uc must use core.uci instead of owning direct UCI cursor or CLI calls"
fi
grep -Fq 'require("core.uci")' "$SINGBOX_GENERATOR_UC" ||
fail "singbox/generator.uc must import core.uci"
grep -Fq 'TACHYON_RULE_CONDITION_CACHE_DIR' "$LIFECYCLE_UC" ||
fail "service/lifecycle.uc must pass rule-condition cache dir through module environment"
if awk '
/"write-current-reload-state-clean"|"write-captured-reload-state"/ {
in_block = 1
bad = 0
}
in_block && /SECTION_CACHE_DIR/ {
bad = 1
}
in_block && /\]\);/ {
if (bad)
found_bad = 1
in_block = 0
}
END {
exit found_bad ? 0 : 1
}
' "$LIFECYCLE_UC"; then
fail "reload-state cleanup must not delete subscription section-cache"
fi
mkdir -p "$WORK_DIR/bin"
cat >"$WORK_DIR/bin/sing-box" <<'EOF_SING_BOX'
#!/bin/sh
echo 'sing-box version 1.12.25'
EOF_SING_BOX
chmod +x "$WORK_DIR/bin/sing-box"
stable_variant="$({
PATH="$WORK_DIR/bin:$PATH" \
TACHYON_LIB="$TACHYON_LIB" \
SB_VARIANT_STATE_FILE="$WORK_DIR/sing-box-variant" \
ucode -L "$TACHYON_LIB" "$SINGBOX_RUNTIME_UC" variant
} 2>"$WORK_DIR/stable-variant.stderr")" ||
fail "singbox/runtime.uc must detect a stable binary without a ucode call-order failure"
[ "$stable_variant" = "stable" ] ||
fail "singbox/runtime.uc should classify a regular sing-box binary as stable"
[ ! -s "$WORK_DIR/stable-variant.stderr" ] ||
fail "stable sing-box variant detection must not emit ucode runtime errors"
mkdir -p "$WORK_DIR/check-bin"
cat >"$WORK_DIR/check-bin/sing-box" <<'EOF_SING_BOX_CHECK'
#!/bin/sh
printf '\n' >&2
printf '%s\n' 'FATAL[0000] route: missing outbound tag' >&2
exit 42
EOF_SING_BOX_CHECK
chmod +x "$WORK_DIR/check-bin/sing-box"
: >"$WORK_DIR/invalid-config.json"
if PATH="$WORK_DIR/check-bin:$PATH" \
ucode -L "$TACHYON_LIB" "$SINGBOX_RUNTIME_UC" check-config-fixture \
"$WORK_DIR/invalid-config.json" "$WORK_DIR/sing-box-check.log" \
>"$WORK_DIR/sing-box-check.reason"; then
fail "singbox/runtime.uc check fixture should preserve a failed sing-box status"
fi
[ "$(cat "$WORK_DIR/sing-box-check.reason")" = 'FATAL[0000] route: missing outbound tag' ] ||
fail "singbox/runtime.uc must return the first non-empty sing-box check diagnostic"
grep -Fq 'Generated sing-box configuration is invalid: " + check_result.reason' "$SINGBOX_RUNTIME_UC" ||
fail "generated config failure must include the captured sing-box check diagnostic"
cat >"$WORK_DIR/generator-failure.log" <<'EOF_GENERATOR_FAILURE'
skipped incompatible subscription outbound for rule 'only_xhttp': Only XHTTP node (XHTTP requires sing-box-extended)
connection section has no usable outbounds
EOF_GENERATOR_FAILURE
generator_reason="$(ucode -L "$TACHYON_LIB" "$SINGBOX_RUNTIME_UC" generator-failure-reason-fixture \
"$WORK_DIR/generator-failure.log" 2)"
[ "$generator_reason" = 'connection section has no usable outbounds' ] ||
fail "singbox/runtime.uc must report the terminal generator error instead of an earlier warning"
mkdir -p "$WORK_DIR/etc" "$WORK_DIR/tmp"
printf 'old config\n' >"$WORK_DIR/etc/config.json"
printf 'new config\n' >"$WORK_DIR/tmp/config.json"
ucode -L "$TACHYON_LIB" "$SINGBOX_RUNTIME_UC" save-config-file-fixture \
"$WORK_DIR/tmp/config.json" "$WORK_DIR/etc/config.json"
[ "$(cat "$WORK_DIR/etc/config.json")" = "new config" ] ||
fail "singbox/runtime.uc must replace an existing config from a temp file"
[ ! -e "$WORK_DIR/tmp/config.json" ] ||
fail "singbox/runtime.uc must consume the temp config after save"
generate_config() {
local fixture="$1"
local output="$2"
local mwan3_active="${3:-0}"
mkdir -p "$output.section-cache" "$output.rulesets"
ucode -L "$TACHYON_LIB" "$TACHYON_LIB/singbox/generator.uc" generate-config-fixture \
"$fixture" "$output" "127.0.0.1" "$mwan3_active"
}
generate_config_with_subscription_cache() {
local fixture="$1"
local output="$2"
local supports_xhttp="${3:-1}"
mkdir -p "$output.section-cache" "$output.rulesets"
TMP_SUBSCRIPTION_FOLDER="$WORK_DIR/subscriptions" \
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR="$WORK_DIR/persistent-subscription-cache" \
ucode -L "$TACHYON_LIB" "$TACHYON_LIB/singbox/generator.uc" generate-config-fixture \
"$fixture" "$output" "127.0.0.1" "0" "$supports_xhttp"
}
cat >"$WORK_DIR/no-enabled-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": []
}
JSON
if ucode -L "$TACHYON_LIB" "$SINGBOX_GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/no-enabled-fixture.json" "$WORK_DIR/no-enabled.json" "127.0.0.1" "0" \
>"$WORK_DIR/no-enabled.stdout" 2>"$WORK_DIR/no-enabled.stderr"; then
fail "generator should reject a config without enabled sections"
fi
if grep -Fq 'unsupported:' "$WORK_DIR/no-enabled.stderr"; then
fail "generator failure reason must not include redundant unsupported prefix"
fi
grep -Fxq 'no enabled sections' "$WORK_DIR/no-enabled.stderr" ||
fail "generator failure reason should be concise"
cat >"$WORK_DIR/generator-uci.state" <<'EOF_UCI'
tachyon.settings=settings
tachyon.settings.dns_server=1.1.1.1
tachyon.settings.bootstrap_dns_server=1.1.1.1
tachyon.settings.config_path=/tmp/sing-box/config.json
tachyon.settings.cache_path=/tmp/sing-box/cache.db
tachyon.settings.log_level=warn
tachyon.uci_proxy=section
tachyon.uci_proxy.enabled=1
tachyon.uci_proxy.action=connection
tachyon.uci_proxy.outbound_jsons={"type":"direct"}
tachyon.uci_proxy.domain_suffix=example.org
tachyon.uci_proxy.user_domains=smart.example
EOF_UCI
TACHYON_UCI_STATE_FILE="$WORK_DIR/generator-uci.state" \
TACHYON_SECTION_CACHE_DIR="$WORK_DIR/generated-from-uci.section-cache" \
ucode -L "$TACHYON_LIB" "$SINGBOX_GENERATOR_UC" generate-config "$WORK_DIR/generated-from-uci.json" "127.0.0.1" "0"
grep -Fq '"example.org"' "$WORK_DIR/generated-from-uci.json" ||
fail "singbox/generator.uc must read section matchers from core.uci"
grep -Fq '"smart.example"' "$WORK_DIR/generated-from-uci.json" ||
fail "singbox/generator.uc must route smart-detect user_domains from core.uci"
grep -Fq '"uci_proxy-out"' "$WORK_DIR/generated-from-uci.json" ||
fail "singbox/generator.uc must read section names from core.uci"
cat >"$WORK_DIR/disabled-updates-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"list_update_enabled": "0",
"update_interval": "5m",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\"}" ],
"domain_suffix": [ "example.org" ],
"rule_set": [ "https://example.com/rules.srs" ],
"resolve_real_ip_for_routing": "1"
}
]
}
JSON
cat >"$WORK_DIR/default-updates-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"direct\"}",
"rule_set": [ "https://example.com/rules.srs" ]
}
]
}
JSON
cat >"$WORK_DIR/server-inbound-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"direct\"}",
"domain_suffix": [ "example.org" ]
}
],
"server": [
{
".name": "edge",
".type": "server",
"enabled": "1",
"protocol": "socks",
"listen": "0.0.0.0",
"listen_port": "18080",
"server_username": "tester",
"server_password": "secret",
"routing_mode": "direct"
},
{
".name": "open",
".type": "server",
"enabled": "1",
"protocol": "socks",
"listen": "0.0.0.0",
"listen_port": "18081",
"socks_auth_enabled": "0",
"server_username": "ignored",
"server_password": "ignored",
"routing_mode": "direct"
}
]
}
JSON
cat >"$WORK_DIR/runtime-matchers-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"dns_strategy": "prefer_ipv6",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "detour",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"direct\"}",
"domain_suffix": [ "detour.example" ]
},
{
".name": "bypass",
".type": "section",
"enabled": "1",
"action": "bypass",
"domain_suffix": [ "example.org" ],
"ip_cidr": [ "198.51.100.0/24" ],
"source_ip_cidr": [ "10.0.0.3/32" ]
},
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"selector_proxy_links": [ "socks5://127.0.0.1:1080" ],
"domain": "commented.example # ignored.example\nfull:exact-comment.example // ignored-full.example\nkeyword:clip",
"domain_suffix": [ "proxy.example.org", "сайт.рф", "full:пример.испытание", "keyword:пример", "regex:^сайт[.]рф$" ],
"ip_cidr": "77.111.247.0/24 #77.111.247.19\n198.51.100.0/24 //203.0.113.0/24",
"source_ip_cidr": "10.0.0.2/32 #10.0.0.99\n2001:db8::2/128 //2001:db8::99/128",
"ports_text": "80 # 443\n8080 // 8443",
"outbound_detour_enabled": "1",
"outbound_detour_section": "detour",
"mixed_proxy_enabled": "1",
"mixed_proxy_port": "19090",
"mixed_proxy_auth_enabled": "1",
"mixed_proxy_username": "user",
"mixed_proxy_password": "pass"
},
{
".name": "source_only",
".type": "section",
"enabled": "1",
"action": "connection",
"selector_proxy_links": [ "socks5://127.0.0.1:1080" ],
"source_ip_cidr": [ "192.168.1.77/32" ]
}
]
}
JSON
cat >"$WORK_DIR/dns-action.lst" <<'EOF_DNS_ACTION_LIST'
list-dns.example
198.51.100.0/24
EOF_DNS_ACTION_LIST
cat >"$WORK_DIR/dns-action-fixture.json" <<JSON
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "dns_first",
".type": "section",
"enabled": "1",
"action": "dns",
"dns_type": "dot",
"dns_server": "9.9.9.9:5353",
"dns_detour_enabled": "1",
"dns_detour_section": "detour",
"domain_suffix": [ "dns-first.example" ],
"community_lists": [ "youtube" ],
"rule_set": [ "https://example.com/domain-only.srs" ],
"domain_ip_lists": [ "$WORK_DIR/dns-action.lst" ],
"ip_cidr": [ "203.0.113.0/24" ],
"source_ip_cidr": [ "192.0.2.1/32" ],
"fully_routed_ips": [ "192.0.2.2/32" ],
"ports": [ "443" ]
},
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\"}" ],
"domain_suffix": [ "dns-first.example", "proxy-first.example" ]
},
{
".name": "dns_second",
".type": "section",
"enabled": "1",
"action": "dns",
"dns_server": "8.8.8.8",
"domain_suffix": [ "proxy-first.example" ]
},
{
".name": "detour",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\"}" ],
"domain_suffix": [ "detour.example" ]
}
]
}
JSON
cat >"$WORK_DIR/urltest-filter-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "proxy",
"outbound_jsons": [
"{\"type\":\"http\",\"tag\":\"Keep\",\"server\":\"127.0.0.1\",\"server_port\":18081}",
"{\"type\":\"http\",\"tag\":\"Drop\",\"server\":\"127.0.0.1\",\"server_port\":18082}"
],
"urltest_enabled": "1",
"urltest_filter_mode": "include",
"urltest_include_outbounds": [ "Keep" ],
"domain_suffix": [ "example.org" ]
}
]
}
JSON
cat >"$WORK_DIR/manual-http-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_server": "1.1.1.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"selector_proxy_links": [ "http://127.0.0.1:8080" ]
}
]
}
JSON
cat >"$WORK_DIR/provider-actions-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{ ".name": "zap", ".type": "section", "enabled": "1", "action": "zapret", "domain_suffix": [ "zap.example" ] },
{ ".name": "zap2", ".type": "section", "enabled": "1", "action": "zapret2", "domain_suffix": [ "zap2.example" ] },
{ ".name": "bye", ".type": "section", "enabled": "1", "action": "byedpi", "domain_suffix": [ "bye.example" ] }
]
}
JSON
cat >"$WORK_DIR/manual-transport-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "proxy",
"selector_proxy_links": [
"vless://11111111-1111-4111-8111-111111111111@127.0.0.1:443?security=tls&type=ws&path=%2Fws&host=example.com&sni=example.com&alpn=h2%2Chttp%2F1.1&encryption=mlkem768x25519plus.native.test#WS",
"vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MgV1MiLCJhZGQiOiIxMjcuMC4wLjEiLCJwb3J0IjoiNDQzIiwiaWQiOiIyMjIyMjIyMi0yMjIyLTQyMjItODIyMi0yMjIyMjIyMjIyMjIiLCJhaWQiOiI0Iiwic2N5IjoiYXV0byIsIm5ldCI6IndzIiwidHlwZSI6Im5vbmUiLCJob3N0Ijoidm1lc3MuZXhhbXBsZSIsInBhdGgiOiIvdm1lc3MiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJ2bWVzcy5leGFtcGxlIiwiYWxwbiI6ImgyLGh0dHAvMS4xIiwiZnAiOiJjaHJvbWUifQ==#VMess",
"ss://YWVzLTEyOC1nY206cGFzcw@127.0.0.1:8388#SS"
],
"domain_suffix": [ "example.org" ]
}
]
}
JSON
cat >"$WORK_DIR/vpn-interface-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "renamed_awg",
".type": "section",
"enabled": "1",
"action": "connection",
"domain_suffix": [ "vpn.example" ]
}
],
"section_interface": [
{
".name": "vpn-interface",
".type": "section_interface",
"section": "renamed_awg",
"name": "tun0",
"domain_resolver_enabled": "1",
"domain_resolver_dns_type": "doh",
"domain_resolver_dns_server": "https://dns.example/dns-query"
}
]
}
JSON
cat >"$WORK_DIR/download-via-proxy-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1",
"download_lists_via_proxy": "1",
"download_subscriptions_via_proxy": "1",
"download_components_via_proxy": "1",
"download_lists_via_proxy_section": "proxy",
"download_components_via_proxy_section": "components_proxy"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"socks\",\"server\":\"127.0.0.1\",\"server_port\":1080}",
"domain_suffix": [ "example.org" ],
"community_lists": [ "discord" ],
"rule_set": [ "https://example.com/rules.srs" ]
},
{
".name": "components_proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"socks\",\"server\":\"127.0.0.2\",\"server_port\":1080}",
"domain_suffix": [ "components.example" ]
}
]
}
JSON
cat >"$WORK_DIR/fully-routed-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"direct\"}",
"fully_routed_ips": [ "192.168.1.20/32", "192.168.1.30/32", "2001:db8::20/128", "a8:8c:3e:c9:25:05" ],
"domain_suffix": [ "example.org" ]
}
]
}
JSON
cat >"$WORK_DIR/mwan3-auto-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"direct\"}",
"domain_suffix": [ "example.org" ]
}
]
}
JSON
cat >"$WORK_DIR/mwan3-pinned-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1",
"output_network_interface": "wan2"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"direct\"}",
"domain_suffix": [ "example.org" ]
}
]
}
JSON
cat >"$WORK_DIR/p2p-isolate-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1",
"isolate_p2p": "1",
"p2p_ports": "tcp:6881,udp:6881-6889,udp"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"direct\"}",
"domain_suffix": [ "example.org" ]
}
]
}
JSON
cat >"$WORK_DIR/domain-ip.lst" <<'EOF_LIST'
example.net
203.0.113.0/24
EOF_LIST
cat >"$WORK_DIR/with-subnets.json" <<'JSON'
{
"version": 3,
"rules": [
{ "domain_suffix": [ "ruleset.example" ], "ip_cidr": [ "198.51.100.0/24" ] }
]
}
JSON
cat >"$WORK_DIR/domain-ip-rulesets-fixture.json" <<JSON
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\\"type\\":\\"direct\\"}",
"domain_suffix": [ "example.org" ],
"rule_set_with_subnets": [ "$WORK_DIR/with-subnets.json" ],
"domain_ip_lists": [ "$WORK_DIR/domain-ip.lst" ]
}
]
}
JSON
cat >"$WORK_DIR/subscription-metadata-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "proxy",
"subscription_urls": [
"https://example.com/sub-v2rayn.txt",
"https://example.com/sub.txt"
],
"subscription_url_settings": "{\"https://example.com/sub-v2rayn.txt\":{\"user_agent\":\"v2rayN\"}}",
"domain_suffix": [ "proxy.example" ]
},
{
".name": "test",
".type": "section",
"enabled": "1",
"action": "proxy",
"subscription_urls": [
"https://example.com/test-sub.txt"
],
"subscription_url_settings": "{\"https://example.com/test-sub.txt\":{\"user_agent\":\"v2rayN\"}}",
"domain_suffix": [ "test.example" ]
}
]
}
JSON
cat >"$WORK_DIR/subscription-group-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "detour",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"socks\",\"server\":\"127.0.0.1\",\"server_port\":1081}" ],
"domain_suffix": [ "detour.example" ]
},
{
".name": "grouped",
".type": "section",
"enabled": "1",
"action": "connection",
"subscription_urls": [
"https://example.com/group.json"
],
"subscription_url_settings": "{\"https://example.com/group.json\":{\"user_agent\":\"Happ\"}}",
"domain_suffix": [ "grouped.example" ]
}
]
}
JSON
cat >"$WORK_DIR/subscription-group-disabled-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "grouped",
".type": "section",
"enabled": "1",
"action": "connection",
"subscription_urls": [
"https://example.com/group.json"
],
"subscription_url_settings": "{\"https://example.com/group.json\":{\"user_agent\":\"Happ\",\"include_urltest_groups\":\"0\"}}",
"domain_suffix": [ "grouped.example" ]
}
]
}
JSON
cat >"$WORK_DIR/subscription-xhttp-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "xhttp",
".type": "section",
"enabled": "1",
"action": "connection",
"subscription_urls": [ "https://example.com/xhttp.json" ],
"subscription_url_settings": "{\"https://example.com/xhttp.json\":{\"user_agent\":\"Happ\"}}",
"domain_suffix": [ "xhttp.example" ]
}
],
"urltest": [
{
".name": "ut_xhttp",
".type": "urltest",
"section": "xhttp",
"name": "Compatible URLTest"
}
],
"priority_group": [
{
".name": "pg_xhttp",
".type": "priority_group",
"section": "xhttp",
"name": "Compatible Priority"
}
],
"priority_level": [
{
".name": "pl_xhttp",
".type": "priority_level",
"group": "pg_xhttp",
"name": "All compatible",
"order": "0",
"filter_mode": "disabled"
}
]
}
JSON
cat >"$WORK_DIR/subscription-only-xhttp-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "only_xhttp",
".type": "section",
"enabled": "1",
"action": "connection",
"subscription_urls": [ "https://example.com/only-xhttp.json" ],
"subscription_url_settings": "{\"https://example.com/only-xhttp.json\":{\"user_agent\":\"Happ\"}}",
"domain_suffix": [ "only-xhttp.example" ]
}
]
}
JSON
cat >"$WORK_DIR/manual-xhttp-fixture.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": "1.1.1.1" },
"section": [
{
".name": "manual_xhttp",
".type": "section",
"enabled": "1",
"action": "connection",
"selector_proxy_links": [
"vless://00000000-0000-4000-8000-000000000001@xhttp.example:443?encryption=none&security=tls&sni=xhttp.example&type=xhttp#Manual XHTTP"
]
}
]
}
JSON
cat >"$WORK_DIR/json-xhttp-fixture.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": "1.1.1.1" },
"section": [
{
".name": "json_xhttp",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [
"{\"type\":\"vless\",\"tag\":\"JSON XHTTP\",\"server\":\"xhttp.example\",\"server_port\":443,\"uuid\":\"00000000-0000-4000-8000-000000000002\",\"transport\":{\"type\":\"xhttp\"}}"
]
}
]
}
JSON
mkdir -p "$WORK_DIR/subscriptions" "$WORK_DIR/persistent-subscription-cache"
for source in proxy-subscription-1 proxy-subscription-2 test-subscription-1; do
cat >"$WORK_DIR/subscriptions/$source.json" <<'JSON'
{"outbounds":[{"type":"socks","tag":"subscription-proxy","server":"127.0.0.1","server_port":1080}]}
JSON
printf '%s' 'https://example.com/sub.txt' >"$WORK_DIR/subscriptions/$source.url"
cat >"$WORK_DIR/persistent-subscription-cache/$source.metadata.json" <<'JSON'
{"version":1,"title":"WolfPN"}
JSON
printf '%s' 'https://example.com/sub.txt' >"$WORK_DIR/persistent-subscription-cache/$source.url"
done
printf '%s' 'https://example.com/sub-v2rayn.txt' >"$WORK_DIR/subscriptions/proxy-subscription-1.url"
printf '%s' 'https://example.com/sub-v2rayn.txt' >"$WORK_DIR/persistent-subscription-cache/proxy-subscription-1.url"
printf '%s' 'https://example.com/test-sub.txt' >"$WORK_DIR/subscriptions/test-subscription-1.url"
printf '%s' 'https://example.com/test-sub.txt' >"$WORK_DIR/persistent-subscription-cache/test-subscription-1.url"
printf '%s' 'v2rayN' >"$WORK_DIR/subscriptions/proxy-subscription-1.user_agent"
printf '%s' 'sing-box/default' >"$WORK_DIR/subscriptions/proxy-subscription-2.user_agent"
printf '%s' 'v2rayN' >"$WORK_DIR/subscriptions/test-subscription-1.user_agent"
printf '%s' 'v2rayN' >"$WORK_DIR/persistent-subscription-cache/proxy-subscription-1.user_agent"
printf '%s' 'sing-box/default' >"$WORK_DIR/persistent-subscription-cache/proxy-subscription-2.user_agent"
printf '%s' 'v2rayN' >"$WORK_DIR/persistent-subscription-cache/test-subscription-1.user_agent"
cat >"$WORK_DIR/subscriptions/grouped-subscription-1.json" <<'JSON'
{
"outbounds": [
{
"type": "urltest",
"tag": "Provider Group",
"outbounds": [ "grouped-out", "leaf" ],
"url": "https://www.gstatic.com/generate_204",
"interval": "10m",
"tolerance": 50,
"remark": "Provider Group",
"__tachyon_allow_group": true
},
{
"type": "vless",
"tag": "grouped-out",
"server": "127.0.0.1",
"server_port": 443,
"uuid": "00000000-0000-4000-8000-000000000001",
"tls": { "enabled": true, "server_name": "example.com" },
"__tachyon_hidden": true
},
{
"type": "vless",
"tag": "leaf",
"server": "127.0.0.2",
"server_port": 443,
"uuid": "00000000-0000-4000-8000-000000000002",
"tls": { "enabled": true, "server_name": "example.org" },
"__tachyon_hidden": true
},
{
"type": "direct",
"tag": "provider-direct"
},
{
"type": "unsupported",
"tag": "provider-unsupported",
"server": "127.0.0.3",
"server_port": 8080
}
]
}
JSON
printf '%s' 'https://example.com/group.json' >"$WORK_DIR/subscriptions/grouped-subscription-1.url"
printf '%s' 'Happ' >"$WORK_DIR/subscriptions/grouped-subscription-1.user_agent"
cat >"$WORK_DIR/subscriptions/xhttp-subscription-1.json" <<'JSON'
{
"outbounds": [
{
"type": "urltest",
"tag": "Provider XHTTP Group",
"outbounds": [ "xhttp-node", "plain-node" ],
"default": "xhttp-node",
"url": "https://www.gstatic.com/generate_204",
"remark": "Provider XHTTP Group",
"__tachyon_allow_group": true
},
{
"type": "vless",
"tag": "xhttp-node",
"remark": "Moscow XHTTP",
"server": "127.0.0.10",
"server_port": 443,
"uuid": "00000000-0000-4000-8000-000000000010",
"transport": { "type": "xhttp" },
"__tachyon_hidden": true
},
{
"type": "socks",
"tag": "plain-node",
"remark": "Plain node",
"server": "127.0.0.11",
"server_port": 1080,
"__tachyon_hidden": true
},
{
"type": "socks",
"tag": "depends-on-xhttp",
"remark": "Depends on XHTTP",
"server": "127.0.0.12",
"server_port": 1080,
"detour": "xhttp-node"
},
{
"type": "socks",
"tag": "recursive-dependent",
"remark": "Recursive dependent",
"server": "127.0.0.13",
"server_port": 1080,
"detour": "depends-on-xhttp"
}
]
}
JSON
printf '%s' 'https://example.com/xhttp.json' >"$WORK_DIR/subscriptions/xhttp-subscription-1.url"
printf '%s' 'Happ' >"$WORK_DIR/subscriptions/xhttp-subscription-1.user_agent"
cat >"$WORK_DIR/subscriptions/only_xhttp-subscription-1.json" <<'JSON'
{
"outbounds": [
{
"type": "vless",
"tag": "only-xhttp-node",
"remark": "Only XHTTP node",
"server": "127.0.0.20",
"server_port": 443,
"uuid": "00000000-0000-4000-8000-000000000020",
"transport": { "type": "xhttp" }
}
]
}
JSON
printf '%s' 'https://example.com/only-xhttp.json' >"$WORK_DIR/subscriptions/only_xhttp-subscription-1.url"
printf '%s' 'Happ' >"$WORK_DIR/subscriptions/only_xhttp-subscription-1.user_agent"
cat >"$WORK_DIR/block-doh-fixture.json" <<'JSON'
{
"settings": {
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1",
"block_doh": "1"
},
"section": [
{
".name": "doh_test",
".type": "section",
"enabled": "1",
"action": "proxy",
"selector_proxy_links": [ "vless://00000000-0000-0000-0000-000000000001@127.0.0.1:443?encryption=none#dummy" ]
}
]
}
JSON
cat >"$WORK_DIR/bulk-proxy-links-fixture.json" <<'JSON'
{
"settings": {
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "sec_test",
".type": "section",
"enabled": "1",
"action": "proxy",
"selector_proxy_links_text": "vless://11111111-1111-4111-8111-111111111111@127.0.0.1:443?encryption=none#BulkOne\n# comment\nvless://22222222-2222-4222-8222-222222222222@127.0.0.1:444?encryption=none#BulkTwo",
"urltest_proxy_links_text": "vless://33333333-3333-4333-8333-333333333333@127.0.0.1:445?encryption=none#BulkUT"
}
]
}
JSON
cat >"$WORK_DIR/subscription-filter-fixture.json" <<'JSON'
{
"settings": {
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1"
},
"section": [
{
".name": "sub_test",
".type": "section",
"enabled": "1",
"action": "proxy",
"subscription_urls": [ "https://example.com/filter-sub.json" ],
"subscription_filter_include_keywords": [ "нидерланды", "германия" ],
"subscription_filter_exclude_keywords": [ "ads" ]
}
]
}
JSON
cat >"$WORK_DIR/subscriptions/sub_test-subscription-1.json" <<'JSON'
{
"outbounds": [
{ "type": "socks", "tag": "nl-fast", "remark": "Нидерланды Fast", "server": "127.0.0.1", "server_port": 1080 },
{ "type": "socks", "tag": "nl-ads", "remark": "Нидерланды Ads Server", "server": "127.0.0.2", "server_port": 1080 },
{ "type": "socks", "tag": "de-highspeed", "remark": "ГЕРМАНИЯ Premium", "server": "127.0.0.3", "server_port": 1080 },
{ "type": "socks", "tag": "us-proxy", "remark": "США Server", "server": "127.0.0.4", "server_port": 1080 }
]
}
JSON
printf '%s' 'https://example.com/filter-sub.json' >"$WORK_DIR/subscriptions/sub_test-subscription-1.url"
generate_config "$WORK_DIR/block-doh-fixture.json" "$WORK_DIR/block-doh.json"
generate_config "$WORK_DIR/bulk-proxy-links-fixture.json" "$WORK_DIR/bulk-proxy-links.json"
generate_config_with_subscription_cache "$WORK_DIR/subscription-filter-fixture.json" "$WORK_DIR/subscription-filter.json"
generate_config "$WORK_DIR/disabled-updates-fixture.json" "$WORK_DIR/disabled.json"
generate_config "$WORK_DIR/default-updates-fixture.json" "$WORK_DIR/default.json"
generate_config "$WORK_DIR/server-inbound-fixture.json" "$WORK_DIR/server.json"
generate_config "$WORK_DIR/runtime-matchers-fixture.json" "$WORK_DIR/matchers.json"
generate_config "$WORK_DIR/dns-action-fixture.json" "$WORK_DIR/dns-action.json"
generate_config "$WORK_DIR/urltest-filter-fixture.json" "$WORK_DIR/urltest.json"
generate_config "$WORK_DIR/manual-http-fixture.json" "$WORK_DIR/manual-http.json"
generate_config "$WORK_DIR/provider-actions-fixture.json" "$WORK_DIR/providers.json"
generate_config "$WORK_DIR/manual-transport-fixture.json" "$WORK_DIR/manual.json"
generate_config "$WORK_DIR/vpn-interface-fixture.json" "$WORK_DIR/vpn.json"
generate_config "$WORK_DIR/download-via-proxy-fixture.json" "$WORK_DIR/download.json"
generate_config "$WORK_DIR/fully-routed-fixture.json" "$WORK_DIR/fully-routed.json"
generate_config "$WORK_DIR/mwan3-auto-fixture.json" "$WORK_DIR/mwan3-auto.json" 1
generate_config "$WORK_DIR/mwan3-pinned-fixture.json" "$WORK_DIR/mwan3-pinned.json" 1
generate_config "$WORK_DIR/p2p-isolate-fixture.json" "$WORK_DIR/p2p-isolate.json"
generate_config "$WORK_DIR/domain-ip-rulesets-fixture.json" "$WORK_DIR/domain-ip-rulesets.json"
generate_config_with_subscription_cache "$WORK_DIR/subscription-metadata-fixture.json" "$WORK_DIR/subscription-metadata.json"
generate_config_with_subscription_cache "$WORK_DIR/subscription-group-fixture.json" "$WORK_DIR/subscription-group.json"
generate_config_with_subscription_cache "$WORK_DIR/subscription-group-disabled-fixture.json" "$WORK_DIR/subscription-group-disabled.json"
generate_config_with_subscription_cache "$WORK_DIR/subscription-xhttp-fixture.json" "$WORK_DIR/subscription-xhttp-extended.json" 1
generate_config_with_subscription_cache "$WORK_DIR/subscription-xhttp-fixture.json" "$WORK_DIR/subscription-xhttp-stable.json" 0 \
2>"$WORK_DIR/subscription-xhttp-stable.stderr"
grep -Fq "Moscow XHTTP (XHTTP requires sing-box-extended)" "$WORK_DIR/subscription-xhttp-stable.stderr" ||
fail "stable sing-box XHTTP warning should identify the incompatible subscription outbound"
grep -Fq "Recursive dependent (detour depends on unavailable outbound 'depends-on-xhttp')" "$WORK_DIR/subscription-xhttp-stable.stderr" ||
fail "stable sing-box XHTTP filtering should report recursively pruned detours"
if generate_config_with_subscription_cache "$WORK_DIR/subscription-only-xhttp-fixture.json" "$WORK_DIR/subscription-only-xhttp.json" 0 \
>"$WORK_DIR/subscription-only-xhttp.stdout" 2>"$WORK_DIR/subscription-only-xhttp.stderr"; then
fail "stable sing-box should reject a subscription section with only XHTTP outbounds"
fi
grep -Fq "Only XHTTP node (XHTTP requires sing-box-extended)" "$WORK_DIR/subscription-only-xhttp.stderr" ||
fail "all-XHTTP subscription failure should identify the incompatible outbound"
[ "$(tail -n 1 "$WORK_DIR/subscription-only-xhttp.stderr")" = 'connection section has no usable outbounds' ] ||
fail "all-XHTTP subscription failure should end with the actionable generator error"
for fixture in manual-xhttp json-xhttp; do
if ucode -L "$TACHYON_LIB" "$SINGBOX_GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/$fixture-fixture.json" "$WORK_DIR/$fixture-stable.json" "127.0.0.1" "0" "0" \
>"$WORK_DIR/$fixture-stable.stdout" 2>"$WORK_DIR/$fixture-stable.stderr"; then
fail "stable sing-box should reject explicit $fixture configuration"
fi
grep -Fq "uses XHTTP transport, but sing-box-extended is not installed" "$WORK_DIR/$fixture-stable.stderr" ||
fail "explicit $fixture XHTTP failure should explain the extended requirement"
generate_config "$WORK_DIR/$fixture-fixture.json" "$WORK_DIR/$fixture-extended.json"
done
ucode -e '
let fs = require("fs");
let dir = ARGV[0];
function cfg(name) {
return json(fs.readfile(dir + "/" + name + ".json"));
}
function as_array(value) {
if (value == null)
return [];
return type(value) == "array" ? value : [ value ];
}
function contains(values, needle) {
for (let value in as_array(values))
if (value == needle)
return true;
return false;
}
function assert(condition, message) {
if (!condition) {
warn("FAIL: ", message, "\n");
exit(1);
}
}
function no_internal_fields(value) {
if (type(value) == "array") {
for (let item in value)
if (!no_internal_fields(item))
return false;
}
else if (type(value) == "object") {
for (let key, item in value) {
if (substr(key, 0, 2) == "__")
return false;
if (!no_internal_fields(item))
return false;
}
}
return true;
}
function first_remote_ruleset(config) {
for (let rule_set in config.route.rule_set || [])
if (rule_set && rule_set.type == "remote")
return rule_set;
return null;
}
function ruleset(config, tag) {
for (let rule_set in config.route.rule_set || [])
if (rule_set && rule_set.tag == tag)
return rule_set;
return null;
}
function ruleset_url(config, url) {
for (let rule_set in config.route.rule_set || [])
if (rule_set && rule_set.url == url)
return rule_set;
return null;
}
function outbound(config, tag) {
for (let item in config.outbounds || [])
if (item && item.tag == tag)
return item;
return null;
}
function inbound(config, tag) {
for (let item in config.inbounds || [])
if (item && item.tag == tag)
return item;
return null;
}
function route_rule(config, predicate) {
for (let rule in config.route.rules || [])
if (rule && predicate(rule))
return rule;
return null;
}
function dns_rule(config, predicate) {
for (let rule in config.dns.rules || [])
if (rule && predicate(rule))
return rule;
return null;
}
function dns_rule_index(config, predicate) {
let rules = config.dns.rules || [];
for (let i = 0; i < length(rules); i++)
if (rules[i] && predicate(rules[i]))
return i;
return -1;
}
function dns_server(config, predicate) {
for (let server in config.dns.servers || [])
if (server && predicate(server))
return server;
return null;
}
let disabled = cfg("disabled");
assert(first_remote_ruleset(disabled).update_interval == "876000h", "disabled list update interval");
assert(route_rule(disabled, r => r.action == "resolve" && r.server == "dns-server") != null, "resolve rule generated");
let defaults = cfg("default");
assert(first_remote_ruleset(defaults).update_interval == "1d", "default list update interval");
assert(defaults.dns.strategy == "prefer_ipv4", "missing DNS strategy keeps the prefer_ipv4 default");
let server = cfg("server");
let socks = inbound(server, "server-edge-in");
assert(socks && socks.type == "socks" && socks.listen_port == 18080, "server socks inbound");
assert(socks.users && socks.users[0].username == "tester", "legacy server socks auth defaults to enabled");
let open_socks = inbound(server, "server-open-in");
assert(open_socks && open_socks.type == "socks" && open_socks.listen_port == 18081, "server open socks inbound");
assert(open_socks.users == null, "disabled server socks auth omits users");
assert(route_rule(server, r => r.inbound == "server-edge-in" && r.outbound == "direct-out") != null, "server direct route");
let matchers = cfg("matchers");
assert(matchers.dns.strategy == "prefer_ipv6", "configured DNS strategy is generated");
assert(dns_server(matchers, r => r.tag == "fakeip-server" && r.inet6_range == "fc00::/18") != null, "FakeIP IPv6 range");
assert(inbound(matchers, "tproxy6-in") != null, "IPv6 TProxy inbound");
assert(inbound(matchers, "tproxy6-in").listen == "::1", "IPv6 TProxy listen address");
assert(route_rule(matchers, r => r.action == "reject" && r.ip_version == 6) == null, "IPv6 traffic is not rejected globally");
assert(outbound(matchers, "bypass-out").type == "direct", "bypass fallback outbound");
assert(outbound(matchers, "proxy-1-out").detour == "detour-out", "connection URL outbound detour");
let mixed = inbound(matchers, "proxy-mixed-in");
assert(mixed && mixed.listen_port == 19090 && mixed.users[0].username == "user", "mixed inbound auth");
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.action == "hijack-dns" && r.port == 53) != null, "mixed inbound hijack-dns port 53");
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.action == "hijack-dns" && r.protocol == "dns") != null, "mixed inbound hijack-dns protocol dns");
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.ip_is_private == true && r.outbound == "direct-out") != null, "mixed inbound private IP bypass");
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.outbound == "proxy-out") != null, "mixed inbound route");
assert(dns_rule(matchers, r => contains(r.domain_suffix, "example.org")).server == "dns-server", "bypass domain real DNS rule");
assert(dns_rule(matchers, r => contains(r.domain_suffix, "proxy.example.org")).server == "fakeip-server", "proxy domain FakeIP DNS rule");
assert(dns_rule(matchers, r => contains(r.domain_suffix, "xn--80aswg.xn--p1ai")).server == "fakeip-server", "IDN suffix converted for DNS rule");
assert(dns_rule(matchers, r => contains(r.domain, "xn--e1afmkfd.xn--80akhbyknj4f")).server == "fakeip-server", "IDN full domain converted for DNS rule");
assert(dns_rule(matchers, r => contains(r.domain_keyword, "xn--e1afmkfd")).server == "fakeip-server", "IDN keyword converted for DNS rule");
assert(dns_rule(matchers, r => contains(r.domain_regex, "^xn--80aswg[.]xn--p1ai$")).server == "fakeip-server", "IDN regex converted for DNS rule");
assert(dns_rule(matchers, r => contains(r.domain_suffix, "commented.example")).server == "fakeip-server", "commented domain value generated");
assert(dns_rule(matchers, r => contains(r.domain, "exact-comment.example")).server == "fakeip-server", "commented full domain generated");
assert(dns_rule(matchers, r => contains(r.domain_keyword, "clip")).server == "fakeip-server", "commented keyword generated");
assert(index(sprintf("%J", matchers), "ignored.example") < 0 && index(sprintf("%J", matchers), "ignored-full.example") < 0, "domain comments excluded from generated config");
assert(route_rule(matchers, r => r.outbound == "bypass-out" && contains(r.domain_suffix, "example.org") && contains(r.source_ip_cidr, "10.0.0.3/32")) != null, "bypass fallback route");
assert(route_rule(matchers, r => r.outbound == "proxy-out" && contains(r.domain_suffix, "xn--80aswg.xn--p1ai")) != null, "IDN suffix converted for route rule");
assert(route_rule(matchers, r => r.outbound == "proxy-out" && contains(r.domain, "xn--e1afmkfd.xn--80akhbyknj4f")) != null, "IDN full domain converted for route rule");
assert(route_rule(matchers, r => r.outbound == "proxy-out" && contains(r.domain_keyword, "xn--e1afmkfd")) != null, "IDN keyword converted for route rule");
assert(route_rule(matchers, r => r.outbound == "proxy-out" && contains(r.domain_regex, "^xn--80aswg[.]xn--p1ai$")) != null, "IDN regex converted for route rule");
assert(route_rule(matchers, r => contains(r.inbound, "tproxy-in") && contains(r.inbound, "tproxy6-in") && r.outbound == "proxy-out") != null, "section route dual tproxy inbound");
assert(route_rule(matchers, r => r.outbound == "direct-out" && contains(r.source_ip_cidr, "192.168.1.5/32")) == null, "routing excluded source removed");
assert(route_rule(matchers, r => r.outbound == "proxy-out" && contains(r.source_ip_cidr, "10.0.0.2/32") && contains(r.source_ip_cidr, "2001:db8::2/128")) != null, "source_ip_cidr matcher");
assert(route_rule(matchers, r => r.outbound == "source_only-out" && contains(r.source_ip_cidr, "192.168.1.77/32")) != null, "source_only route rule generated");
let commented_ip_rule = route_rule(matchers, r => r.outbound == "proxy-out" && contains(r.ip_cidr, "77.111.247.0/24") && contains(r.ip_cidr, "198.51.100.0/24"));
assert(commented_ip_rule != null, "comment-aware ip_cidr matcher");
assert(contains(commented_ip_rule.port, 80) && contains(commented_ip_rule.port, 8080), "comment-aware port matcher");
let commented_rule_json = sprintf("%J", commented_ip_rule);
assert(index(commented_rule_json, "#77.111.247.19") < 0 && index(commented_rule_json, "203.0.113.0/24") < 0 && index(commented_rule_json, "10.0.0.99") < 0 && index(commented_rule_json, "2001:db8::99/128") < 0, "IP comments excluded from generated config");
assert(index(commented_rule_json, "443") < 0 && index(commented_rule_json, "8443") < 0, "port comments excluded from generated config");
let dns_action = cfg("dns-action");
let dns_first_server = dns_server(dns_action, r => r.tag == "dns_first-dns-server");
assert(dns_first_server && dns_first_server.type == "tls" && dns_first_server.server == "9.9.9.9" && dns_first_server.server_port == 5353, "DNS action protocol and server");
assert(dns_first_server.detour == "detour-out", "DNS action detour");
assert(dns_server(dns_action, r => r.tag == "dns_second-dns-server" && r.server == "8.8.8.8") != null, "second DNS action server");
assert(outbound(dns_action, "dns_first-out") == null && outbound(dns_action, "dns_second-out") == null, "DNS actions do not create outbounds");
assert(route_rule(dns_action, r => contains(r.domain_suffix, "dns-first.example") && r.outbound != null) != null, "proxy still owns its route rule");
assert(route_rule(dns_action, r => contains(r.domain_suffix, "list-dns.example")) == null, "DNS action does not create route rules");
let dns_first_index = dns_rule_index(dns_action, r => contains(r.domain_suffix, "dns-first.example") && r.server == "dns_first-dns-server");
let proxy_after_dns_index = dns_rule_index(dns_action, r => contains(r.domain_suffix, "dns-first.example") && r.server == "fakeip-server");
assert(dns_first_index >= 0 && proxy_after_dns_index > dns_first_index, "higher DNS action wins over lower proxy DNS rule");
let proxy_first_index = dns_rule_index(dns_action, r => contains(r.domain_suffix, "proxy-first.example") && r.server == "fakeip-server");
let dns_after_proxy_index = dns_rule_index(dns_action, r => contains(r.domain_suffix, "proxy-first.example") && r.server == "dns_second-dns-server");
assert(proxy_first_index >= 0 && dns_after_proxy_index > proxy_first_index, "higher proxy DNS rule wins over lower DNS action");
assert(dns_rule(dns_action, r => contains(r.rule_set, "dns_first-youtube-community-ruleset") && r.server == "dns_first-dns-server") != null, "DNS action built-in domain rule set");
let dns_custom_ruleset = ruleset_url(dns_action, "https://example.com/domain-only.srs");
assert(dns_custom_ruleset && dns_rule(dns_action, r => contains(r.rule_set, dns_custom_ruleset.tag) && r.server == "dns_first-dns-server") != null, "DNS action custom domain rule set");
let dns_list = json(fs.readfile(dir + "/dns-action.json.rulesets/dns_first-lists-ruleset.json"));
assert(index(sprintf("%J", dns_list), "list-dns.example") >= 0, "DNS action imports domains from plain lists");
assert(index(sprintf("%J", dns_list), "198.51.100.0/24") < 0, "DNS action drops subnets from plain lists");
let urltest = cfg("urltest");
let urltest_out = outbound(urltest, "proxy-urltest-out");
assert(urltest_out && length(urltest_out.outbounds) == 1 && urltest_out.outbounds[0] == "Keep", "URLTest include filter");
assert(outbound(urltest, "Keep").type == "http", "HTTP JSON outbound remains supported");
assert(outbound(urltest, "proxy-out").default == "proxy-urltest-out", "selector defaults to URLTest");
let manual_http = cfg("manual-http");
let manual_http_out = outbound(manual_http, "proxy-1-out");
assert(manual_http_out && manual_http_out.type == "http" && manual_http_out.server == "127.0.0.1" && manual_http_out.server_port == 8080, "manual HTTP outbound supported");
assert(route_rule(manual_http, r => r.action == "reject" && contains(r.network, "udp")) != null, "HTTP outbound generates UDP reject rule");
let providers = cfg("providers");
assert(outbound(providers, "zap-out").routing_mark == 0x01000001, "Zapret mark");
assert(outbound(providers, "zap2-out").routing_mark == 0x02000001, "Zapret2 mark");
assert(outbound(providers, "bye-out").type == "socks" && outbound(providers, "bye-out").server_port == 1080, "ByeDPI outbound");
let manual = cfg("manual");
let vless = outbound(manual, "proxy-1-out");
assert(vless.transport.type == "ws" && vless.transport.path == "/ws", "manual VLESS WS transport");
assert(vless.tls.server_name == "example.com", "manual VLESS TLS");
assert(length(vless.tls.alpn) == 1 && vless.tls.alpn[0] == "http/1.1", "manual VLESS WS ALPN normalized");
assert(vless.encryption == "mlkem768x25519plus.native.test", "manual VLESS encryption preserved");
let vmess = outbound(manual, "proxy-2-out");
assert(vmess.type == "vmess" && vmess.uuid == "22222222-2222-4222-8222-222222222222", "manual VMess link");
assert(vmess.alter_id == 4 && vmess.security == "auto", "manual VMess alter_id and security");
assert(vmess.transport.type == "ws" && vmess.transport.path == "/vmess", "manual VMess WS transport");
assert(vmess.transport.headers.Host == "vmess.example", "manual VMess WS Host header");
assert(vmess.tls.server_name == "vmess.example", "manual VMess TLS SNI");
assert(length(vmess.tls.alpn) == 1 && vmess.tls.alpn[0] == "http/1.1", "manual VMess WS ALPN normalized");
assert(vmess.tls.utls.fingerprint == "chrome", "manual VMess fingerprint");
assert(outbound(manual, "proxy-3-out").type == "shadowsocks", "manual Shadowsocks link");
let vpn = cfg("vpn");
assert(outbound(vpn, "renamed_awg-interface-1-out").bind_interface == "tun0", "renamed VPN interface outbound");
assert(outbound(vpn, "renamed_awg-interface-1-out").domain_resolver == "renamed_awg-interface-1-domain-resolver", "renamed VPN domain resolver tag");
assert(dns_server(vpn, r => r.tag == "renamed_awg-interface-1-domain-resolver") != null, "renamed VPN domain resolver DNS server");
assert(outbound(vpn, "renamed_awg-interface-1-out").detour == null, "interface outbound must not receive a detour");
assert(route_rule(vpn, r => r.outbound == "renamed_awg-out" && contains(r.domain_suffix, "vpn.example")) != null, "renamed VPN custom domain route");
assert(dns_rule(vpn, r => contains(r.domain_suffix, "vpn.example")) != null, "renamed VPN custom domain DNS rule");
let download = cfg("download");
assert(inbound(download, "service-mixed-in") != null, "service mixed inbound");
assert(route_rule(download, r => r.inbound == "service-mixed-in" && r.outbound == "proxy-out") != null, "service mixed route");
assert(inbound(download, "service-components-in") != null, "components service mixed inbound");
assert(route_rule(download, r => r.inbound == "service-components-in" && r.outbound == "components_proxy-out") != null, "components service mixed route");
assert(ruleset(download, "proxy-discord-community-ruleset").download_detour == "proxy-out", "download_detour on community ruleset");
assert(ruleset_url(download, "https://example.com/rules.srs").download_detour == "proxy-out", "download_detour on custom remote ruleset");
let fully = cfg("fully-routed");
assert(route_rule(fully, r => contains(r.inbound, "tproxy-in") && contains(r.inbound, "tproxy6-in") && contains(r.source_ip_cidr, "192.168.1.20/32") && contains(r.source_ip_cidr, "192.168.1.30/32") && contains(r.source_ip_cidr, "2001:db8::20/128")) != null, "fully routed IP route");
assert(route_rule(fully, r => contains(r.source_ip_cidr, "a8:8c:3e:c9:25:05")) == null, "raw MAC address never present in source_ip_cidr");
let mwan3_auto = cfg("mwan3-auto");
assert(mwan3_auto.route.auto_detect_interface === false, "mwan3 disables auto_detect_interface");
assert(mwan3_auto.route.default_interface == null, "mwan3 without pinned interface does not set default_interface");
let mwan3_pinned = cfg("mwan3-pinned");
assert(mwan3_pinned.route.auto_detect_interface === false, "mwan3 pinned interface disables auto_detect_interface");
assert(mwan3_pinned.route.default_interface == "wan2", "mwan3 pinned interface is preserved");
let p2p_isolate = cfg("p2p-isolate");
assert(route_rule(p2p_isolate, r => contains(r.protocol, "bittorrent") && r.outbound == "direct-out") != null, "p2p isolation sniffer rule");
assert(route_rule(p2p_isolate, r => contains(r.protocol, "tcp") && r.source_port == 6881 && r.outbound == "direct-out") != null, "p2p client tcp port rule");
assert(route_rule(p2p_isolate, r => contains(r.protocol, "udp") && contains(r.source_port_range, "6881:6889") && r.outbound == "direct-out") != null, "p2p client udp port range rule");
assert(route_rule(p2p_isolate, r => contains(r.protocol, "udp") && r.source_port == null && r.source_port_range == null && r.outbound == "direct-out") != null, "p2p protocol-wide direct rule");
let lists = cfg("domain-ip-rulesets");
assert(no_internal_fields(lists), "internal runtime fields stripped from generated config");
let local_ruleset = null;
for (let rule_set in lists.route.rule_set || [])
if (rule_set.tag == "proxy-lists-ruleset")
local_ruleset = rule_set;
assert(local_ruleset && local_ruleset.type == "local" && local_ruleset.format == "source", "domain_ip_lists local ruleset");
assert(route_rule(lists, r => contains(r.rule_set, "proxy-lists-ruleset") && length(as_array(r.rule_set)) >= 2) != null, "domain_ip_lists and rule_set_with_subnets route");
assert(dns_rule(lists, r => contains(r.rule_set, "proxy-lists-ruleset")) != null, "domain_ip_lists fakeip DNS rule");
let generated_list = json(fs.readfile(dir + "/domain-ip-rulesets.json.rulesets/proxy-lists-ruleset.json"));
let has_domain = false;
let has_ip = false;
for (let rule in generated_list.rules || []) {
if (contains(rule.domain_suffix, "example.net"))
has_domain = true;
if (contains(rule.ip_cidr, "203.0.113.0/24"))
has_ip = true;
}
assert(has_domain && has_ip, "generated domain/IP source ruleset contents");
let subscription_group = cfg("subscription-group");
let provider_group = outbound(subscription_group, "Provider Group");
assert(provider_group && provider_group.type == "urltest", "provider URLTest group imported");
assert(length(provider_group.outbounds) == 2, "provider URLTest group keeps leaf references");
assert(provider_group.outbounds[0] == "grouped-out-1", "provider URLTest group leaf reference retagged");
assert(provider_group.outbounds[1] == "leaf", "provider URLTest group second leaf reference kept");
assert(provider_group.detour == null, "provider URLTest group does not receive outbound detour");
let grouped_leaf = outbound(subscription_group, "grouped-out-1");
let second_leaf = outbound(subscription_group, "leaf");
assert(grouped_leaf && grouped_leaf.detour == null, "hidden subscription leaf does not receive connection URL detour");
assert(second_leaf && second_leaf.detour == null, "second hidden subscription leaf does not receive connection URL detour");
assert(outbound(subscription_group, "provider-direct") == null, "provider direct outbound skipped");
assert(outbound(subscription_group, "provider-unsupported") == null, "provider unsupported outbound skipped");
let grouped_selector = outbound(subscription_group, "grouped-out");
assert(grouped_selector && length(grouped_selector.outbounds) == 1 && grouped_selector.outbounds[0] == "Provider Group", "selector exposes provider group only");
let grouped_state = cfg("subscription-group.json.section-cache/grouped");
assert(grouped_state.outboundMetadata.names["Provider Group"] == "Provider Group", "provider group metadata visible");
assert(grouped_state.outboundMetadata.names["grouped-out-1"] == "grouped-out", "hidden leaf metadata visible");
assert(length(grouped_state.urltestGroups["Provider Group"].outbounds) == 2, "provider group membership cached");
assert(grouped_state.urltestGroups["Provider Group"].outbounds[0] == "grouped-out-1", "provider group membership retagged");
assert(grouped_state.linkRefs == null, "section cache omits source link refs");
assert(index(grouped_state.links["grouped-out-1"], "vless://") == 0, "hidden leaf caches its direct proxy link");
assert(index(grouped_state.links["leaf"], "vless://") == 0, "second hidden leaf caches its direct proxy link");
let subscription_group_disabled = cfg("subscription-group-disabled");
assert(outbound(subscription_group_disabled, "Provider Group") == null, "provider URLTest group skipped when import is disabled");
let disabled_grouped_leaf = outbound(subscription_group_disabled, "grouped-out-1");
let disabled_second_leaf = outbound(subscription_group_disabled, "leaf");
assert(disabled_grouped_leaf && disabled_second_leaf, "URLTest group leaf outbounds kept when import is disabled");
let disabled_grouped_selector = outbound(subscription_group_disabled, "grouped-out");
assert(disabled_grouped_selector && contains(disabled_grouped_selector.outbounds, "grouped-out-1") && contains(disabled_grouped_selector.outbounds, "leaf"), "selector exposes URLTest group leaves when import is disabled");
let disabled_grouped_state = cfg("subscription-group-disabled.json.section-cache/grouped");
assert(disabled_grouped_state.urltestGroups["Provider Group"] == null, "skipped provider group is not cached as URLTest");
assert(disabled_grouped_state.linkRefs == null, "disabled group cache omits source link refs");
assert(index(disabled_grouped_state.links["grouped-out-1"], "vless://") == 0, "visible leaf caches its direct proxy link when group import is disabled");
let xhttp_stable = cfg("subscription-xhttp-stable");
assert(outbound(xhttp_stable, "xhttp-node") == null, "stable sing-box excludes XHTTP subscription leaf");
assert(outbound(xhttp_stable, "depends-on-xhttp") == null, "stable sing-box excludes outbound detouring through XHTTP");
assert(outbound(xhttp_stable, "recursive-dependent") == null, "stable sing-box recursively excludes XHTTP-dependent detours");
let stable_provider_group = outbound(xhttp_stable, "Provider XHTTP Group");
assert(stable_provider_group && length(stable_provider_group.outbounds) == 1 && stable_provider_group.outbounds[0] == "plain-node", "provider group removes XHTTP member");
assert(stable_provider_group.default == null, "stable sing-box provider URLTest removes unsupported default field");
let stable_urltest = outbound(xhttp_stable, "xhttp-urltest-ut_xhttp-out");
assert(stable_urltest && length(stable_urltest.outbounds) == 1 && stable_urltest.outbounds[0] == "plain-node", "URLTest receives only compatible subscription leaves");
let stable_priority = outbound(xhttp_stable, "xhttp-priority-pg_xhttp-out");
assert(stable_priority && length(stable_priority.outbounds) == 1 && stable_priority.outbounds[0] == "plain-node", "Priority receives only compatible subscription leaves");
let xhttp_extended = cfg("subscription-xhttp-extended");
assert(outbound(xhttp_extended, "xhttp-node") != null, "extended sing-box keeps XHTTP subscription leaf");
assert(outbound(xhttp_extended, "Provider XHTTP Group").default == null, "extended sing-box provider URLTest also removes unsupported default field");
assert(outbound(xhttp_extended, "depends-on-xhttp").detour == "xhttp-node", "extended sing-box keeps XHTTP detour chain");
assert(outbound(xhttp_extended, "recursive-dependent").detour == "depends-on-xhttp", "extended sing-box keeps recursive detour chain");
let proxy_cache = json(fs.readfile(dir + "/subscription-metadata.json.section-cache/proxy.json"));
let test_cache = json(fs.readfile(dir + "/subscription-metadata.json.section-cache/test.json"));
let proxy_share_link_cached = false;
for (let tag, link in proxy_cache.links || {})
if (link == "socks5://127.0.0.1:1080#subscription-proxy")
proxy_share_link_cached = true;
assert(proxy_share_link_cached, "subscription share link cached for instant dashboard copy");
assert(proxy_cache.linkRefs == null && test_cache.linkRefs == null, "subscription caches contain direct links without source refs");
let proxy_metadata = as_array(proxy_cache.subscriptionMetadata);
let test_metadata = as_array(test_cache.subscriptionMetadata);
assert(length(proxy_metadata) == 2, "duplicate subscription URL metadata kept for both proxy sources");
assert(proxy_metadata[0].sourceSection == "proxy-subscription-1" && proxy_metadata[0].title == "WolfPN", "proxy source 1 metadata marker");
assert(proxy_metadata[1].sourceSection == "proxy-subscription-2" && proxy_metadata[1].title == "WolfPN", "proxy source 2 metadata marker");
assert(length(test_metadata) == 1, "same subscription URL metadata kept for second section");
assert(test_metadata[0].sourceSection == "test-subscription-1" && test_metadata[0].title == "WolfPN", "test source metadata marker");
let doh_cfg = cfg("block-doh");
assert(route_rule(doh_cfg, r => r.action == "reject" && contains(r.inbound, "tproxy-in") && contains(r.ip_cidr, "1.1.1.1/32")) != null, "block_doh IPv4 reject rule present");
assert(route_rule(doh_cfg, r => r.action == "reject" && contains(r.inbound, "tproxy-in") && contains(r.ip_cidr, "2606:4700:4700::1111/128")) != null, "block_doh IPv6 reject rule present");
let bulk_cfg = cfg("bulk-proxy-links");
assert(outbound(bulk_cfg, "sec_test-1-out") != null, "bulk selector proxy link 1 created");
assert(outbound(bulk_cfg, "sec_test-2-out") != null, "bulk selector proxy link 2 created");
assert(outbound(bulk_cfg, "sec_test-3-out") != null, "bulk urltest proxy link created");
assert(outbound(bulk_cfg, "sec_test-urltest-out") != null, "bulk urltest group created");
let filter_cfg = cfg("subscription-filter");
assert(outbound(filter_cfg, "nl-fast") != null, "include keyword match preserves node");
assert(outbound(filter_cfg, "de-highspeed") != null, "case-insensitive Cyrillic include keyword match preserves node");
assert(outbound(filter_cfg, "nl-ads") == null, "exclude keyword drops node even if included");
assert(outbound(filter_cfg, "us-proxy") == null, "unmatched include keyword drops node");
' "$WORK_DIR"
printf 'sing-box runtime checks passed\n'