fix(singbox): separate dns query and response rulesets for sing-box 1.14+ compatibility
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$TEST_DIR/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
GENERATOR_UC="$TACHYON_LIB/singbox/generator.uc"
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 1. Test sing-box 1.14+ with domain-only list (youtube)
|
||||
# Should only generate query rule, NO evaluate rule, NO match_response
|
||||
printf 'v1.14.1\n' > "$WORK_DIR/sb_v14"
|
||||
|
||||
cat > "$WORK_DIR/fixture_domain_only.json" << 'EOF'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"enabled": "1",
|
||||
"dns_type": "udp",
|
||||
"dns_server": "1.1.1.1",
|
||||
"service_listen_address": "127.0.0.1"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "sec_yt",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_yt-out\"}" ],
|
||||
"community_lists": [ "youtube" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
output_dom="$WORK_DIR/out_dom.json"
|
||||
mkdir -p "$output_dom.section-cache" "$output_dom.rulesets"
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v14" \
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/fixture_domain_only.json" "$output_dom" "127.0.0.1" "0" "1"
|
||||
|
||||
# Verify query rule exists and NO evaluate rule exists
|
||||
if grep -Fq '"action": "evaluate"' "$output_dom"; then
|
||||
fail "domain-only list must NOT generate evaluate DNS rule"
|
||||
fi
|
||||
if grep -Fq '"match_response"' "$output_dom"; then
|
||||
fail "domain-only list must NOT have match_response"
|
||||
fi
|
||||
grep -Fq '"action": "route"' "$output_dom" || fail "domain-only list must generate route DNS rule"
|
||||
|
||||
# 2. Test sing-box 1.14+ with mixed list (telegram)
|
||||
# Should generate evaluate rule and match_response: true on response rule
|
||||
cat > "$WORK_DIR/fixture_mixed.json" << 'EOF'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"enabled": "1",
|
||||
"dns_type": "udp",
|
||||
"dns_server": "1.1.1.1",
|
||||
"service_listen_address": "127.0.0.1"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "sec_tg",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_tg-out\"}" ],
|
||||
"community_lists": [ "telegram" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
output_mixed="$WORK_DIR/out_mixed.json"
|
||||
mkdir -p "$output_mixed.section-cache" "$output_mixed.rulesets"
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v14" \
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/fixture_mixed.json" "$output_mixed" "127.0.0.1" "0" "1"
|
||||
|
||||
grep -Fq '"action": "evaluate"' "$output_mixed" || fail "mixed list must generate evaluate DNS rule"
|
||||
grep -Fq '"match_response": true' "$output_mixed" || fail "mixed list must have match_response: true"
|
||||
|
||||
# 3. Test sing-box 1.14+ with BOTH domain-only (russia_inside) and mixed (telegram)
|
||||
# This is the exact reproduction case from user bug report
|
||||
cat > "$WORK_DIR/fixture_user_case.json" << 'EOF'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"enabled": "1",
|
||||
"dns_type": "udp",
|
||||
"dns_server": "1.1.1.1",
|
||||
"service_listen_address": "127.0.0.1"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "Main",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"Main-out\"}" ],
|
||||
"community_lists": [ "russia_inside", "telegram" ],
|
||||
"community_subnets": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
output_user="$WORK_DIR/out_user.json"
|
||||
mkdir -p "$output_user.section-cache" "$output_user.rulesets"
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v14" \
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/fixture_user_case.json" "$output_user" "127.0.0.1" "0" "1"
|
||||
|
||||
# Verify structure with ucode:
|
||||
# Check that query rule has russia_inside and NOT telegram
|
||||
# Check that response rule has telegram and match_response: true
|
||||
ucode -L "$TACHYON_LIB" -e '
|
||||
let common = require("core.common");
|
||||
let fs = require("fs");
|
||||
let cfg = common.read_json_file("'"$output_user"'");
|
||||
if (!cfg || !cfg.dns || !cfg.dns.rules) {
|
||||
warn("Failed to read generated config\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let found_https_reject = false;
|
||||
let query_rule = null;
|
||||
let eval_rule = null;
|
||||
let resp_rule = null;
|
||||
|
||||
for (let r in cfg.dns.rules) {
|
||||
if (r.action == "reject" && r.query_type && r.query_type[0] == "HTTPS")
|
||||
found_https_reject = true;
|
||||
if (r.action == "route" && !r.match_response && r.rule_set) {
|
||||
let rs = type(r.rule_set) == "array" ? r.rule_set : [ r.rule_set ];
|
||||
for (let s in rs) {
|
||||
if (index(s, "russia_inside") >= 0)
|
||||
query_rule = r;
|
||||
}
|
||||
}
|
||||
if (r.action == "evaluate")
|
||||
eval_rule = r;
|
||||
if (r.action == "route" && r.match_response === true && r.rule_set) {
|
||||
let rs = type(r.rule_set) == "array" ? r.rule_set : [ r.rule_set ];
|
||||
for (let s in rs) {
|
||||
if (index(s, "telegram") >= 0)
|
||||
resp_rule = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_https_reject) {
|
||||
warn("HTTPS reject rule missing\n");
|
||||
exit(2);
|
||||
}
|
||||
if (!query_rule) {
|
||||
warn("query rule for russia_inside missing\n");
|
||||
exit(3);
|
||||
}
|
||||
if (!eval_rule) {
|
||||
warn("eval_rule missing for telegram response\n");
|
||||
exit(4);
|
||||
}
|
||||
if (!resp_rule) {
|
||||
warn("resp_rule missing for telegram\n");
|
||||
exit(5);
|
||||
}
|
||||
|
||||
// Ensure telegram is NOT in query_rule
|
||||
let qrs = type(query_rule.rule_set) == "array" ? query_rule.rule_set : [ query_rule.rule_set ];
|
||||
for (let s in qrs) {
|
||||
if (index(s, "telegram") >= 0) {
|
||||
warn("telegram leaked into query_rule!\n");
|
||||
exit(6);
|
||||
}
|
||||
}
|
||||
' || fail "user reproduction verification failed"
|
||||
|
||||
# 4. Test source-based routing and excluded_ips with mixed ruleset on sing-box 1.14+
|
||||
cat > "$WORK_DIR/fixture_scoped.json" << 'EOF'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"enabled": "1",
|
||||
"dns_type": "udp",
|
||||
"dns_server": "1.1.1.1",
|
||||
"service_listen_address": "127.0.0.1"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "sec_scoped",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_scoped-out\"}" ],
|
||||
"source_ip_cidr": [ "192.168.1.50/32" ],
|
||||
"excluded_ips": [ "192.168.1.200/32" ],
|
||||
"community_lists": [ "telegram" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
output_scoped="$WORK_DIR/out_scoped.json"
|
||||
mkdir -p "$output_scoped.section-cache" "$output_scoped.rulesets"
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v14" \
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/fixture_scoped.json" "$output_scoped" "127.0.0.1" "0" "1"
|
||||
|
||||
# Verify eval_rule and resp_rule both have source matching / excluded IPs
|
||||
ucode -L "$TACHYON_LIB" -e '
|
||||
let common = require("core.common");
|
||||
let cfg = common.read_json_file("'"$output_scoped"'");
|
||||
let eval_rule = null;
|
||||
let resp_rule = null;
|
||||
|
||||
for (let r in cfg.dns.rules) {
|
||||
if (r.action == "evaluate")
|
||||
eval_rule = r;
|
||||
if (r.action == "route" && (r.match_response === true || (r.rules && r.rules[0] && r.rules[0].match_response === true)))
|
||||
resp_rule = r;
|
||||
}
|
||||
|
||||
if (!eval_rule) {
|
||||
warn("eval_rule missing in scoped section\n");
|
||||
exit(1);
|
||||
}
|
||||
if (!resp_rule) {
|
||||
warn("resp_rule missing in scoped section\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// Check source scoping on eval_rule
|
||||
if (eval_rule.type == "logical") {
|
||||
let has_invert = false;
|
||||
for (let child in eval_rule.rules) {
|
||||
if (child.invert && child.source_ip_cidr) has_invert = true;
|
||||
}
|
||||
if (!has_invert) exit(3);
|
||||
} else if (!eval_rule.source_ip_cidr) {
|
||||
exit(4);
|
||||
}
|
||||
|
||||
// Check source scoping on resp_rule
|
||||
if (resp_rule.type == "logical") {
|
||||
let has_invert = false;
|
||||
for (let child in resp_rule.rules) {
|
||||
if (child.invert && child.source_ip_cidr) has_invert = true;
|
||||
}
|
||||
if (!has_invert) exit(5);
|
||||
} else if (!resp_rule.source_ip_cidr) {
|
||||
exit(6);
|
||||
}
|
||||
' || fail "scoped section evaluate/response source matcher verification failed"
|
||||
|
||||
# 5. Test community_subnets=0 with mixed list
|
||||
cat > "$WORK_DIR/fixture_subnets0.json" << 'EOF'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"enabled": "1",
|
||||
"dns_type": "udp",
|
||||
"dns_server": "1.1.1.1",
|
||||
"service_listen_address": "127.0.0.1"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "sec_no_sub",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_no_sub-out\"}" ],
|
||||
"community_lists": [ "telegram" ],
|
||||
"community_subnets": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
output_subnets0="$WORK_DIR/out_subnets0.json"
|
||||
mkdir -p "$output_subnets0.section-cache" "$output_subnets0.rulesets"
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v14" \
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/fixture_subnets0.json" "$output_subnets0" "127.0.0.1" "0" "1"
|
||||
|
||||
grep -Fq '"action": "evaluate"' "$output_subnets0" || fail "community_subnets=0 with mixed list must still generate evaluate"
|
||||
grep -Fq '"match_response": true' "$output_subnets0" || fail "community_subnets=0 with mixed list must still have match_response"
|
||||
|
||||
# 6. Test multiple routing sections with different kinds (domain, mixed, subnets)
|
||||
cat > "$WORK_DIR/fixture_multi.json" << 'EOF'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"enabled": "1",
|
||||
"dns_type": "udp",
|
||||
"dns_server": "1.1.1.1",
|
||||
"service_listen_address": "127.0.0.1"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "sec_dom",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_dom-out\"}" ],
|
||||
"community_lists": [ "youtube" ]
|
||||
},
|
||||
{
|
||||
".name": "sec_mixed",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_mixed-out\"}" ],
|
||||
"community_lists": [ "discord" ],
|
||||
"routed_dns_enabled": "1",
|
||||
"routed_dns_type": "udp",
|
||||
"routed_dns_server": "8.8.8.8"
|
||||
},
|
||||
{
|
||||
".name": "sec_subnet",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"sec_subnet-out\"}" ],
|
||||
"community_lists": [ "geoip_nl" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
output_multi="$WORK_DIR/out_multi.json"
|
||||
mkdir -p "$output_multi.section-cache" "$output_multi.rulesets"
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v14" \
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/fixture_multi.json" "$output_multi" "127.0.0.1" "0" "1"
|
||||
|
||||
# 7. Test sing-box < 1.14 backwards compatibility (e.g. v1.13.5)
|
||||
# Must generate single legacy rule without evaluate and without match_response
|
||||
printf 'v1.13.5\n' > "$WORK_DIR/sb_v13"
|
||||
|
||||
output_v13="$WORK_DIR/out_v13.json"
|
||||
mkdir -p "$output_v13.section-cache" "$output_v13.rulesets"
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v13" \
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/fixture_user_case.json" "$output_v13" "127.0.0.1" "0" "1"
|
||||
|
||||
if grep -Fq '"evaluate"' "$output_v13"; then
|
||||
fail "sing-box < 1.14 must NOT contain evaluate rule"
|
||||
fi
|
||||
if grep -Fq '"match_response"' "$output_v13"; then
|
||||
fail "sing-box < 1.14 must NOT contain match_response"
|
||||
fi
|
||||
|
||||
# Verify in v13 that both russia_inside and telegram are in the legacy rule
|
||||
ucode -L "$TACHYON_LIB" -e '
|
||||
let common = require("core.common");
|
||||
let cfg = common.read_json_file("'"$output_v13"'");
|
||||
let legacy_rule = null;
|
||||
|
||||
for (let r in cfg.dns.rules) {
|
||||
if (r.action == "route" && r.rule_set) {
|
||||
let rs = type(r.rule_set) == "array" ? r.rule_set : [ r.rule_set ];
|
||||
let has_ru = false;
|
||||
let has_tg = false;
|
||||
for (let s in rs) {
|
||||
if (index(s, "russia_inside") >= 0) has_ru = true;
|
||||
if (index(s, "telegram") >= 0) has_tg = true;
|
||||
}
|
||||
if (has_ru && has_tg)
|
||||
legacy_rule = r;
|
||||
}
|
||||
}
|
||||
|
||||
if (!legacy_rule) {
|
||||
warn("Legacy rule combining both rulesets not found on v1.13\n");
|
||||
exit(1);
|
||||
}
|
||||
' || fail "sing-box < 1.14 legacy rule combining verification failed"
|
||||
|
||||
# 8. Validate generated configs against real sing-box 1.14 binary (if available)
|
||||
SB_BIN=""
|
||||
if command -v sing-box >/dev/null 2>&1; then
|
||||
SB_BIN="sing-box"
|
||||
elif [ -x "/tmp/sing-box-bin" ]; then
|
||||
SB_BIN="/tmp/sing-box-bin"
|
||||
fi
|
||||
|
||||
if [ -n "$SB_BIN" ]; then
|
||||
INSTALLED_SB_VER="$("$SB_BIN" version 2>/dev/null | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1 || echo '0.0')"
|
||||
if [ "$(printf '%s\n1.14\n' "$INSTALLED_SB_VER" | sort -V | head -n1)" = "1.14" ]; then
|
||||
"$SB_BIN" check -c "$output_user" || fail "sing-box 1.14 check failed on generated user config!"
|
||||
"$SB_BIN" check -c "$output_dom" || fail "sing-box 1.14 check failed on domain-only config!"
|
||||
"$SB_BIN" check -c "$output_mixed" || fail "sing-box 1.14 check failed on mixed config!"
|
||||
"$SB_BIN" check -c "$output_scoped" || fail "sing-box 1.14 check failed on scoped config!"
|
||||
"$SB_BIN" check -c "$output_subnets0" || fail "sing-box 1.14 check failed on subnets0 config!"
|
||||
"$SB_BIN" check -c "$output_multi" || fail "sing-box 1.14 check failed on multi config!"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "sing-box 1.14 DNS rules tests passed"
|
||||
Reference in New Issue
Block a user