1434 lines
59 KiB
Bash
1434 lines
59 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" ],
|
|
"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": "http",
|
|
"tag": "provider-http",
|
|
"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"
|
|
|
|
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"
|
|
if generate_config "$WORK_DIR/manual-http-fixture.json" "$WORK_DIR/manual-http.json" \
|
|
>"$WORK_DIR/manual-http.stdout" 2>"$WORK_DIR/manual-http.stderr"; then
|
|
fail "generator should reject native HTTP connection URLs"
|
|
fi
|
|
grep -Fxq 'manual proxy link scheme is not supported by sing-box config generation yet' "$WORK_DIR/manual-http.stderr" ||
|
|
fail "native HTTP connection URL failure should identify the unsupported scheme"
|
|
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.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 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");
|
|
|
|
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-http") == null, "provider HTTP 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");
|
|
' "$WORK_DIR"
|
|
|
|
printf 'sing-box runtime checks passed\n'
|