fix(singbox): prevent empty logical subrules when excluding IPs on evaluate and match-all rules (#60)
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-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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user