From c7ff142a485ed2fcc725f63582c2e27d41aa374c Mon Sep 17 00:00:00 2001 From: Dushnilin Date: Wed, 29 Jul 2026 08:58:00 +0300 Subject: [PATCH] Release 1.2.43 - Pre-download and cache all preset rulesets & databases --- fe-app-tachyon/locales/tachyon.ru.po | 6 + fe-app-tachyon/package.json | 2 +- .../resources/view/tachyon/settings.js | 12 ++ luci-app-tachyon/po/ru/tachyon.po | 6 + tachyon/files/etc/config/tachyon | 1 + tachyon/files/usr/lib/components/updates.uc | 105 +++++++++++++++++- tachyon/files/usr/lib/nft/apply.uc | 2 + .../files/usr/lib/singbox/generator_routes.uc | 42 +++++-- tests/nft_apply.sh | 12 ++ tests/runtime_state_predicates.sh | 16 +++ tests/updates_due.sh | 15 +++ 11 files changed, 205 insertions(+), 14 deletions(-) diff --git a/fe-app-tachyon/locales/tachyon.ru.po b/fe-app-tachyon/locales/tachyon.ru.po index 7f1e5a83..495aa017 100644 --- a/fe-app-tachyon/locales/tachyon.ru.po +++ b/fe-app-tachyon/locales/tachyon.ru.po @@ -845,6 +845,12 @@ msgstr "Включить быстрый DNS-кэш" msgid "Enable list updates" msgstr "Включить обновление списков" +msgid "Pre-download all preset lists" +msgstr "Скачивать все предустановленные списки" + +msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive." +msgstr "Кешировать все встроенные списки и базы (даже если они не выбраны ни в одной секции). Рекомендуется при наличии свободного места или USB-накопителя." + msgid "Enable Mixed Proxy" msgstr "Включить смешанный прокси" diff --git a/fe-app-tachyon/package.json b/fe-app-tachyon/package.json index ce2c8ba1..7f603298 100644 --- a/fe-app-tachyon/package.json +++ b/fe-app-tachyon/package.json @@ -1,6 +1,6 @@ { "name": "fe-app-tachyon", - "version": "1.2.35", + "version": "1.2.43", "license": "MIT", "type": "module", "scripts": { diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/settings.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/settings.js index e7239449..77b5938e 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/settings.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/settings.js @@ -1362,6 +1362,18 @@ function createSettingsContent(section, capabilities) { return _("Use sing-box duration format like 1d, 12h or 30m"); }; + o = section.option( + form.Flag, + "download_all_presets", + _("Pre-download all preset lists"), + _( + "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive.", + ), + ); + o.depends("list_update_enabled", "1"); + o.default = "0"; + o.rmempty = false; + o = section.option( form.Flag, "component_update_check_enabled", diff --git a/luci-app-tachyon/po/ru/tachyon.po b/luci-app-tachyon/po/ru/tachyon.po index 7f1e5a83..495aa017 100644 --- a/luci-app-tachyon/po/ru/tachyon.po +++ b/luci-app-tachyon/po/ru/tachyon.po @@ -845,6 +845,12 @@ msgstr "Включить быстрый DNS-кэш" msgid "Enable list updates" msgstr "Включить обновление списков" +msgid "Pre-download all preset lists" +msgstr "Скачивать все предустановленные списки" + +msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive." +msgstr "Кешировать все встроенные списки и базы (даже если они не выбраны ни в одной секции). Рекомендуется при наличии свободного места или USB-накопителя." + msgid "Enable Mixed Proxy" msgstr "Включить смешанный прокси" diff --git a/tachyon/files/etc/config/tachyon b/tachyon/files/etc/config/tachyon index 1f618bc6..d94ed34a 100644 --- a/tachyon/files/etc/config/tachyon +++ b/tachyon/files/etc/config/tachyon @@ -23,6 +23,7 @@ config settings 'settings' option disable_quic '0' option list_update_enabled '1' option update_interval '1d' + option download_all_presets '0' option component_update_check_enabled '1' option component_update_check_interval '1d' option latency_test_url 'https://www.gstatic.com/generate_204' diff --git a/tachyon/files/usr/lib/components/updates.uc b/tachyon/files/usr/lib/components/updates.uc index 611de181..3aacfc6f 100644 --- a/tachyon/files/usr/lib/components/updates.uc +++ b/tachyon/files/usr/lib/components/updates.uc @@ -595,7 +595,11 @@ function rule_has_list_update_source(section) { ); } -function has_list_update_sources(sections) { +function has_list_update_sources(sections, settings) { + if (settings == null) + settings = uci_settings(); + if (bool_option(settings, "download_all_presets", false)) + return true; for (let section in sections) if (rule_has_list_update_source(section)) return true; @@ -689,7 +693,7 @@ function cron_refresh_plan_rows(settings, sections, bin, list_marker, subscripti let status = 0; let rows = []; - if (has_list_update_sources(sections)) { + if (has_list_update_sources(sections, settings)) { let interval = settings_update_interval(settings); if (interval == "") { push(rows, "list-disabled"); @@ -2143,6 +2147,97 @@ function rebuild_domain_ip_lists_from_rule(section, settings) { return ok; } +function import_community_srs_file(service, settings) { + service = as_string(service); + if (!singbox_rulesets_module().is_community(service)) + return true; + + let url = singbox_rulesets_module().community_url(service); + let cached_file = TMP_RULESET_FOLDER + "/community-" + service + ".srs"; + let persistent_file = "/etc/tachyon/rulesets/community-" + service + ".srs"; + let tmpfile = temp_path(); + if (tmpfile == "") + return false; + + let ok = true; + if (download_to_file(url, tmpfile, service_proxy_address(settings, "lists")) && file_nonempty(tmpfile)) { + ensure_dir(TMP_RULESET_FOLDER); + copy_file(tmpfile, cached_file); + ensure_dir("/etc/tachyon/rulesets"); + copy_file(tmpfile, persistent_file); + log_message("Successfully cached preset ruleset " + service, "info"); + } + else if (file_exists_value(persistent_file)) { + ensure_dir(TMP_RULESET_FOLDER); + copy_file(persistent_file, cached_file); + } + else { + log_message("Failed to download preset ruleset " + service + "; skipping it until next update", "warn"); + ok = false; + } + + remove_file(tmpfile); + return ok; +} + +function import_all_preset_lists(settings) { + log_message("Pre-downloading all preset lists and databases", "info"); + let ok = true; + + for (let service, urls in BUILTIN_SUBNET_URLS) { + if (type(urls) != "array") + continue; + + let cached_file = TMP_RULESET_FOLDER + "/community-subnets-" + as_string(service) + ".lst"; + let persistent_file = "/etc/tachyon/rulesets/community-subnets-" + as_string(service) + ".lst"; + let combined_lines = []; + + for (let url in urls) { + let tmpfile = temp_path(); + if (tmpfile == "") { + ok = false; + continue; + } + + let downloaded = download_to_file(url, tmpfile, service_proxy_address(settings, "lists")) && file_nonempty(tmpfile); + if (!downloaded) { + remove_file(tmpfile); + if (file_exists_value(persistent_file)) { + tmpfile = persistent_file; + } else { + log_message("Failed to download built-in preset subnet list " + as_string(service) + "; skipping it until next update", "warn"); + continue; + } + } + + let lines = split(as_string(fs.readfile(tmpfile)), "\n"); + for (let l in lines) { + l = trim(replace(as_string(l), /\r/g, "")); + if (l != "" && substr(l, 0, 1) != "#") + push(combined_lines, l); + } + + if (downloaded) + remove_file(tmpfile); + } + + if (length(combined_lines) > 0) { + ensure_dir(TMP_RULESET_FOLDER); + write_file(cached_file, join("\n", combined_lines) + "\n"); + ensure_dir("/etc/tachyon/rulesets"); + write_file(persistent_file, join("\n", combined_lines) + "\n"); + } + } + + let community_services = keys(singbox_rulesets_module().COMMUNITY_SERVICES); + for (let service in community_services) { + if (!import_community_srs_file(service, settings)) + ok = false; + } + + return ok; +} + function import_builtin_subnets_from_rule(section, settings) { if (!bool_option(section, "enabled", true)) return true; @@ -2154,6 +2249,8 @@ function import_builtin_subnets_from_rule(section, settings) { if (!singbox_rulesets_module().is_community(service)) continue; + import_community_srs_file(service, settings); + let urls = BUILTIN_SUBNET_URLS[as_string(service)]; if (type(urls) != "array") continue; @@ -2555,6 +2652,10 @@ function list_update() { let ok = true; // Фаза 1: скачивание файлов (без блокировки, не трогает nft) + if (bool_option(settings, "download_all_presets", false)) { + if (!import_all_preset_lists(settings)) + ok = false; + } for (let section in sections) if (!rebuild_domain_ip_lists_from_rule(section, settings)) ok = false; diff --git a/tachyon/files/usr/lib/nft/apply.uc b/tachyon/files/usr/lib/nft/apply.uc index b0b9fb1c..b80d1b47 100644 --- a/tachyon/files/usr/lib/nft/apply.uc +++ b/tachyon/files/usr/lib/nft/apply.uc @@ -1562,6 +1562,8 @@ function nft_rule_signature_body(body, section) { body = signature_add_value(body, "rule." + section_name + ".ports", section_rule_ports_csv(section)); body = signature_add_value(body, "rule." + section_name + ".fully_routed_ips", option(section, "fully_routed_ips", "")); body = signature_add_value(body, "rule." + section_name + ".excluded_ips", option(section, "excluded_ips", "")); + body = signature_add_value(body, "rule." + section_name + ".excluded_protocol", option(section, "excluded_protocol", "")); + body = signature_add_value(body, "rule." + section_name + ".protocol", option(section, "protocol", "")); body = signature_add_value(body, "rule." + section_name + ".community_subnet_lists", filter_community_subnet_lists_value(connections.community_lists_value(section))); body = signature_add_value(body, "rule." + section_name + ".remote_subnet_lists", option(section, "remote_subnet_lists", "")); body = signature_add_value(body, "rule." + section_name + ".rule_set_with_subnets", connections.rule_sets_with_subnets_value(section)); diff --git a/tachyon/files/usr/lib/singbox/generator_routes.uc b/tachyon/files/usr/lib/singbox/generator_routes.uc index 2afdbee2..94791a8f 100644 --- a/tachyon/files/usr/lib/singbox/generator_routes.uc +++ b/tachyon/files/usr/lib/singbox/generator_routes.uc @@ -754,17 +754,37 @@ function ensure_community_ruleset(config, section_name, community) { let tag_name = ruleset_tag(section_name, community, "community"); if (!ruleset_registered(config, tag_name)) { - let rule_set = { - type: "remote", - tag: tag_name, - format: "binary", - url: runtime_rulesets.community_url(community), - update_interval: remote_ruleset_update_interval() - }; - let detour = ctx.download_detour_tag(ctx.runtime_settings(), "lists"); - if (detour != "") - rule_set.download_detour = detour; - push(config.route.rule_set, rule_set); + let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder; + let tmp_srs = folder + "/community-" + community + ".srs"; + let etc_srs = "/etc/tachyon/rulesets/community-" + community + ".srs"; + let local_path = null; + + if (fs.stat(tmp_srs) != null && int(fs.stat(tmp_srs).size || 0) > 0) + local_path = tmp_srs; + else if (fs.stat(etc_srs) != null && int(fs.stat(etc_srs).size || 0) > 0) + local_path = etc_srs; + + if (local_path != null) { + push(config.route.rule_set, { + type: "local", + tag: tag_name, + format: "binary", + path: local_path + }); + } + else { + let rule_set = { + type: "remote", + tag: tag_name, + format: "binary", + url: runtime_rulesets.community_url(community), + update_interval: remote_ruleset_update_interval() + }; + let detour = ctx.download_detour_tag(ctx.runtime_settings(), "lists"); + if (detour != "") + rule_set.download_detour = detour; + push(config.route.rule_set, rule_set); + } } return { tag: tag_name, diff --git a/tests/nft_apply.sh b/tests/nft_apply.sh index 2bfce348..d28cfe88 100644 --- a/tests/nft_apply.sh +++ b/tests/nft_apply.sh @@ -654,6 +654,10 @@ bypass 192.168.1.10/32 [rule.text_rule.excluded_ips] +[rule.text_rule.excluded_protocol] + +[rule.text_rule.protocol] + [rule.text_rule.community_subnet_lists] meta telegram discord [rule.text_rule.remote_subnet_lists] @@ -676,6 +680,10 @@ proxy [rule.default_enabled.excluded_ips] +[rule.default_enabled.excluded_protocol] + +[rule.default_enabled.protocol] + [rule.default_enabled.community_subnet_lists] cloudflare roblox [rule.default_enabled.remote_subnet_lists] @@ -728,6 +736,10 @@ bypass 192.168.1.10/32 [rule.enabled.excluded_ips] +[rule.enabled.excluded_protocol] + +[rule.enabled.protocol] + [rule.enabled.community_subnet_lists] meta telegram discord [rule.enabled.remote_subnet_lists] diff --git a/tests/runtime_state_predicates.sh b/tests/runtime_state_predicates.sh index 9880e2a5..ec5862a3 100644 --- a/tests/runtime_state_predicates.sh +++ b/tests/runtime_state_predicates.sh @@ -663,6 +663,10 @@ ads 198.51.100.2 [rule.proxy1.excluded_ips] +[rule.proxy1.excluded_protocol] + +[rule.proxy1.protocol] + [rule.proxy1.community_lists] telegram [rule.proxy1.rule_set] @@ -741,6 +745,10 @@ disabled [rule.out1.excluded_ips] +[rule.out1.excluded_protocol] + +[rule.out1.protocol] + [rule.out1.community_lists] [rule.out1.rule_set] @@ -775,6 +783,10 @@ byedpi [rule.bye1.excluded_ips] +[rule.bye1.excluded_protocol] + +[rule.bye1.protocol] + [rule.bye1.community_lists] [rule.bye1.rule_set] @@ -853,6 +865,10 @@ disabled [rule.vpn1.excluded_ips] +[rule.vpn1.excluded_protocol] + +[rule.vpn1.protocol] + [rule.vpn1.community_lists] [rule.vpn1.rule_set] diff --git a/tests/updates_due.sh b/tests/updates_due.sh index bbde5131..732867c2 100644 --- a/tests/updates_due.sh +++ b/tests/updates_due.sh @@ -163,6 +163,21 @@ assert_eq $'list\t*/30 * * * * /usr/bin/tachyon list_update_if_due # list\nsubsc grep -Fq 'fs.writefile(tmp, as_string(text)) == null' "$UPDATES_UC" || fail "empty crontab writes must not be treated as fs.writefile failure" +cat >"$WORK_DIR/cron-plan-all-presets.json" <<'JSON' +{ + "settings": { + "list_update_enabled": "1", + "update_interval": "1h", + "download_all_presets": "1" + }, + "section": [] +} +JSON + +assert_eq $'list\t0 * * * * /usr/bin/tachyon list_update_if_due # list' \ + "$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-all-presets.json" /usr/bin/tachyon '# list' '# sub' '# comp')" \ + "cron refresh plan with download_all_presets" + cat >"$WORK_DIR/cron-plan-list-disabled.json" <<'JSON' { "settings": {