#!/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"