Harden outbound tag allocation

This commit is contained in:
ushan0v
2026-07-11 00:10:57 +03:00
parent 584f72f503
commit ef4bb76ad3
28 changed files with 1670 additions and 1177 deletions
@@ -33,6 +33,10 @@ if grep -n -E 'require\("uci"\)\.cursor|uci -q|"uci", "-q"' "$BYEDPI_RUNTIME_UC"
fi
grep -Fq 'require("core.uci")' "$BYEDPI_RUNTIME_UC" ||
fail "providers/byedpi/runtime.uc must use core.uci for runtime UCI access"
grep -Fq 'require("singbox.constants")' "$BYEDPI_RUNTIME_UC" ||
fail "providers/byedpi/runtime.uc must share the sing-box runtime tag allocator"
grep -Fq 'runtime_constants.tag(base, postfix)' "$BYEDPI_RUNTIME_UC" ||
fail "providers/byedpi/runtime.uc must allocate section tags through singbox.constants"
grep -Fq 'mode == "start-runtime"' "$BYEDPI_RUNTIME_UC" ||
fail "providers/byedpi/runtime.uc must own ByeDPI start"
@@ -126,8 +126,8 @@ assert(outbound("independent").detour == "hop-out", "independent subscription ou
assert(outbound("provider-group").detour == null, "subscription URLTest group must not receive Dial Fields");
assert(outbound("source-interface-1-out").bind_interface == "wg0", "interface binding was not preserved");
assert(outbound("source-interface-1-out").detour == null, "network interface must be excluded from section cascade");
assert(outbound("source-json-1-out").detour == null, "plain JSON outbound must be excluded from section cascade");
assert(outbound("source-json-2-out").detour == "direct-out", "explicit JSON detour was not preserved");
assert(outbound("JSON plain").detour == null, "plain JSON outbound must be excluded from section cascade");
assert(outbound("JSON explicit").detour == "direct-out", "explicit JSON detour was not preserved");
assert(outbound("source-out").detour == null, "section selector must not receive its own detour");
' "$WORK_DIR/output.json" || fail "section cascade generation"
+6
View File
@@ -47,6 +47,12 @@ assert_eq dns-server-out-1 \
assert_eq direct-out-1 \
"$(ucode "$HELPERS_UC" outbound-tag direct)" \
"reserved direct outbound tag"
assert_eq bypass-out-1 \
"$(ucode "$HELPERS_UC" outbound-tag bypass)" \
"reserved bypass outbound tag"
assert_eq tproxy6-in-1 \
"$(ucode "$HELPERS_UC" inbound-tag tproxy6)" \
"reserved IPv6 TProxy inbound tag"
assert_eq direct-1-out \
"$(ucode "$HELPERS_UC" outbound-tag direct-1)" \
"numbered direct outbound tag"
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PODKOP_LIB="$ROOT_DIR/podkop/files/usr/lib"
GENERATOR_UC="$PODKOP_LIB/singbox/generator.uc"
PARSER_UC="$PODKOP_LIB/subscription/parser.uc"
VALIDATOR_UC="$PODKOP_LIB/config/validator.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
mkdir -p "$WORK_DIR/subscriptions"
cat >"$WORK_DIR/subscriptions/proxy-subscription-1.json" <<'JSON'
{
"outbounds": [
{ "type": "socks", "tag": "germany", "server": "127.0.0.1", "server_port": 1101 },
{ "type": "socks", "tag": "germany", "server": "127.0.0.1", "server_port": 1102 },
{ "type": "socks", "tag": "bypass-out", "server": "127.0.0.1", "server_port": 1103 },
{ "type": "socks", "tag": "proxy-urltest-ut_name-out", "server": "127.0.0.1", "server_port": 1104 },
{ "type": "socks", "tag": "json-hop", "server": "127.0.0.1", "server_port": 1105 },
{ "type": "socks", "tag": "proxy-priority-pg_main-out", "server": "127.0.0.1", "server_port": 1106 },
{ "type": "socks", "tag": "dns-server", "server": "127.0.0.1", "server_port": 1107 }
]
}
JSON
printf '%s\n' 'https://duplicate.example/sub' >"$WORK_DIR/subscriptions/proxy-subscription-1.url"
: >"$WORK_DIR/subscriptions/proxy-subscription-1.user_agent"
cat >"$WORK_DIR/runtime-tags.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "log_level": "warn" },
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"subscription_urls": [ "https://duplicate.example/sub" ],
"interfaces": [ "wg0" ],
"outbound_jsons": [
"{\"type\":\"socks\",\"tag\":\"germany\",\"server\":\"127.0.0.1\",\"server_port\":1201}",
"{\"type\":\"socks\",\"tag\":\"germany\",\"server\":\"127.0.0.1\",\"server_port\":1203}",
"{\"type\":\"direct\",\"tag\":\"direct-out\"}",
"{\"type\":\"socks\",\"tag\":\"json-hop\",\"server\":\"127.0.0.1\",\"server_port\":1202}",
"{\"type\":\"selector\",\"tag\":\"json-group\",\"outbounds\":[\"proxy-json-4-out\",\"direct-out\"],\"default\":\"json-hop\"}",
"{\"type\":\"direct\",\"tag\":\"service-mixed-in\"}"
],
"domain_suffix": [ "example.org" ]
},
{
".name": "backup",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [
"{\"type\":\"socks\",\"tag\":\"json-hop\",\"server\":\"127.0.0.1\",\"server_port\":1301}"
],
"domain_suffix": [ "backup.example.org" ]
}
],
"urltest": [
{
".name": "ut_name",
".type": "urltest",
"section": "proxy",
"name": "By name",
"filter_mode": "include",
"include_outbounds": [ "germany", "wg0", "json-hop" ]
},
{
".name": "ut_regex",
".type": "urltest",
"section": "proxy",
"name": "By regex",
"filter_mode": "include",
"include_regex": [ "^germany$" ]
}
],
"priority_group": [
{
".name": "pg_main",
".type": "priority_group",
"section": "proxy",
"name": "Priority"
}
],
"priority_level": [
{
".name": "pl_main",
".type": "priority_level",
"group": "pg_main",
"name": "All",
"order": "0",
"filter_mode": "disabled"
}
]
}
JSON
runtime_config="$WORK_DIR/runtime-config.json"
mkdir -p "$runtime_config.section-cache"
TMP_SUBSCRIPTION_FOLDER="$WORK_DIR/subscriptions" \
ucode -L "$PODKOP_LIB" "$GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/runtime-tags.json" "$runtime_config" "127.0.0.1" "0"
ucode -e '
let fs = require("fs");
let config = json(fs.readfile(ARGV[0]));
let proxy_cache = json(fs.readfile(ARGV[1]));
let backup_cache = json(fs.readfile(ARGV[2]));
let by_tag = {};
let count = 0;
for (let outbound in config.outbounds || []) {
if (by_tag[outbound.tag])
die("duplicate generated outbound tag: " + outbound.tag + "\n");
by_tag[outbound.tag] = outbound;
count++;
}
if (length(keys(by_tag)) != count)
die("generated outbound tags are not globally unique\n");
for (let tag in [
"direct-out", "bypass-out", "proxy-out", "backup-out",
"germany", "germany-1", "germany-2", "germany-3",
"bypass-out-1", "dns-server-1", "service-mixed-in-1",
"proxy-urltest-ut_name-out-1", "proxy-priority-pg_main-out-1",
"proxy-interface-1-out", "direct-out-1", "json-hop", "json-hop-1", "json-hop-2",
"json-group", "proxy-urltest-ut_name-out", "proxy-urltest-ut_regex-out",
"proxy-priority-pg_main-out"
])
if (!by_tag[tag])
die("missing allocated outbound tag: " + tag + "\n");
let json_group = by_tag["json-group"];
if (length(json_group.outbounds || []) != 2 ||
json_group.outbounds[0] != "json-hop-1" || json_group.outbounds[1] != "direct-out-1")
die("JSON outbound references were not rewritten to allocated tags\n");
if (json_group.default != "json-hop-1")
die("JSON selector default was not rewritten to its allocated tag\n");
let name_group = by_tag["proxy-urltest-ut_name-out"];
let regex_group = by_tag["proxy-urltest-ut_regex-out"];
if (length(name_group.outbounds || []) != 7)
die("display-name filter did not select duplicate subscription, JSON and interface outbounds\n");
for (let tag in [ "germany", "germany-1", "germany-2", "germany-3", "proxy-interface-1-out", "json-hop", "json-hop-1" ])
if (index(name_group.outbounds, tag) < 0)
die("display-name filter is missing " + tag + "\n");
if (length(regex_group.outbounds || []) != 4)
die("display-name regex did not collapse runtime suffixes\n");
for (let tag in [ "germany", "germany-1", "germany-2", "germany-3" ])
if (index(regex_group.outbounds, tag) < 0)
die("display-name regex is missing " + tag + "\n");
let names = proxy_cache.outboundMetadata.names || {};
for (let tag in [ "germany", "germany-1", "germany-2", "germany-3" ])
if (names[tag] != "germany")
die("runtime suffix leaked into display name for " + tag + "\n");
if (names["proxy-interface-1-out"] != "wg0" ||
names["json-hop"] != "json-hop" || names["json-hop-1"] != "json-hop")
die("interface or JSON display tag metadata is missing\n");
if (names["bypass-out-1"] != "bypass-out" ||
names["dns-server-1"] != "dns-server" ||
names["service-mixed-in-1"] != "service-mixed-in" ||
names["proxy-urltest-ut_name-out-1"] != "proxy-urltest-ut_name-out" ||
names["proxy-priority-pg_main-out-1"] != "proxy-priority-pg_main-out")
die("system-tag collision suffix leaked into display metadata\n");
if ((backup_cache.outboundMetadata.names || {})["json-hop-2"] != "json-hop")
die("cross-section JSON tag allocation or display metadata is invalid\n");
' "$runtime_config" "$runtime_config.section-cache/proxy.json" \
"$runtime_config.section-cache/backup.json" || fail "runtime outbound tag allocation"
cat >"$WORK_DIR/duplicate-json-tags.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [
"{\"type\":\"direct\",\"tag\":\"same\"}",
"{\"type\":\"direct\",\"tag\":\"same\"}"
],
"domain_suffix": [ "example.org" ]
}
]
}
JSON
cat >"$WORK_DIR/tagless-json.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\"}" ],
"domain_suffix": [ "example.org" ]
}
]
}
JSON
validate_rejects() {
local fixture="$1"
local expected="$2"
local output
if output="$(PODKOP_LIB="$PODKOP_LIB" ucode -L "$PODKOP_LIB" "$VALIDATOR_UC" \
validate-runtime-fixture "$fixture" '{}' 2>/dev/null)"; then
fail "validator accepted $fixture"
fi
printf '%s\n' "$output" | grep -Fq "$expected" ||
fail "validator message for $fixture does not contain: $expected"
}
validate_rejects "$WORK_DIR/duplicate-json-tags.json" "duplicate JSON outbound tag 'same'"
validate_rejects "$WORK_DIR/tagless-json.json" "JSON outbound without a non-empty tag"
cat >"$WORK_DIR/xray-duplicates.json" <<'JSON'
[
{
"outbounds": [
{
"protocol": "socks",
"tag": "germany",
"settings": { "servers": [ { "address": "127.0.0.1", "port": 1401 } ] }
}
]
},
{
"outbounds": [
{
"protocol": "socks",
"tag": "germany",
"settings": { "servers": [ { "address": "127.0.0.1", "port": 1402 } ] }
}
]
}
]
JSON
ucode -L "$PODKOP_LIB" "$PARSER_UC" normalize-content \
"$WORK_DIR/xray-duplicates.json" "$WORK_DIR/xray-normalized.json"
ucode -e '
let fs = require("fs");
let value = json(fs.readfile(ARGV[0]));
let outbounds = value.outbounds || [];
if (length(outbounds) != 2 || outbounds[0].tag != "germany" || outbounds[1].tag != "germany-1")
die("Xray duplicate runtime tags were not allocated\n");
if (outbounds[0].remark != "germany" || outbounds[1].remark != "germany")
die("Xray runtime suffix leaked into user-visible names\n");
' "$WORK_DIR/xray-normalized.json" || fail "Xray duplicate display names"
printf 'outbound tag regression checks passed\n'
@@ -86,6 +86,10 @@ if grep -n -E 'require\("uci"\)\.cursor|uci -q|"uci", "-q"|command_output\(comma
fi
grep -Fq 'require("core.uci")' "$NFQUEUE_RUNTIME" ||
fail "providers/nfqueue/runtime.uc must use core.uci for runtime UCI access"
grep -Fq 'require("singbox.constants")' "$NFQUEUE_RUNTIME" ||
fail "providers/nfqueue/runtime.uc must share the sing-box runtime tag allocator"
grep -Fq 'runtime_constants.tag(base, postfix)' "$NFQUEUE_RUNTIME" ||
fail "providers/nfqueue/runtime.uc must allocate section tags through singbox.constants"
for mode in \
'mode == "start-runtime"' \