fix(singbox): separate dns query and response rulesets for sing-box 1.14+ compatibility
This commit is contained in:
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-16 19:54+0300\n"
|
||||
"PO-Revision-Date: 2026-09-16 19:54+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 05:13+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 05:13+0300\n"
|
||||
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-16 22:54+0300\n"
|
||||
"PO-Revision-Date: 2026-09-16 22:54+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 08:13+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 08:13+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-16 22:54+0300\n"
|
||||
"PO-Revision-Date: 2026-09-16 22:54+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 08:13+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 08:13+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-16 19:54+0300\n"
|
||||
"PO-Revision-Date: 2026-09-16 19:54+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 05:13+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 05:13+0300\n"
|
||||
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
|
||||
@@ -7,6 +7,7 @@ let as_string = common.as_string;
|
||||
let object_or_empty = common.object_or_empty;
|
||||
let bool_value_common = common.bool_value;
|
||||
let remove_file = common.remove_file;
|
||||
let list_option = common.list_option;
|
||||
|
||||
function bool_value(value, fallback) {
|
||||
if (value == null || value == "")
|
||||
|
||||
@@ -592,6 +592,7 @@ function base_config(settings, service_address, runtime_context) {
|
||||
: false);
|
||||
|
||||
let is_sb_1_14_plus = is_sb_1_14_plus_detected(sb_version_val);
|
||||
ctx.is_sb_1_14_plus = function() { return is_sb_1_14_plus; };
|
||||
|
||||
let route_section = runtime_route.config(settings, runtime_context);
|
||||
if (is_sb_1_14_plus)
|
||||
|
||||
@@ -907,7 +907,7 @@ function ensure_custom_ruleset(config, reference) {
|
||||
|
||||
if (runtime_rulesets.is_community(reference)) {
|
||||
tag_name = "builtin-" + reference + "-ruleset";
|
||||
kind = "domains";
|
||||
kind = runtime_rulesets.community_kind ? runtime_rulesets.community_kind(reference) : "domains";
|
||||
if (!ruleset_registered(config, tag_name)) {
|
||||
let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder;
|
||||
let tmp_srs = folder + "/community-" + reference + ".srs";
|
||||
@@ -1149,7 +1149,7 @@ function ensure_community_ruleset(config, section_name, community) {
|
||||
}
|
||||
return {
|
||||
tag: tag_name,
|
||||
kind: "domains"
|
||||
kind: runtime_rulesets.community_kind ? runtime_rulesets.community_kind(community) : "domains"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1424,25 +1424,31 @@ function add_dns_action_rules_for_section(config, section) {
|
||||
let domain_suffix = domains.domain_suffix;
|
||||
let domain_keyword = domains.domain_keyword;
|
||||
let domain_regex = domains.domain_regex;
|
||||
let rule_set_tags = [];
|
||||
let query_rule_set_tags = [];
|
||||
let response_rule_set_tags = [];
|
||||
let section_name = section[".name"];
|
||||
let source_ip_cidr = core_ip.normalize_to_cidrs(legacy_condition_values(section, "source_ip_cidr"));
|
||||
let fully_routed_ips = core_ip.normalize_to_cidrs(list_option(section, "fully_routed_ips"));
|
||||
|
||||
for (let community in connections.community_lists(section)) {
|
||||
let ensured = ensure_community_ruleset(config, section_name, as_string(community));
|
||||
push(rule_set_tags, ensured.tag);
|
||||
if (ensured.kind == "domains")
|
||||
push(query_rule_set_tags, ensured.tag);
|
||||
else if (ensured.kind == "subnets" || ensured.kind == "mixed")
|
||||
push(response_rule_set_tags, ensured.tag);
|
||||
else
|
||||
push(response_rule_set_tags, ensured.tag);
|
||||
}
|
||||
for (let reference in connections.rule_sets(section)) {
|
||||
let ensured = ensure_custom_ruleset(config, as_string(reference));
|
||||
if (ensured != null && ensured.kind == "domains")
|
||||
push(rule_set_tags, ensured.tag);
|
||||
push(query_rule_set_tags, ensured.tag);
|
||||
}
|
||||
add_domain_ip_list_ruleset(
|
||||
config,
|
||||
section_name,
|
||||
[],
|
||||
rule_set_tags,
|
||||
query_rule_set_tags,
|
||||
list_option(section, "domain_ip_lists"),
|
||||
true
|
||||
);
|
||||
@@ -1482,17 +1488,58 @@ function add_dns_action_rules_for_section(config, section) {
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, dns_rule);
|
||||
}
|
||||
if (length(rule_set_tags) > 0) {
|
||||
|
||||
let is_1_14 = ctx.is_sb_1_14_plus && ctx.is_sb_1_14_plus();
|
||||
if (is_1_14) {
|
||||
if (length(query_rule_set_tags) > 0) {
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
server: server_tag,
|
||||
rewrite_ttl,
|
||||
rule_set: single_or_array(rule_set_tags)
|
||||
rule_set: single_or_array(query_rule_set_tags)
|
||||
};
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, dns_rule);
|
||||
}
|
||||
if (!has_inline_domains && length(rule_set_tags) == 0 && length(fully_routed_ips) == 0)
|
||||
if (length(response_rule_set_tags) > 0) {
|
||||
let eval_rule = {
|
||||
action: "evaluate",
|
||||
server: runtime_constants.DNS_SERVER_TAG
|
||||
};
|
||||
add_source_dns_matchers(eval_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, eval_rule);
|
||||
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
server: server_tag,
|
||||
rewrite_ttl,
|
||||
rule_set: single_or_array(response_rule_set_tags),
|
||||
match_response: true
|
||||
};
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, dns_rule);
|
||||
}
|
||||
}
|
||||
else {
|
||||
let all_rule_set_tags = [];
|
||||
for (let tag in query_rule_set_tags)
|
||||
push(all_rule_set_tags, tag);
|
||||
for (let tag in response_rule_set_tags)
|
||||
push(all_rule_set_tags, tag);
|
||||
|
||||
if (length(all_rule_set_tags) > 0) {
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
server: server_tag,
|
||||
rewrite_ttl,
|
||||
rule_set: single_or_array(all_rule_set_tags)
|
||||
};
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, dns_rule);
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_inline_domains && length(query_rule_set_tags) == 0 && length(response_rule_set_tags) == 0 && length(fully_routed_ips) == 0)
|
||||
ctx.runtime_generate_unsupported("DNS action '" + section_name + "' has no domain matchers");
|
||||
}
|
||||
|
||||
@@ -1749,7 +1796,8 @@ function add_combined_route_for_section(config, section) {
|
||||
let source_ip_cidr = core_ip.normalize_to_cidrs(legacy_condition_values(section, "source_ip_cidr"));
|
||||
let excluded_cidrs = core_ip.normalize_to_cidrs(list_option(section, "excluded_ips"));
|
||||
let rule_set_tags = [];
|
||||
let dns_rule_set_tags = [];
|
||||
let dns_query_rule_set_tags = [];
|
||||
let dns_response_rule_set_tags = [];
|
||||
let section_name = section[".name"];
|
||||
|
||||
add_excluded_protocol_rule(config, section);
|
||||
@@ -1762,7 +1810,13 @@ function add_combined_route_for_section(config, section) {
|
||||
let service = as_string(community);
|
||||
let ensured = ensure_community_ruleset(config, section_name, service);
|
||||
push(rule_set_tags, ensured.tag);
|
||||
push(dns_rule_set_tags, ensured.tag);
|
||||
if (ensured.kind == "domains")
|
||||
push(dns_query_rule_set_tags, ensured.tag);
|
||||
else if (ensured.kind == "subnets" || ensured.kind == "mixed")
|
||||
push(dns_response_rule_set_tags, ensured.tag);
|
||||
else
|
||||
push(dns_response_rule_set_tags, ensured.tag);
|
||||
|
||||
if (include_community_subnets) {
|
||||
for (let cidr in load_community_subnet_cidrs(community, "exclude_cloudflare"))
|
||||
push(ip_cidr, cidr);
|
||||
@@ -1778,7 +1832,7 @@ function add_combined_route_for_section(config, section) {
|
||||
continue;
|
||||
push(rule_set_tags, ensured.tag);
|
||||
if (ensured.kind == "domains")
|
||||
push(dns_rule_set_tags, ensured.tag);
|
||||
push(dns_query_rule_set_tags, ensured.tag);
|
||||
}
|
||||
for (let reference in connections.rule_sets_with_subnets(section)) {
|
||||
let ensured = ensure_custom_ruleset(config, as_string(reference));
|
||||
@@ -1786,13 +1840,13 @@ function add_combined_route_for_section(config, section) {
|
||||
continue;
|
||||
push(rule_set_tags, ensured.tag);
|
||||
if (ensured.kind == "domains")
|
||||
push(dns_rule_set_tags, ensured.tag);
|
||||
push(dns_query_rule_set_tags, ensured.tag);
|
||||
}
|
||||
add_domain_ip_list_ruleset(
|
||||
config,
|
||||
section_name,
|
||||
rule_set_tags,
|
||||
dns_rule_set_tags,
|
||||
dns_query_rule_set_tags,
|
||||
list_option(section, "domain_ip_lists"),
|
||||
false
|
||||
);
|
||||
@@ -1914,16 +1968,55 @@ function add_combined_route_for_section(config, section) {
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, apply_excluded_source_ips(dns_rule, excluded_cidrs));
|
||||
}
|
||||
if (length(dns_rule_set_tags) > 0) {
|
||||
let is_1_14 = ctx.is_sb_1_14_plus && ctx.is_sb_1_14_plus();
|
||||
if (is_1_14) {
|
||||
if (length(dns_query_rule_set_tags) > 0) {
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
server: section_dns_server(section),
|
||||
rewrite_ttl,
|
||||
rule_set: single_or_array(dns_rule_set_tags)
|
||||
rule_set: single_or_array(dns_query_rule_set_tags)
|
||||
};
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, apply_excluded_source_ips(dns_rule, excluded_cidrs));
|
||||
}
|
||||
if (length(dns_response_rule_set_tags) > 0) {
|
||||
let eval_rule = {
|
||||
action: "evaluate",
|
||||
server: runtime_constants.DNS_SERVER_TAG
|
||||
};
|
||||
add_source_dns_matchers(eval_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, apply_excluded_source_ips(eval_rule, excluded_cidrs));
|
||||
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
server: section_dns_server(section),
|
||||
rewrite_ttl,
|
||||
rule_set: single_or_array(dns_response_rule_set_tags),
|
||||
match_response: true
|
||||
};
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, apply_excluded_source_ips(dns_rule, excluded_cidrs));
|
||||
}
|
||||
}
|
||||
else {
|
||||
let all_dns_tags = [];
|
||||
for (let tag in dns_query_rule_set_tags)
|
||||
push(all_dns_tags, tag);
|
||||
for (let tag in dns_response_rule_set_tags)
|
||||
push(all_dns_tags, tag);
|
||||
|
||||
if (length(all_dns_tags) > 0) {
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
server: section_dns_server(section),
|
||||
rewrite_ttl,
|
||||
rule_set: single_or_array(all_dns_tags)
|
||||
};
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, apply_excluded_source_ips(dns_rule, excluded_cidrs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unsupported_matcher_key(section) {
|
||||
|
||||
@@ -48,7 +48,54 @@ const COMMUNITY_SERVICES = {
|
||||
geoip_ru: true,
|
||||
geosite_ru: true,
|
||||
geoip_us: true,
|
||||
geoip_cn: true
|
||||
geoip_cn: true,
|
||||
google_meet: true
|
||||
};
|
||||
|
||||
// Classification of community rule-sets based on upstream rule-set generation.
|
||||
// Upstream allow-domains (itdoginfo/allow-domains convert.py) compiles:
|
||||
// - general lists (russia_inside, russia_outside, ukraine_inside) and category lists as domain-only ("domains");
|
||||
// - SUBNET_SERVICES (discord, meta, twitter, telegram, cloudflare, hetzner, ovh, digitalocean, cloudfront, roblox, google_meet)
|
||||
// as mixed containing both domain_suffix and ip_cidr ("mixed");
|
||||
// - geoip_* lists from MetaCubeX/meta-rules-dat contain only ip_cidr ("subnets");
|
||||
// - geosite_* lists from MetaCubeX/meta-rules-dat contain only domains ("domains");
|
||||
// - external lists (github, twitch, ads_hagezi_pro, supercell) are domain-only ("domains").
|
||||
//
|
||||
// In sing-box 1.14+, referencing a rule-set with ip_cidr in a DNS rule without match_response: true
|
||||
// is treated as a deprecated legacy address filter and rejected if query_type is present in DNS configuration.
|
||||
const COMMUNITY_SUBNET_SERVICES = {
|
||||
discord: true,
|
||||
meta: true,
|
||||
twitter: true,
|
||||
telegram: true,
|
||||
cloudflare: true,
|
||||
hetzner: true,
|
||||
ovh: true,
|
||||
digitalocean: true,
|
||||
cloudfront: true,
|
||||
roblox: true,
|
||||
google_meet: true
|
||||
};
|
||||
|
||||
const COMMUNITY_DOMAIN_SERVICES = {
|
||||
russia_inside: true,
|
||||
russia_outside: true,
|
||||
ukraine_inside: true,
|
||||
geoblock: true,
|
||||
block: true,
|
||||
porn: true,
|
||||
news: true,
|
||||
anime: true,
|
||||
youtube: true,
|
||||
hdrezka: true,
|
||||
tiktok: true,
|
||||
google_ai: true,
|
||||
google_play: true,
|
||||
hodca: true,
|
||||
ads_hagezi_pro: true,
|
||||
supercell: true,
|
||||
github: true,
|
||||
twitch: true
|
||||
};
|
||||
|
||||
let as_string = common.as_string;
|
||||
@@ -62,6 +109,19 @@ function is_community(name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function community_kind(name) {
|
||||
name = as_string(name);
|
||||
if (match(name, /^geoip_[a-z]{2}$/) != null)
|
||||
return "subnets";
|
||||
if (match(name, /^geosite_[a-z]{2}$/) != null)
|
||||
return "domains";
|
||||
if (COMMUNITY_SUBNET_SERVICES[name] === true)
|
||||
return "mixed";
|
||||
if (COMMUNITY_DOMAIN_SERVICES[name] === true)
|
||||
return "domains";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function community_url(name) {
|
||||
name = as_string(name);
|
||||
if (name == "ads_hagezi_pro")
|
||||
@@ -179,8 +239,11 @@ function module_exports() {
|
||||
EMPTY_SRS_PATH,
|
||||
EMPTY_SRS_B64,
|
||||
COMMUNITY_SERVICES,
|
||||
COMMUNITY_SUBNET_SERVICES,
|
||||
COMMUNITY_DOMAIN_SERVICES,
|
||||
is_community,
|
||||
community_url,
|
||||
community_kind,
|
||||
hash12,
|
||||
file_extension,
|
||||
kind_from_reference_hint,
|
||||
@@ -200,6 +263,8 @@ if (mode == "file-extension")
|
||||
print(file_extension(ARGV[1]), "\n");
|
||||
else if (mode == "is-community")
|
||||
exit(is_community(ARGV[1]) ? 0 : 1);
|
||||
else if (mode == "community-kind")
|
||||
print(community_kind(ARGV[1]), "\n");
|
||||
else if (mode == "kind-from-reference-hint")
|
||||
print(kind_from_reference_hint(ARGV[1]), "\n");
|
||||
else if (mode == "remote-format")
|
||||
@@ -211,6 +276,6 @@ else if (mode == "is-valid-srs-file")
|
||||
else if (mode == "ensure-empty-srs-stub")
|
||||
exit(ensure_empty_srs_stub(ARGV[1]) ? 0 : 1);
|
||||
else {
|
||||
warn("Usage: singbox/rulesets.uc <file-extension|is-community|kind-from-reference-hint|remote-format|is-valid-srs-file|ensure-empty-srs-stub> ...\n");
|
||||
warn("Usage: singbox/rulesets.uc <file-extension|is-community|community-kind|kind-from-reference-hint|remote-format|is-valid-srs-file|ensure-empty-srs-stub> ...\n");
|
||||
exit(1);
|
||||
}
|
||||
@@ -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"
|
||||
@@ -58,5 +58,27 @@ assert_eq "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/g
|
||||
"$(ucode -L "$TACHYON_LIB" -e 'let rulesets = require("singbox.rulesets"); print(rulesets.community_url("twitch"));')" \
|
||||
"twitch community ruleset url"
|
||||
|
||||
assert_eq mixed \
|
||||
"$(ucode -L "$TACHYON_LIB" "$RULESETS_UC" community-kind telegram)" \
|
||||
"telegram community kind"
|
||||
assert_eq mixed \
|
||||
"$(ucode -L "$TACHYON_LIB" "$RULESETS_UC" community-kind discord)" \
|
||||
"discord community kind"
|
||||
assert_eq domains \
|
||||
"$(ucode -L "$TACHYON_LIB" "$RULESETS_UC" community-kind youtube)" \
|
||||
"youtube community kind"
|
||||
assert_eq domains \
|
||||
"$(ucode -L "$TACHYON_LIB" "$RULESETS_UC" community-kind russia_inside)" \
|
||||
"russia_inside community kind"
|
||||
assert_eq domains \
|
||||
"$(ucode -L "$TACHYON_LIB" "$RULESETS_UC" community-kind geosite_ru)" \
|
||||
"geosite_ru community kind"
|
||||
assert_eq subnets \
|
||||
"$(ucode -L "$TACHYON_LIB" "$RULESETS_UC" community-kind geoip_ru)" \
|
||||
"geoip_ru community kind"
|
||||
assert_eq unknown \
|
||||
"$(ucode -L "$TACHYON_LIB" "$RULESETS_UC" community-kind unknown_service)" \
|
||||
"unknown_service community kind"
|
||||
|
||||
printf 'singbox rulesets checks passed\n'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user