diff --git a/fe-app-tachyon/package-lock.json b/fe-app-tachyon/package-lock.json index 8892d3c4..7be4571d 100644 --- a/fe-app-tachyon/package-lock.json +++ b/fe-app-tachyon/package-lock.json @@ -1,12 +1,12 @@ { "name": "fe-app-tachyon", - "version": "1.3.18", + "version": "1.3.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fe-app-tachyon", - "version": "1.3.18", + "version": "1.3.19", "license": "MIT", "dependencies": { "@esbuild/win32-x64": "^0.28.2", diff --git a/fe-app-tachyon/package.json b/fe-app-tachyon/package.json index 08967ab8..ec0e017c 100644 --- a/fe-app-tachyon/package.json +++ b/fe-app-tachyon/package.json @@ -1,6 +1,6 @@ { "name": "fe-app-tachyon", - "version": "1.3.18", + "version": "1.3.19", "license": "MIT", "type": "module", "scripts": { diff --git a/fe-app-tachyon/src/constants.ts b/fe-app-tachyon/src/constants.ts index d0fee7cc..3f9ea053 100644 --- a/fe-app-tachyon/src/constants.ts +++ b/fe-app-tachyon/src/constants.ts @@ -16,8 +16,8 @@ export const DOMAIN_LIST_OPTIONS = { russia_inside: 'Russia inside', russia_outside: 'Russia outside', ukraine_inside: 'Ukraine', - geoblock: 'Geo Block', - block: 'Block', + geoblock: 'Geo Block (снаружи РФ)', + block: 'Block (РКН / внутри РФ)', porn: 'Porn', news: 'News', anime: 'Anime', diff --git a/fe-app-tachyon/src/tachyon/methods/shell/index.ts b/fe-app-tachyon/src/tachyon/methods/shell/index.ts index c77901cb..28bcf99e 100644 --- a/fe-app-tachyon/src/tachyon/methods/shell/index.ts +++ b/fe-app-tachyon/src/tachyon/methods/shell/index.ts @@ -350,7 +350,7 @@ export const TachyonShellMethods = { doctor: async () => callBaseMethod( Tachyon.AvailableMethods.DOCTOR, - [], + ['--fix'], '/usr/bin/tachyon', { timeout: 30000 }, ), diff --git a/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/getDnsCheckPresentation.ts b/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/getDnsCheckPresentation.ts index f34d8a85..c3689282 100644 --- a/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/getDnsCheckPresentation.ts +++ b/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/getDnsCheckPresentation.ts @@ -34,7 +34,9 @@ export function getDnsCheckPresentation(data: Tachyon.DnsCheckResult) { : 'error'; const dhcpItemKey = dhcpManagedManually ? _('DHCP is managed manually') - : _('DHCP has DNS server'); + : data.dhcp_config_status + ? _('DHCP forwards DNS to Tachyon') + : _('DHCP does not forward DNS to Tachyon'); return { state, diff --git a/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/tests/getDnsCheckPresentation.test.ts b/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/tests/getDnsCheckPresentation.test.ts index 246c3e0a..8a61b7e6 100644 --- a/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/tests/getDnsCheckPresentation.test.ts +++ b/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/tests/getDnsCheckPresentation.test.ts @@ -36,7 +36,21 @@ describe('getDnsCheckPresentation', () => { state: 'warning', description: 'Issues detected', dhcpItemState: 'error', - dhcpItemKey: 'DHCP has DNS server', + dhcpItemKey: 'DHCP does not forward DNS to Tachyon', + }); + }); + + it('shows success when DHCP forwards DNS to Tachyon', () => { + expect( + getDnsCheckPresentation({ + ...baseDnsResult, + dhcp_config_status: 1, + }), + ).toMatchObject({ + state: 'success', + description: 'Checks passed', + dhcpItemState: 'success', + dhcpItemKey: 'DHCP forwards DNS to Tachyon', }); }); }); diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index 0dc5f27e..bfbab63a 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -1095,8 +1095,8 @@ var DOMAIN_LIST_OPTIONS = { russia_inside: "Russia inside", russia_outside: "Russia outside", ukraine_inside: "Ukraine", - geoblock: "Geo Block", - block: "Block", + geoblock: "Geo Block (\u0441\u043D\u0430\u0440\u0443\u0436\u0438 \u0420\u0424)", + block: "Block (\u0420\u041A\u041D / \u0432\u043D\u0443\u0442\u0440\u0438 \u0420\u0424)", porn: "Porn", news: "News", anime: "Anime", @@ -3497,7 +3497,7 @@ var TachyonShellMethods = { ]), doctor: async () => callBaseMethod( Tachyon.AvailableMethods.DOCTOR, - [], + ["--fix"], "/usr/bin/tachyon", { timeout: 3e4 } ), @@ -9662,7 +9662,7 @@ function getDnsCheckPresentation(data) { const state = dhcpManagedManually && meta.state === "success" ? "warning" : meta.state; const description = dhcpManagedManually && meta.state === "success" ? _("Checks passed with manual DHCP") : meta.description; const dhcpItemState = dhcpManagedManually ? "warning" : data.dhcp_config_status ? "success" : "error"; - const dhcpItemKey = dhcpManagedManually ? _("DHCP is managed manually") : _("DHCP has DNS server"); + const dhcpItemKey = dhcpManagedManually ? _("DHCP is managed manually") : data.dhcp_config_status ? _("DHCP forwards DNS to Tachyon") : _("DHCP does not forward DNS to Tachyon"); return { state, description, diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js index 18bb2183..7e0d0acc 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js @@ -7126,8 +7126,8 @@ var DOMAIN_LIST_HINTS = { russia_inside: "youtube.com, instagram.com, twitter.com, ...", russia_outside: "gosuslugi.ru, ozon.ru, rzd.ru, ...", ukraine_inside: "ukr.net, pravda.com.ua, suspilne.media, ...", - geoblock: "Все домены, заблокированные в РФ", - block: "Заблокировать совсем (без прокси)", + geoblock: "Домены, заблокированные снаружи России (геоблокировка зарубежными сервисами)", + block: "Домены, заблокированные внутри России (полный реестр блокировок РКН)", porn: "Pornhub, xvideos, xhamster, ...", news: "meduza.io, bbc.com, pravda.com.ua, ...", anime: "bato.to, mangadex.org, myanimelist.net, ...", @@ -7157,8 +7157,14 @@ function loadRulesetValues(option) { delete option.vallist; Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { + let displayLabel = _(label); + if (key === "block") { + displayLabel = "Block (РКН / внутри РФ)"; + } else if (key === "geoblock") { + displayLabel = "Geo Block (снаружи РФ)"; + } const hint = DOMAIN_LIST_HINTS[key]; - option.value(key, hint ? `${_(label)} — ${hint}` : _(label)); + option.value(key, hint ? `${displayLabel} — ${hint}` : displayLabel); }); } @@ -9869,7 +9875,8 @@ function createSectionContent(section) { form.DynamicList, "community_lists", _("Built-in rule sets"), - _("Select a predefined domain list"), + _("Select a predefined domain list") + + ' (source: itdoginfo/allow-domains)', ); builtInRulesetOption.modalonly = true; builtInRulesetOption.placeholder = _("Service list"); @@ -10408,6 +10415,44 @@ async function performTrace(query) { queryForMatching = query.split("/")[0]; } + // Check DNS Detour from settings if enabled + const dnsDetourEnabled = uci.get(UCI_PACKAGE, "settings", "dns_detour_enabled"); + if (dnsDetourEnabled === "1") { + const detourSection = uci.get(UCI_PACKAGE, "settings", "dns_detour_section"); + if (detourSection) { + const dnsServers = normalizeOptionValues(uci.get(UCI_PACKAGE, "settings", "dns_server")); + const fallbackServers = normalizeOptionValues(uci.get(UCI_PACKAGE, "settings", "dns_fallback_server")); + const allDns = [...dnsServers, ...fallbackServers]; + const detourIps = []; + const detourDomains = []; + for (let s of allDns) { + if (!s) continue; + s = s.split("#")[0].split("/")[0].trim(); + if (s.match(/^[0-9.]+$|^[0-9a-fA-F:]+$/)) { + detourIps.push(s); + } else if (s) { + detourDomains.push(s); + } + } + const isDnsMatch = (type === "ip" && detourIps.includes(queryForMatching)) || + (type === "domain" && detourDomains.includes(queryForMatching)); + if (isDnsMatch) { + const label = uci.get(UCI_PACKAGE, detourSection, "label") || detourSection; + const action = uci.get(UCI_PACKAGE, detourSection, "action") || "connection"; + return { + matched: true, + sectionName: detourSection, + label: label, + action: action, + ruleType: "DNS Detour (Settings)", + pattern: queryForMatching, + priority: 0, + totalSections: totalSections, + }; + } + } + } + for (let i = 0; i < sections.length; i++) { const sec = sections[i]; const secName = sec[".name"]; diff --git a/tachyon/files/usr/lib/diagnostics/runtime.uc b/tachyon/files/usr/lib/diagnostics/runtime.uc index 25a5397b..347b7b5e 100644 --- a/tachyon/files/usr/lib/diagnostics/runtime.uc +++ b/tachyon/files/usr/lib/diagnostics/runtime.uc @@ -1239,7 +1239,12 @@ function get_server_capabilities() { } function neutralize_zapret_defaults() { - log_message("Standalone zapret is not neutralized automatically; Tachyon uses /opt/zapret/nfq/nfqws as an external provider and manages only its own NFQUEUE range.", "info"); + log_message("Neutralizing standalone zapret/zapret2/byedpi services and tables", "info"); + command_status("/etc/init.d/zapret stop >/dev/null 2>&1; /etc/init.d/zapret disable >/dev/null 2>&1 || true"); + command_status("/etc/init.d/zapret2 stop >/dev/null 2>&1; /etc/init.d/zapret2 disable >/dev/null 2>&1 || true"); + command_status("/etc/init.d/byedpi stop >/dev/null 2>&1; /etc/init.d/byedpi disable >/dev/null 2>&1 || true"); + command_status("nft delete table inet zapret >/dev/null 2>&1 || true"); + command_status("nft delete table inet zapret2 >/dev/null 2>&1 || true"); return 0; } @@ -2839,6 +2844,57 @@ function run_doctor_checks_impl(repair) { } } + // 6d. DPI bypass providers health (Zapret, Zapret2, ByeDPI) + let providers_to_check = [ + { name: "Zapret", kind: "zapret", runtime: ZAPRET_RUNTIME_UC }, + { name: "Zapret2", kind: "zapret2", runtime: ZAPRET2_RUNTIME_UC }, + { name: "ByeDPI", kind: "byedpi", runtime: BYEDPI_RUNTIME_UC } + ]; + + for (let p in providers_to_check) { + let raw_st = trim(command_capture("ucode -L " + LIB_DIR + " " + p.runtime + " status 2>/dev/null").output); + if (raw_st == "") continue; + let st = null; + try { st = json(raw_st); } catch (e) {} + if (!st || st.configured != true) continue; + + // Check for standalone service conflict + if (st.standalone_conflict == true || st.standalone_service_running == true) { + issues++; + if (!DOCTOR_REPAIR_MODE) { + doc_plan("/etc/init.d/" + p.kind + " stop && /etc/init.d/" + p.kind + " disable && nft delete table inet " + p.kind); + doc_check("⚠️", "Standalone " + p.name, "active conflict", "→ WILL FIX: отключение автономной службы и удаление конфликтующей таблицы"); + } else { + command_status("/etc/init.d/" + p.kind + " stop >/dev/null 2>&1; /etc/init.d/" + p.kind + " disable >/dev/null 2>&1; nft delete table inet " + p.kind + " >/dev/null 2>&1 || true"); + doc_check("❌", "Standalone " + p.name, "active conflict", "→ FIXED: автономная служба отключена, таблица очищена"); + fixed++; + } + } + + // Check if Tachyon-managed runtime is ready + if (st.ready == true) { + doc_check("✅", p.name + " runtime", sprintf("ready (%d/%d workers)", st.running_process_count || 0, st.expected_process_count || 0), ""); + } else { + issues++; + if (!DOCTOR_REPAIR_MODE) { + doc_plan("ucode -L " + LIB_DIR + " " + p.runtime + " start-runtime"); + doc_check("⚠️", p.name + " runtime", sprintf("not ready (%d/%d workers)", st.running_process_count || 0, st.expected_process_count || 0), "→ WILL FIX: запуск воркеров Tachyon " + p.name); + } else { + command_status("ucode -L " + LIB_DIR + " " + p.runtime + " start-runtime >/dev/null 2>&1"); + command_status("sleep 1"); + let raw_st2 = trim(command_capture("ucode -L " + LIB_DIR + " " + p.runtime + " status 2>/dev/null").output); + let st2 = null; + try { st2 = json(raw_st2); } catch (e) {} + if (st2 && st2.ready == true) { + doc_check("❌", p.name + " runtime", sprintf("not ready (%d/%d workers)", st.running_process_count || 0, st.expected_process_count || 0), "→ FIXED: воркеры " + p.name + " запущены"); + fixed++; + } else { + doc_check("❌", p.name + " runtime", "failed to start", "→ не удалось запустить воркеры " + p.name); + } + } + } + } + // 7. Free RAM Check let free_mb = -1; let mem_info = fs.readfile("/proc/meminfo") || ""; diff --git a/tachyon/files/usr/lib/dns/apply.uc b/tachyon/files/usr/lib/dns/apply.uc index 0d67ba8e..d167a9f2 100644 --- a/tachyon/files/usr/lib/dns/apply.uc +++ b/tachyon/files/usr/lib/dns/apply.uc @@ -58,9 +58,15 @@ function truthy(value) { return value == "1" || value == "true" || value == "yes" || value == "on"; } +function starts_with(str, prefix) { + str = as_string(str); + prefix = as_string(prefix); + return substr(str, 0, length(prefix)) == prefix; +} + function list_has(values, needle) { for (let value in words(values)) - if (value == needle) + if (value == needle || (needle != "" && starts_with(value, needle + "#"))) return true; return false; } @@ -106,6 +112,8 @@ function dnsmasq_has_tachyon_managed_state() { return uci_get("dhcp.@dnsmasq[0].tachyon_server") != "" || uci_get("dhcp.@dnsmasq[0].tachyon_noresolv") != "" || uci_get("dhcp.@dnsmasq[0].tachyon_cachesize") != "" || + uci_get("dhcp.@dnsmasq[0].tachyon_rebind_protection") != "" || + uci_get("dhcp.@dnsmasq[0].tachyon_localuse") != "" || uci_get("dhcp.@dnsmasq[0].tachyon_notinterface") != "" || dnsmasq_legacy_instance_exists(); } @@ -118,6 +126,8 @@ function dnsmasq_default_config_is_complete() { return dnsmasq_default_has_tachyon_dns() && uci_get("dhcp.@dnsmasq[0].noresolv") == "1" && uci_get("dhcp.@dnsmasq[0].cachesize") == "0" && + uci_get("dhcp.@dnsmasq[0].rebind_protection") == "0" && + uci_get("dhcp.@dnsmasq[0].localuse") == "1" && !dnsmasq_legacy_instance_exists(); } @@ -146,7 +156,7 @@ function backup_dnsmasq_server_list() { return; for (let server in words(dnsmasq_default_servers())) { - if (server != SB_DNS_INBOUND_ADDRESS) + if (server != SB_DNS_INBOUND_ADDRESS && !starts_with(server, SB_DNS_INBOUND_ADDRESS + "#")) uci_add_list("dhcp.@dnsmasq[0].tachyon_server", server); } } @@ -195,6 +205,8 @@ function dnsmasq_configure_default_instance() { if (!default_has_tachyon_dns) { backup_dnsmasq_config_option("noresolv", "tachyon_noresolv"); backup_dnsmasq_config_option("cachesize", "tachyon_cachesize"); + backup_dnsmasq_config_option("rebind_protection", "tachyon_rebind_protection"); + backup_dnsmasq_config_option("localuse", "tachyon_localuse"); } backup_dnsmasq_config_option("addn_hosts", "tachyon_addn_hosts"); @@ -204,6 +216,8 @@ function dnsmasq_configure_default_instance() { uci_add_list("dhcp.@dnsmasq[0].server", SB_DNS_INBOUND_ADDRESS); uci_set("dhcp.@dnsmasq[0].noresolv", "1"); uci_set("dhcp.@dnsmasq[0].cachesize", "0"); + uci_set("dhcp.@dnsmasq[0].rebind_protection", "0"); + uci_set("dhcp.@dnsmasq[0].localuse", "1"); } function dnsmasq_restore_default_instance() { @@ -219,7 +233,7 @@ function dnsmasq_restore_default_instance() { } else { for (let value in words(server_list)) { - if (value != SB_DNS_INBOUND_ADDRESS) + if (value != SB_DNS_INBOUND_ADDRESS && !starts_with(value, SB_DNS_INBOUND_ADDRESS + "#")) uci_add_list("dhcp.@dnsmasq[0].server", value); } } @@ -237,6 +251,18 @@ function dnsmasq_restore_default_instance() { else if (managed_global_dns) uci_set("dhcp.@dnsmasq[0].cachesize", "150"); + let rebind_protection = uci_get("dhcp.@dnsmasq[0].tachyon_rebind_protection"); + if (rebind_protection != "") + restore_dnsmasq_config_option("rebind_protection", "tachyon_rebind_protection", ""); + else if (managed_global_dns) + uci_delete("dhcp.@dnsmasq[0].rebind_protection"); + + let localuse = uci_get("dhcp.@dnsmasq[0].tachyon_localuse"); + if (localuse != "") + restore_dnsmasq_config_option("localuse", "tachyon_localuse", ""); + else if (managed_global_dns) + uci_delete("dhcp.@dnsmasq[0].localuse"); + restore_dnsmasq_config_option("addn_hosts", "tachyon_addn_hosts", ""); } diff --git a/tachyon/files/usr/lib/providers/byedpi/runtime.uc b/tachyon/files/usr/lib/providers/byedpi/runtime.uc index bc7e39d5..e77e44a7 100644 --- a/tachyon/files/usr/lib/providers/byedpi/runtime.uc +++ b/tachyon/files/usr/lib/providers/byedpi/runtime.uc @@ -304,8 +304,10 @@ function start_runtime() { if (length(sections) == 0 || !provider_available()) return; - if (standalone_service_enabled()) - log_message("Standalone byedpi service is enabled. Tachyon manages ciadpi itself for action 'byedpi'; disable standalone byedpi autostart to avoid boot-time port conflicts.", "warn"); + if (standalone_service_enabled()) { + log_message("Disabling standalone byedpi autostart to avoid port conflicts", "warn"); + command_status_from_args([ BYEDPI_SERVICE_INIT, "disable" ]); + } if (standalone_service_running()) { log_message("Stopping standalone byedpi service before starting Tachyon-managed ciadpi runtime", "info"); diff --git a/tachyon/files/usr/lib/providers/nfqueue/runtime.uc b/tachyon/files/usr/lib/providers/nfqueue/runtime.uc index 15be344c..15eca887 100644 --- a/tachyon/files/usr/lib/providers/nfqueue/runtime.uc +++ b/tachyon/files/usr/lib/providers/nfqueue/runtime.uc @@ -411,6 +411,23 @@ function start_rule(cfg, section, index_value) { return true; } +function neutralize_standalone_service(cfg) { + if (fs.stat(cfg.service_init) != null) { + if (command_success_from_args([ cfg.service_init, "status" ])) { + log_message("Stopping conflicting standalone " + cfg.status_label + " service before starting Tachyon runtime", "info"); + command_status_from_args([ cfg.service_init, "stop" ]); + command_status_from_args([ "sleep", "1" ]); + } + if (command_success_from_args([ cfg.service_init, "enabled" ])) { + log_message("Disabling conflicting standalone " + cfg.status_label + " autostart", "warn"); + command_status_from_args([ cfg.service_init, "disable" ]); + } + } + if (cfg.kind != "") { + command_status_from_args([ "nft", "delete", "table", "inet", cfg.kind ]); + } +} + function start_runtime(cfg) { stop_runtime(cfg); @@ -418,6 +435,7 @@ function start_runtime(cfg) { if (length(sections) == 0 || !provider_available(cfg)) return; + neutralize_standalone_service(cfg); cleanup_legacy_runtime(cfg); if (!ensure_runtime_dirs(cfg)) { log_message("Failed to prepare the Tachyon " + cfg.status_label + " state directory in " + cfg.state_dir + ". Aborted.", "fatal"); @@ -621,6 +639,9 @@ function create_nft_rules(cfg) { if (length(sections) == 0 || !provider_available(cfg)) return; + if (cfg.kind != "") + command_status_from_args([ "nft", "delete", "table", "inet", cfg.kind ]); + command_success_from_args([ "nft", "flush", "chain", "inet", NFT_TABLE_NAME, "mangle_output" ]); command_success_from_args([ "nft", "add", "rule", "inet", NFT_TABLE_NAME, "mangle_output", "meta", "mark", "&", cfg.desync_mark, "==", cfg.desync_mark, "return" ]); @@ -648,6 +669,8 @@ function run(provider, argv) { start_runtime(cfg); else if (mode == "stop-runtime") stop_runtime(cfg); + else if (mode == "neutralize-standalone") + exit(neutralize_standalone_service(cfg) ? 0 : 0); else if (mode == "create-nft-rules") create_nft_rules(cfg); else if (mode == "status") diff --git a/tachyon/files/usr/lib/providers/zapret2/common.uc b/tachyon/files/usr/lib/providers/zapret2/common.uc index c5080ddc..2520bc5c 100644 --- a/tachyon/files/usr/lib/providers/zapret2/common.uc +++ b/tachyon/files/usr/lib/providers/zapret2/common.uc @@ -1,6 +1,7 @@ #!/usr/bin/env ucode let constants = require("core.constants"); +let fs = require("fs"); let validator_module = null; const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon"; @@ -17,6 +18,40 @@ function config(ctx) { let desync_mark = getenv("ZAPRET2_DESYNC_MARK") || runtime_constants.ZAPRET2_DESYNC_MARK; let provider_lua_dir = getenv("ZAPRET2_PROVIDER_LUA_DIR") || runtime_constants.ZAPRET2_PROVIDER_LUA_DIR; + let base_args = [ + "--fwmark=" + desync_mark + ]; + let candidate_dirs = [ + provider_lua_dir, + "/opt/zapret2/lua", + "/usr/share/zapret2/lua", + "/etc/zapret2/lua", + "/usr/lib/zapret2/lua" + ]; + let lua_scripts = [ + "zapret-lib.lua", + "zapret-antidpi.lua", + "zapret-auto.lua" + ]; + for (let script in lua_scripts) { + let found = null; + for (let dir in candidate_dirs) { + if (!dir) + continue; + let p = dir + "/" + script; + if (fs.stat(p) != null) { + found = p; + break; + } + if (fs.stat(p + ".gz") != null) { + found = p + ".gz"; + break; + } + } + if (found != null) + push(base_args, "--lua-init=@" + found); + } + return { kind: "zapret2", action: "zapret2", @@ -53,12 +88,7 @@ function config(ctx) { hostlist_dir: "", status_label: "zapret2", check_prefix: "zapret2", - base_args: [ - "--fwmark=" + desync_mark, - "--lua-init=@" + provider_lua_dir + "/zapret-lib.lua", - "--lua-init=@" + provider_lua_dir + "/zapret-antidpi.lua", - "--lua-init=@" + provider_lua_dir + "/zapret-auto.lua" - ] + base_args }; } diff --git a/tachyon/files/usr/lib/service/watchdog.uc b/tachyon/files/usr/lib/service/watchdog.uc index c4d43839..af018dab 100644 --- a/tachyon/files/usr/lib/service/watchdog.uc +++ b/tachyon/files/usr/lib/service/watchdog.uc @@ -27,6 +27,8 @@ let telegram_msg_window = time(); let telegram_msg_history = {}; let tailscale_fail_streak = 0; let tailscale_last_alert = 0; +let provider_fail_streaks = {}; +let dnsmasq_redirection_fail_streak = 0; let command_from_args = common.command_from_args; let command_status = common.command_status; @@ -2023,12 +2025,76 @@ function check_section_failover() { command_status("/usr/bin/tachyon failover_check >/dev/null 2>&1"); } +// ── DPI provider workers (Zapret, Zapret2, ByeDPI) self-healing ─────────────── +function check_provider_workers() { + let providers = [ + { kind: "zapret", runtime: LIB_DIR + "/providers/zapret/runtime.uc" }, + { kind: "zapret2", runtime: LIB_DIR + "/providers/zapret2/runtime.uc" }, + { kind: "byedpi", runtime: LIB_DIR + "/providers/byedpi/runtime.uc" } + ]; + + for (let p in providers) { + let raw = trim(command_output_from_args([ "/usr/bin/tachyon", "get_" + p.kind + "_status" ]) || ""); + if (raw == "") + continue; + let st = null; + try { st = json(raw); } catch (e) {} + if (!st || st.configured != true) + continue; + + // Auto-neutralize standalone conflict if running or enabled + if (st.standalone_conflict == true || st.standalone_service_running == true) { + log_message(sprintf("Watchdog: Conflicting standalone %s service detected; neutralizing it.", p.kind), "warn"); + command_status_from_args([ "/etc/init.d/" + p.kind, "stop" ]); + command_status_from_args([ "/etc/init.d/" + p.kind, "disable" ]); + command_status_from_args([ "nft", "delete", "table", "inet", p.kind ]); + } + + // Auto-heal dead worker processes + if (st.ready != true) { + let streak = int(provider_fail_streaks[p.kind] || 0) + 1; + provider_fail_streaks[p.kind] = streak; + log_message(sprintf("Watchdog: %s runtime is unhealthy (%s, streak %d); restarting.", + p.kind, as_string(st.status_message || "workers down"), streak), "warn"); + command_status_from_args([ "ucode", "-L", LIB_DIR, p.runtime, "start-runtime" ]); + } else { + if (int(provider_fail_streaks[p.kind] || 0) > 0) + log_message(sprintf("Watchdog: %s runtime recovered.", p.kind), "info"); + provider_fail_streaks[p.kind] = 0; + } + } +} + +// ── Dnsmasq redirection watchdog ───────────────────────────────────────────── +function check_dnsmasq_redirection() { + let cfg = settings(); + if (as_string(cfg.dont_touch_dhcp || "0") == "1") + return; + if (!is_process_name_running("sing-box")) + return; + + let has_dns = command_status_from_args([ "ucode", "-L", LIB_DIR, LIB_DIR + "/dns/apply.uc", "default-config-complete" ]); + if (has_dns == 0) { + if (dnsmasq_redirection_fail_streak > 0) + log_message("Watchdog: dnsmasq redirection recovered.", "info"); + dnsmasq_redirection_fail_streak = 0; + return; + } + + dnsmasq_redirection_fail_streak++; + log_message(sprintf("Watchdog: dnsmasq is not redirecting to Tachyon DNS (streak %d); re-applying configuration.", + dnsmasq_redirection_fail_streak), "warn"); + command_status_from_args([ "ucode", "-L", LIB_DIR, LIB_DIR + "/dns/apply.uc", "configure", "force" ]); +} + function perform_slow_checks() { controller.probe_slow(); safe_call(ai_heal_dns_loop, "ai_heal_dns_loop"); safe_call(check_mixed_proxy_port, "check_mixed_proxy_port"); safe_call(check_telegram_worker, "check_telegram_worker"); safe_call(check_tailscale_worker, "check_tailscale_worker"); + safe_call(check_provider_workers, "check_provider_workers"); + safe_call(check_dnsmasq_redirection, "check_dnsmasq_redirection"); safe_call(check_section_failover, "check_section_failover"); } if (uloop) { diff --git a/tachyon/files/usr/lib/singbox/generator_routes.uc b/tachyon/files/usr/lib/singbox/generator_routes.uc index dd0d769b..36861d47 100644 --- a/tachyon/files/usr/lib/singbox/generator_routes.uc +++ b/tachyon/files/usr/lib/singbox/generator_routes.uc @@ -3,6 +3,8 @@ let fs = require("fs"); let common = require("core.common"); let helpers = require("core.helpers"); +let core_ip = require("core.ip"); +let core_url = require("core.url"); let runtime_constants = require("singbox.constants"); let runtime_dns = require("singbox.dns"); let runtime_route = require("singbox.route"); @@ -1503,9 +1505,67 @@ function failover_default_name(candidates) { const FAILOVER_GROUP_TAG = "tachyon-failover"; function add_service_route_rules(config, sections) { + let settings = object_or_empty(ctx.uci_cursor().get_all(CONFIG_NAME, "settings")); + if (bool_option(settings, "dns_detour_enabled", false)) { + let detour_section = option(settings, "dns_detour_section", ""); + if (detour_section != "") { + let detour_target = outbound_tag(detour_section); + let dns_servers = list_option(settings, "dns_server"); + if (length(dns_servers) == 0) { + let single_dns = option(settings, "dns_server", ""); + if (single_dns != "") + dns_servers = [single_dns]; + } + let fallback_servers = list_option(settings, "dns_fallback_server"); + if (length(fallback_servers) == 0) { + let single_fb = option(settings, "dns_fallback_server", ""); + if (single_fb != "") + fallback_servers = [single_fb]; + } + let all_dns = []; + for (let s in dns_servers) + push(all_dns, s); + for (let s in fallback_servers) + push(all_dns, s); + + let detour_ips = []; + let detour_domains = []; + for (let s in all_dns) { + s = trim(as_string(s)); + if (s == "") + continue; + s = split(s, "#")[0]; + let host = core_url.host(s); + if (host == "") + host = s; + host = split(host, "/")[0]; + if (core_ip.valid_ipv4(host) || core_ip.valid_ipv6(host)) + push(detour_ips, host); + else if (host != "") + push(detour_domains, host); + } + if (length(detour_ips) > 0) { + push(config.route.rules, { + action: "route", + inbound: tproxy_inbound_matcher(), + outbound: detour_target, + ip_cidr: single_or_array(detour_ips) + }); + } + if (length(detour_domains) > 0) { + push(config.route.rules, { + action: "route", + inbound: tproxy_inbound_matcher(), + outbound: detour_target, + domain: single_or_array(detour_domains) + }); + } + } + } + let candidates = failover_candidate(sections); let first = length(candidates) > 0 ? candidates[0] : null; - let failover_active = bool_option(object_or_empty(ctx.uci_cursor().get_all(CONFIG_NAME, "settings")), "section_failover_enabled", false) && + let failover_active = bool_option(settings, "section_failover_enabled", false) && length(candidates) > 1; if (first != null) { push(config.route.rules, { diff --git a/tests/dns_apply.sh b/tests/dns_apply.sh index 7510b19f..b46aca32 100644 --- a/tests/dns_apply.sh +++ b/tests/dns_apply.sh @@ -186,6 +186,8 @@ cat >"$STATE" <<'EOF_STATE' dhcp.@dnsmasq[0].server=1.1.1.1 8.8.8.8 dhcp.@dnsmasq[0].noresolv=0 dhcp.@dnsmasq[0].cachesize=150 +dhcp.@dnsmasq[0].rebind_protection=1 +dhcp.@dnsmasq[0].localuse=0 tachyon.settings.shutdown_correctly=1 EOF_STATE @@ -198,6 +200,10 @@ assert_value 'dhcp.@dnsmasq[0].noresolv' '1' assert_value 'dhcp.@dnsmasq[0].tachyon_noresolv' '0' assert_value 'dhcp.@dnsmasq[0].cachesize' '0' assert_value 'dhcp.@dnsmasq[0].tachyon_cachesize' '150' +assert_value 'dhcp.@dnsmasq[0].rebind_protection' '0' +assert_value 'dhcp.@dnsmasq[0].tachyon_rebind_protection' '1' +assert_value 'dhcp.@dnsmasq[0].localuse' '1' +assert_value 'dhcp.@dnsmasq[0].tachyon_localuse' '0' assert_log_contains 'commit dhcp' assert_dnsmasq_reloaded @@ -207,9 +213,13 @@ ucode -L "$UCODE_LIB" "$APPLY" restore force assert_value 'dhcp.@dnsmasq[0].server' '1.1.1.1 8.8.8.8' assert_value 'dhcp.@dnsmasq[0].noresolv' '0' assert_value 'dhcp.@dnsmasq[0].cachesize' '150' +assert_value 'dhcp.@dnsmasq[0].rebind_protection' '1' +assert_value 'dhcp.@dnsmasq[0].localuse' '0' assert_absent 'dhcp.@dnsmasq[0].tachyon_server' assert_absent 'dhcp.@dnsmasq[0].tachyon_noresolv' assert_absent 'dhcp.@dnsmasq[0].tachyon_cachesize' +assert_absent 'dhcp.@dnsmasq[0].tachyon_rebind_protection' +assert_absent 'dhcp.@dnsmasq[0].tachyon_localuse' assert_log_contains 'commit dhcp' assert_dnsmasq_reloaded