fix(singbox): prevent empty logical subrules when excluding IPs on evaluate and match-all rules (#60)

This commit is contained in:
Dushnilin
2026-09-17 14:53:12 +03:00
parent 2ab4e0db74
commit a27173a04a
6 changed files with 194 additions and 19 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 11:39+0300\n"
"PO-Revision-Date: 2026-09-17 11:39+0300\n"
"POT-Creation-Date: 2026-09-17 11:53+0300\n"
"PO-Revision-Date: 2026-09-17 11:53+0300\n"
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 14:39+0300\n"
"PO-Revision-Date: 2026-09-17 14:39+0300\n"
"POT-Creation-Date: 2026-09-17 14:53+0300\n"
"PO-Revision-Date: 2026-09-17 14:53+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 14:39+0300\n"
"PO-Revision-Date: 2026-09-17 14:39+0300\n"
"POT-Creation-Date: 2026-09-17 14:53+0300\n"
"PO-Revision-Date: 2026-09-17 14:53+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 11:39+0300\n"
"PO-Revision-Date: 2026-09-17 11:39+0300\n"
"POT-Creation-Date: 2026-09-17 11:53+0300\n"
"PO-Revision-Date: 2026-09-17 11:53+0300\n"
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -1352,8 +1352,42 @@ function add_domain_array(rule, key, values) {
rule[key] = values;
}
function sanitize_rule(rule) {
if (!rule)
return null;
if (rule.type == "logical" && type(rule.rules) == "array") {
let clean_subrules = [];
for (let sub in rule.rules) {
if (sub && length(keys(sub)) > 0)
push(clean_subrules, sub);
}
if (length(clean_subrules) == 0)
return null;
if (length(clean_subrules) == 1) {
let single = {};
for (let k, v in rule) {
if (k != "type" && k != "mode" && k != "rules")
single[k] = v;
}
for (let k, v in clean_subrules[0])
single[k] = v;
return single;
}
rule.rules = clean_subrules;
}
return rule;
}
function push_dns_matcher_rule(config, rule) {
push(config.dns.rules, rule);
rule = sanitize_rule(rule);
if (rule)
push(config.dns.rules, rule);
}
function push_route_matcher_rule(config, rule) {
rule = sanitize_rule(rule);
if (rule)
push(config.route.rules, rule);
}
function section_dns_server(section) {
@@ -1690,9 +1724,51 @@ function apply_excluded_source_ips(rule, excluded_cidrs) {
action: true,
outbound: true,
server: true,
rewrite_ttl: true
rewrite_ttl: true,
client_subnet: true,
disable_cache: true,
disable_optimistic_cache: true,
timeout: true,
method: true,
no_drop: true,
strategy: true
};
if (rule.type == "logical" && rule.mode == "and" && type(rule.rules) == "array") {
let cloned = {};
for (let k, v in rule) {
if (k == "rules") {
cloned.rules = [];
for (let r in v) {
if (r && length(keys(r)) > 0)
push(cloned.rules, r);
}
push(cloned.rules, {
invert: true,
source_ip_cidr: single_or_array(excluded_cidrs)
});
} else {
cloned[k] = v;
}
}
return cloned;
}
let match_part = {};
for (let k, v in rule) {
if (!action_keys[k] && substr(k, 0, 2) != "__")
match_part[k] = v;
}
if (length(keys(match_part)) == 0) {
let single_rule = {};
for (let k, v in rule)
single_rule[k] = v;
single_rule.invert = true;
single_rule.source_ip_cidr = single_or_array(excluded_cidrs);
return single_rule;
}
let logical_rule = {
type: "logical",
mode: "and",
@@ -1705,10 +1781,8 @@ function apply_excluded_source_ips(rule, excluded_cidrs) {
]
};
for (let k, v in rule) {
if (action_keys[k])
if (action_keys[k] || substr(k, 0, 2) == "__")
logical_rule[k] = v;
else
match_part[k] = v;
}
return logical_rule;
}
@@ -1716,13 +1790,13 @@ function apply_excluded_source_ips(rule, excluded_cidrs) {
function push_section_route_rule(config, rule, target_outbound, excluded_cidrs) {
if (target_outbound && !outbound_supports_udp(config, target_outbound)) {
if (rule.network == "tcp") {
push(config.route.rules, apply_excluded_source_ips(rule, excluded_cidrs));
push_route_matcher_rule(config, apply_excluded_source_ips(rule, excluded_cidrs));
return;
}
if (rule.network == "udp") {
delete rule.outbound;
rule.action = "reject";
push(config.route.rules, apply_excluded_source_ips(rule, excluded_cidrs));
push_route_matcher_rule(config, apply_excluded_source_ips(rule, excluded_cidrs));
return;
}
let udp_rule = {};
@@ -1731,11 +1805,11 @@ function push_section_route_rule(config, rule, target_outbound, excluded_cidrs)
delete udp_rule.outbound;
udp_rule.action = "reject";
udp_rule.network = "udp";
push(config.route.rules, apply_excluded_source_ips(udp_rule, excluded_cidrs));
push_route_matcher_rule(config, apply_excluded_source_ips(udp_rule, excluded_cidrs));
rule.network = "tcp";
}
push(config.route.rules, apply_excluded_source_ips(rule, excluded_cidrs));
push_route_matcher_rule(config, apply_excluded_source_ips(rule, excluded_cidrs));
}
function add_fully_routed_ips_rule(config, section) {
@@ -1940,7 +2014,7 @@ function add_combined_route_for_section(config, section) {
if (type(resolve) == "object" && resolve.warning)
warn(resolve.warning, "\n");
else if (type(resolve) == "object" && resolve.rule)
push(config.route.rules, apply_excluded_source_ips(resolve.rule, excluded_cidrs));
push_route_matcher_rule(config, apply_excluded_source_ips(resolve.rule, excluded_cidrs));
push_section_route_rule(config, domain_rule, target.outbound, excluded_cidrs);
}
@@ -1953,7 +2027,7 @@ function add_combined_route_for_section(config, section) {
if (type(resolve) == "object" && resolve.warning)
warn(resolve.warning, "\n");
else if (type(resolve) == "object" && resolve.rule)
push(config.route.rules, apply_excluded_source_ips(resolve.rule, excluded_cidrs));
push_route_matcher_rule(config, apply_excluded_source_ips(resolve.rule, excluded_cidrs));
push_section_route_rule(config, rs_rule, target.outbound, excluded_cidrs);
}
+101
View File
@@ -486,4 +486,105 @@ if [ -n "$SB_BIN" ]; then
fi
fi
# 10. Test Issue #60: Section with mixed list and excluded_ips WITHOUT source_ip_cidr
# Must generate evaluate rule with invert: true, NOT a logical rule with empty subrules
cat > "$WORK_DIR/fixture_issue60.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": "vpn_issue60",
".type": "section",
"enabled": "1",
"action": "connection",
"outbound_jsons": [ "{\"type\":\"direct\",\"tag\":\"vpn_issue60-out\"}" ],
"community_lists": [ "telegram" ],
"excluded_ips": [ "192.168.1.200" ]
}
]
}
EOF
output_issue60="$WORK_DIR/out_issue60.json"
mkdir -p "$output_issue60.section-cache" "$output_issue60.rulesets"
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v14" \
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
"$WORK_DIR/fixture_issue60.json" "$output_issue60" "127.0.0.1" "0" "1"
ucode -e '
let fs = require("fs");
let cfg = json(fs.readfile(ARGV[0]));
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 issue60 reproduction config\n");
exit(1);
}
if (!resp_rule) {
warn("resp_rule missing in issue60 reproduction config\n");
exit(2);
}
// eval_rule MUST NOT be a logical rule with an empty condition sub-rule
if (eval_rule.type == "logical") {
for (let child in eval_rule.rules || []) {
if (!child || length(keys(child)) == 0) {
warn("eval_rule contains empty condition sub-rule!\n");
exit(3);
}
}
}
// eval_rule must have invert: true and source_ip_cidr
if (!eval_rule.invert || !eval_rule.source_ip_cidr) {
warn("eval_rule missing invert or source_ip_cidr\n");
exit(4);
}
// Check NO rule in dns.rules or route.rules has empty condition sub-rules
for (let r in cfg.dns.rules || []) {
if (r.type == "logical") {
for (let child in r.rules || []) {
if (!child || length(keys(child)) == 0) {
warn("dns rule contains empty sub-rule!\n");
exit(5);
}
}
}
}
for (let r in cfg.route.rules || []) {
if (r.type == "logical") {
for (let child in r.rules || []) {
if (!child || length(keys(child)) == 0) {
warn("route rule contains empty sub-rule!\n");
exit(6);
}
}
}
}
' "$output_issue60" || fail "issue60 reproduction config verification failed"
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_issue60" || fail "sing-box 1.14 check failed on issue60 reproduction config!"
fi
fi
echo "sing-box 1.14 DNS rules tests passed"