From df61efce80926a0a9a9e5de6b6bd03b38c49f92a Mon Sep 17 00:00:00 2001 From: Dushnilin Date: Thu, 17 Sep 2026 12:35:14 +0300 Subject: [PATCH] fix(singbox): allow routed DNS on bypass sections and hide proxy group outbounds in urltest --- fe-app-tachyon/locales/tachyon.pot | 4 +- fe-app-tachyon/locales/tachyon.ru.po | 4 +- install.sh | 4 + luci-app-tachyon/po/ru/tachyon.po | 4 +- luci-app-tachyon/po/templates/tachyon.pot | 4 +- tachyon/files/usr/lib/components/updates.uc | 16 +++- tachyon/files/usr/lib/config/validator.uc | 4 +- .../usr/lib/singbox/generator_outbounds.uc | 2 + .../files/usr/lib/singbox/generator_routes.uc | 41 +++++++--- tachyon/files/usr/lib/singbox/subscription.uc | 3 +- tests/singbox_114_dns_rules.sh | 75 +++++++++++++++++++ 11 files changed, 137 insertions(+), 24 deletions(-) diff --git a/fe-app-tachyon/locales/tachyon.pot b/fe-app-tachyon/locales/tachyon.pot index 849830c3..54c986e9 100644 --- a/fe-app-tachyon/locales/tachyon.pot +++ b/fe-app-tachyon/locales/tachyon.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: TACHYON\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-17 08:16+0300\n" -"PO-Revision-Date: 2026-09-17 08:16+0300\n" +"POT-Creation-Date: 2026-09-17 09:35+0300\n" +"PO-Revision-Date: 2026-09-17 09:35+0300\n" "Last-Translator: Dushnilin \n" "Language-Team: LANGUAGE \n" "Language: \n" diff --git a/fe-app-tachyon/locales/tachyon.ru.po b/fe-app-tachyon/locales/tachyon.ru.po index a672667f..98a8ca62 100644 --- a/fe-app-tachyon/locales/tachyon.ru.po +++ b/fe-app-tachyon/locales/tachyon.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: TACHYON\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-17 11:16+0300\n" -"PO-Revision-Date: 2026-09-17 11:16+0300\n" +"POT-Creation-Date: 2026-09-17 12:35+0300\n" +"PO-Revision-Date: 2026-09-17 12:35+0300\n" "Last-Translator: Dushnilin\n" "Language-Team: none\n" "Language: ru\n" diff --git a/install.sh b/install.sh index 4a356649..26873501 100755 --- a/install.sh +++ b/install.sh @@ -730,6 +730,10 @@ function ends_with(value, suffix) { return length(value) >= length(suffix) && substr(value, length(value) - length(suffix)) == suffix; } +function file_exists(path) { + return fs.stat(as_string(path)) != null; +} + let uci_cursor_state = false; function words(value) { diff --git a/luci-app-tachyon/po/ru/tachyon.po b/luci-app-tachyon/po/ru/tachyon.po index a672667f..98a8ca62 100644 --- a/luci-app-tachyon/po/ru/tachyon.po +++ b/luci-app-tachyon/po/ru/tachyon.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: TACHYON\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-17 11:16+0300\n" -"PO-Revision-Date: 2026-09-17 11:16+0300\n" +"POT-Creation-Date: 2026-09-17 12:35+0300\n" +"PO-Revision-Date: 2026-09-17 12:35+0300\n" "Last-Translator: Dushnilin\n" "Language-Team: none\n" "Language: ru\n" diff --git a/luci-app-tachyon/po/templates/tachyon.pot b/luci-app-tachyon/po/templates/tachyon.pot index 849830c3..54c986e9 100644 --- a/luci-app-tachyon/po/templates/tachyon.pot +++ b/luci-app-tachyon/po/templates/tachyon.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: TACHYON\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-17 08:16+0300\n" -"PO-Revision-Date: 2026-09-17 08:16+0300\n" +"POT-Creation-Date: 2026-09-17 09:35+0300\n" +"PO-Revision-Date: 2026-09-17 09:35+0300\n" "Last-Translator: Dushnilin \n" "Language-Team: LANGUAGE \n" "Language: \n" diff --git a/tachyon/files/usr/lib/components/updates.uc b/tachyon/files/usr/lib/components/updates.uc index 9a350de2..c92fb014 100644 --- a/tachyon/files/usr/lib/components/updates.uc +++ b/tachyon/files/usr/lib/components/updates.uc @@ -1864,8 +1864,20 @@ function finish_component_job(path, component, action, exit_code, output_file) { // reason; fall back to the result file only if stderr is empty. let raw_output = file_last_nonblank_line_value(output_file + ".stderr", "", 240); if (raw_output == "") - raw_output = file_last_nonblank_line_value(output_file, "Failed to execute", 240); - ok = write_state_file(path, component_fallback_job_state(component, action, raw_output, exit_code, updated_at)); + raw_output = file_last_nonblank_line_value(output_file, "", 240); + let log_file = substr(output_file, 0, length(output_file) - 4) + ".log"; + if (raw_output == "" && file_exists_value(log_file)) + raw_output = file_last_nonblank_line_value(log_file, "", 240); + let is_success = arg_number(exit_code) == 0; + if (raw_output == "") + raw_output = is_success ? sprintf("%s %s completed successfully", component, action) : "Failed to execute"; + let state = component_fallback_job_state(component, action, raw_output, exit_code, updated_at); + if (is_success) { + state.success = true; + state.status = "latest"; + state.changed = 1; + } + ok = write_state_file(path, state); } remove_file(output_file); diff --git a/tachyon/files/usr/lib/config/validator.uc b/tachyon/files/usr/lib/config/validator.uc index b9fc0c5a..917b56b9 100644 --- a/tachyon/files/usr/lib/config/validator.uc +++ b/tachyon/files/usr/lib/config/validator.uc @@ -1621,8 +1621,8 @@ function validate_rule(section, sections, context) { validate_combined_domain_text_value(option(section, "domain_suffix_text", ""), name); if (connections.routed_dns_enabled(section)) { - if (action == "dns" || action == "hosts" || action == "bypass" || action == "block") - fail_validation("Rule '" + name + "' has routed DNS enabled but action '" + action + "' does not support routed DNS. Use a proxy action (connection, zapret, etc.). Aborted."); + if (action == "dns" || action == "hosts" || action == "block") + fail_validation("Rule '" + name + "' has routed DNS enabled but action '" + action + "' does not support routed DNS. Aborted."); let routed_type = connections.routed_dns_type(section); if (!contains(["udp", "dot", "doh", "doq"], routed_type)) fail_validation("Rule '" + name + "' has invalid routed DNS type '" + routed_type + "'. Use udp, dot, doh, or doq. Aborted."); diff --git a/tachyon/files/usr/lib/singbox/generator_outbounds.uc b/tachyon/files/usr/lib/singbox/generator_outbounds.uc index b7797253..0fd215ce 100644 --- a/tachyon/files/usr/lib/singbox/generator_outbounds.uc +++ b/tachyon/files/usr/lib/singbox/generator_outbounds.uc @@ -1197,6 +1197,8 @@ function add_subscription_source_with_state(config, section, source_index, sourc if (hidden_flags[i] !== true) { push(selector_tags, outbound.tag); runtime_subscription.remember_urltest_group(state, outbound.tag, display_names[i], outbound); + } else if (state && state.hiddenOutboundTags) { + state.hiddenOutboundTags[outbound.tag] = true; } } return added; diff --git a/tachyon/files/usr/lib/singbox/generator_routes.uc b/tachyon/files/usr/lib/singbox/generator_routes.uc index 912ac206..4493a434 100644 --- a/tachyon/files/usr/lib/singbox/generator_routes.uc +++ b/tachyon/files/usr/lib/singbox/generator_routes.uc @@ -410,12 +410,24 @@ function filter_candidate_outbounds(filter_mode, urltest_candidate_tags, names, include_proxy_parameters, include_protocols, include_transports, include_securities, exclude_names, exclude_regex, exclude_countries, exclude_proxy_parameters, exclude_protocols, exclude_transports, exclude_securities, - include_additional_matches, exclude_additional_matches) { + include_additional_matches, exclude_additional_matches, + hidden_tags) { let all_outbounds = urltest_all_candidate_outbounds(urltest_candidate_tags); + let visible_outbounds = all_outbounds; + if (hidden_tags && length(keys(hidden_tags)) > 0) { + let filtered = []; + for (let tag in all_outbounds) { + if (!hidden_tags[tag]) + push(filtered, tag); + } + if (length(filtered) > 0) + visible_outbounds = filtered; + } + if (filter_mode == "" || filter_mode == "disabled") - return all_outbounds; + return visible_outbounds; if (!supported_urltest_filter_mode(filter_mode)) - return all_outbounds; + return visible_outbounds; let include_outbounds = urltest_matching_candidate_outbounds( urltest_candidate_tags, @@ -451,10 +463,10 @@ function filter_candidate_outbounds(filter_mode, urltest_candidate_tags, names, if (filter_mode == "include") return include_outbounds; if (filter_mode == "exclude") - return urltest_exclude_outbounds(all_outbounds, exclude_outbounds); + return urltest_exclude_outbounds(visible_outbounds, exclude_outbounds); if (filter_mode == "mixed") return urltest_exclude_outbounds(include_outbounds, exclude_outbounds); - return all_outbounds; + return visible_outbounds; } function urltest_filtered_outbounds(section, urltest_id, urltest_candidate_tags, state) { @@ -477,7 +489,10 @@ function urltest_filtered_outbounds(section, urltest_id, urltest_candidate_tags, connections.urltest_exclude_proxy_parameters(section, urltest_id), connections.urltest_exclude_protocols(section, urltest_id), connections.urltest_exclude_transports(section, urltest_id), - connections.urltest_exclude_securities(section, urltest_id) + connections.urltest_exclude_securities(section, urltest_id), + null, + null, + state ? state.hiddenOutboundTags : null ); } @@ -520,7 +535,10 @@ function priority_level_filtered_outbounds(group_id, level_id, urltest_candidate connections.priority_level_exclude_proxy_parameters(group_id, level_id), connections.priority_level_exclude_protocols(group_id, level_id), connections.priority_level_exclude_transports(group_id, level_id), - connections.priority_level_exclude_securities(group_id, level_id) + connections.priority_level_exclude_securities(group_id, level_id), + null, + null, + state ? state.hiddenOutboundTags : null ); } @@ -1339,10 +1357,10 @@ function push_dns_matcher_rule(config, rule) { } function section_dns_server(section) { - if (option(section, "action", "") == "bypass") - return runtime_constants.DNS_SERVER_TAG; if (connections.routed_dns_enabled(section)) return runtime_constants.tag(section[".name"], "routed-dns-server"); + if (option(section, "action", "") == "bypass") + return runtime_constants.DNS_SERVER_TAG; return runtime_constants.FAKEIP_DNS_SERVER_TAG; } @@ -1406,10 +1424,11 @@ function add_routed_dns_server_for_section(config, section) { if (!connections.routed_dns_enabled(section)) return; let section_name = section[".name"]; + let action = option(section, "action", ""); let servers = connections.routed_dns_servers(section); let dns_type = connections.routed_dns_type(section); let tag_name = routed_dns_server_tag(section_name); - let detour = outbound_tag(section_name); + let detour = action == "bypass" ? null : outbound_tag(section_name); for (let i = 0; i < length(servers); i++) { let s_val = servers[i]; @@ -2128,7 +2147,7 @@ function add_outbound_for_section(config, section, taken, sections) { ctx.runtime_generate_unsupported("unsupported action " + action); } - if (action != "dns" && action != "hosts" && action != "bypass" && action != "block") + if (action != "dns" && action != "hosts" && action != "block") add_routed_dns_server_for_section(config, section); } diff --git a/tachyon/files/usr/lib/singbox/subscription.uc b/tachyon/files/usr/lib/singbox/subscription.uc index 7baf20e6..c846f7ed 100644 --- a/tachyon/files/usr/lib/singbox/subscription.uc +++ b/tachyon/files/usr/lib/singbox/subscription.uc @@ -347,7 +347,8 @@ function new_section_state(section_name) { urltestCandidateTags: [], urltestGroups: {}, priorityGroups: {}, - subscriptionMetadata: [] + subscriptionMetadata: [], + hiddenOutboundTags: {} }; } diff --git a/tests/singbox_114_dns_rules.sh b/tests/singbox_114_dns_rules.sh index a92b83a2..dae69139 100644 --- a/tests/singbox_114_dns_rules.sh +++ b/tests/singbox_114_dns_rules.sh @@ -411,4 +411,79 @@ if [ -n "$SB_BIN" ]; then fi fi +# 9. Test bypass section with routed DNS +cat >"$WORK_DIR/bypass_routed_dns_fixture.json" <<'EOF' +{ + "settings": { + ".name": "settings", + ".type": "settings", + "dns_server": [ "77.88.8.8" ], + "bootstrap_dns_server": [ "77.88.8.8" ] + }, + "section": [ + { + ".name": "ByPass", + ".type": "section", + "enabled": "1", + "action": "bypass", + "community_lists": [ "russia_outside" ], + "routed_dns_enabled": "1", + "routed_dns_type": "doh", + "routed_dns_server": [ "https://dns.comss.one/dns-query" ] + } + ] +} +EOF +output_bypass="$WORK_DIR/output_bypass.json" +mkdir -p "$output_bypass.section-cache" "$output_bypass.rulesets" +SB_VERSION_STATE_FILE="$WORK_DIR/sb_v14" \ +ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \ + "$WORK_DIR/bypass_routed_dns_fixture.json" "$output_bypass" "127.0.0.1" "0" "1" + +ucode -e ' +let fs = require("fs"); +let cfg = json(fs.readfile(ARGV[0])); +let found_server = false; +for (let s in cfg.dns.servers || []) { + if (s.tag == "ByPass-routed-dns-server") { + found_server = true; + if (s.type != "https" || s.server != "dns.comss.one" || s.detour) { + warn("ByPass-routed-dns-server properties mismatch: " + json(s) + "\n"); + exit(1); + } + } +} +if (!found_server) { + warn("ByPass-routed-dns-server not found in dns.servers\n"); + exit(1); +} + +let found_dns_rule = false; +for (let r in cfg.dns.rules || []) { + if (r.server == "ByPass-routed-dns-server") + found_dns_rule = true; +} +if (!found_dns_rule) { + warn("DNS rule targeting ByPass-routed-dns-server not found\n"); + exit(1); +} + +let found_route_rule = false; +for (let r in cfg.route.rules || []) { + if (r.outbound == "bypass-out" && r.rule_set == "ByPass-russia_outside-community-ruleset") + found_route_rule = true; +} +if (!found_route_rule) { + warn("Route rule for ByPass community ruleset not found\n"); + exit(1); +} +' "$output_bypass" || fail "bypass with routed DNS 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_bypass" || fail "sing-box 1.14 check failed on bypass with routed DNS config!" + fi +fi + echo "sing-box 1.14 DNS rules tests passed"