From 4202530585e4e26681184699c1a03a7fb439d65f Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 7 Mar 2026 14:55:11 +0300 Subject: [PATCH 01/75] add HWID and subscription support --- README.md | 55 +++++- fe-app-podkop/src/constants.ts | 9 + .../methods/custom/getDashboardSections.ts | 35 ++++ .../src/podkop/methods/shell/index.ts | 2 + fe-app-podkop/src/podkop/types.ts | 9 + install.sh | 10 +- .../luci-static/resources/view/podkop/main.js | 42 ++++- .../resources/view/podkop/section.js | 48 ++++- podkop/files/etc/config/podkop | 12 +- podkop/files/usr/bin/podkop | 174 +++++++++++++++++- podkop/files/usr/lib/constants.sh | 1 + podkop/files/usr/lib/helpers.sh | 91 +++++++++ .../files/usr/lib/sing_box_config_facade.sh | 96 ++++++++++ 13 files changed, 563 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a938bc1f..2b8ac9ca 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ +# Podkop Evolution + +> **Podkop's fork with HWID and Subscription URL support** +> +> Этот форк добавляет поддержку ссылок подписки (subscription URL) с кастомными заголовками (HWID, Device-OS, Device-Model) и автоматическим обновлением. Основан на [itdoginfo/podkop](https://github.com/itdoginfo/podkop). + +--- + # Вещи, которые вам нужно знать перед установкой - Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться. @@ -16,12 +24,45 @@ # Документация https://podkop.net/ -# Установка Podkop +# Установка Podkop Evolution Полная информация в [документации](https://podkop.net/docs/install/) Вкратце, достаточно одного скрипта для установки и обновления: ``` -sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh) +sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/install.sh) +``` + +## Новое в этом форке: Подписки (Subscription) + +Добавлена поддержка subscription URL — ссылки подписки от провайдера прокси. При выборе типа конфигурации **Subscription** в LuCI: + +- Введите URL подписки от вашего провайдера +- Выберите интервал автообновления (от 30 минут до 1 дня) +- Все серверы из подписки автоматически появятся в дашборде +- Автоматический выбор лучшего сервера по задержке (URLTest) +- Ручное переключение между серверами через дашборд + +При скачивании подписки отправляются заголовки: +- `User-Agent: singbox/<версия>` +- `X-HWID` — уникальный идентификатор роутера +- `X-Device-OS: OpenWrt Linux` +- `X-Device-Model` — модель роутера +- `X-Ver-OS` — версия ядра + +Пример конфигурации через UCI: +``` +uci set podkop.my_sub=section +uci set podkop.my_sub.connection_type='proxy' +uci set podkop.my_sub.proxy_config_type='subscription' +uci set podkop.my_sub.subscription_url='https://your-provider.com/api/sub' +uci set podkop.my_sub.subscription_update_interval='1h' +uci add_list podkop.my_sub.community_lists='russia_inside' +uci commit podkop +``` + +Ручное обновление подписки: +``` +/usr/bin/podkop subscription_update ``` ## Изменения 0.7.0 @@ -38,7 +79,7 @@ mv /etc/config/podkop /etc/config/podkop-070 ``` 2. Стянуть новый дефолтный конфиг: ``` -wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop +wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/podkop/files/etc/config/podkop ``` 3. Настроить заново ваш Podkop через Luci или UCI. @@ -48,14 +89,12 @@ wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/re > PR принимаются только по issues, у которых стоит label "enhancement". Либо по согласованию с авторами в ТГ-чате. Остальные PR на данный момент не рассматриваются. ## Будущее -- [ ] [Подписка](https://github.com/itdoginfo/podkop/issues/118). Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров. +- [x] [Подписка](https://github.com/itdoginfo/podkop/issues/118) — **реализовано в этом форке!** - [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне. -- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусственно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111) +- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. [Issue](https://github.com/itdoginfo/podkop/issues/111) - [ ] Галочка, которая режет доступ к doh серверам. - [ ] IPv6. Только после наполнения Wiki. ## Тесты - [ ] Unit тесты (BATS) -- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS) - -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop) \ No newline at end of file +- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS) \ No newline at end of file diff --git a/fe-app-podkop/src/constants.ts b/fe-app-podkop/src/constants.ts index 6f840d14..19e0907c 100644 --- a/fe-app-podkop/src/constants.ts +++ b/fe-app-podkop/src/constants.ts @@ -66,6 +66,15 @@ export const UPDATE_INTERVAL_OPTIONS = { '3d': 'Every 3 days', }; +export const SUBSCRIPTION_UPDATE_INTERVAL_OPTIONS = { + '30m': 'Every 30 minutes', + '1h': 'Every hour', + '3h': 'Every 3 hours', + '6h': 'Every 6 hours', + '12h': 'Every 12 hours', + '1d': 'Every day', +}; + export const DNS_SERVER_OPTIONS = { '1.1.1.1': '1.1.1.1 (Cloudflare)', '8.8.8.8': '8.8.8.8 (Google)', diff --git a/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts b/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts index 6696d66b..554caa6d 100644 --- a/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts +++ b/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts @@ -154,6 +154,41 @@ export async function getDashboardSections(): Promise proxy.code === `${section['.name']}-out`, + ); + const outbound = proxies.find( + (proxy) => proxy.code === `${section['.name']}-urltest-out`, + ); + + const outbounds = (outbound?.value?.all ?? []) + .map((code) => proxies.find((item) => item.code === code)) + .map((item) => ({ + code: item?.code || '', + displayName: item?.value?.name || '', + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || '', + selected: selector?.value?.now === item?.code, + })); + + return { + withTagSelect: true, + code: selector?.code || section['.name'], + displayName: section['.name'], + outbounds: [ + { + code: outbound?.code || '', + displayName: _('Fastest'), + latency: outbound?.value?.history?.[0]?.delay || 0, + type: outbound?.value?.type || '', + selected: selector?.value?.now === outbound?.code, + }, + ...outbounds, + ], + }; + } } if (section.connection_type === 'vpn') { diff --git a/fe-app-podkop/src/podkop/methods/shell/index.ts b/fe-app-podkop/src/podkop/methods/shell/index.ts index e9c29406..6b0337a4 100644 --- a/fe-app-podkop/src/podkop/methods/shell/index.ts +++ b/fe-app-podkop/src/podkop/methods/shell/index.ts @@ -84,4 +84,6 @@ export const PodkopShellMethods = { callBaseMethod( Podkop.AvailableMethods.GET_SYSTEM_INFO, ), + subscriptionUpdate: async () => + callBaseMethod(Podkop.AvailableMethods.SUBSCRIPTION_UPDATE), }; diff --git a/fe-app-podkop/src/podkop/types.ts b/fe-app-podkop/src/podkop/types.ts index 672ec0cd..e93ab032 100644 --- a/fe-app-podkop/src/podkop/types.ts +++ b/fe-app-podkop/src/podkop/types.ts @@ -65,6 +65,7 @@ export namespace Podkop { SHOW_SING_BOX_CONFIG = 'show_sing_box_config', CHECK_LOGS = 'check_logs', GET_SYSTEM_INFO = 'get_system_info', + SUBSCRIPTION_UPDATE = 'subscription_update', } export enum AvailableClashAPIMethods { @@ -113,6 +114,13 @@ export namespace Podkop { outbound_json: string; } + export interface ConfigProxySubscriptionSection { + connection_type: 'proxy'; + proxy_config_type: 'subscription'; + subscription_url: string; + subscription_update_interval?: string; + } + export interface ConfigVpnSection { connection_type: 'vpn'; interface: string; @@ -127,6 +135,7 @@ export namespace Podkop { | ConfigProxySelectorSection | ConfigProxyUrlSection | ConfigProxyOutboundSection + | ConfigProxySubscriptionSection | ConfigVpnSection | ConfigBlockSection; diff --git a/install.sh b/install.sh index 6376d7c2..6fde5176 100755 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ #!/bin/sh # shellcheck shell=dash -REPO="https://api.github.com/repos/itdoginfo/podkop/releases/latest" +REPO="https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest" DOWNLOAD_DIR="/tmp/podkop" COUNT=3 @@ -66,7 +66,7 @@ update_config() { printf "\033[48;5;196m\033[1m║ ! Обнаружена старая версия podkop. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Если продолжите обновление, вам потребуется настроить Podkop заново. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Старая конфигурация будет сохранена в /etc/config/podkop-070 ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/itdoginfo/podkop ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -76,7 +76,7 @@ update_config() { printf "\033[48;5;196m\033[1m║ ! Detected old podkop version. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ If you continue the update, you will need to RECONFIGURE podkop. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Your old configuration will be saved to /etc/config/podkop-070 ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Details: https://github.com/itdoginfo/podkop ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -88,7 +88,7 @@ update_config() { yes|y|Y) mv /etc/config/podkop /etc/config/podkop-070 - wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop + wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/podkop/files/etc/config/podkop msg "Podkop config has been reset to default. Your old config saved in /etc/config/podkop-070" break ;; @@ -115,7 +115,7 @@ main() { fi if command -v curl >/dev/null 2>&1; then - check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest") + check_response=$(curl -s "https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest") if echo "$check_response" | grep -q 'API rate limit '; then msg "You've reached the GitHub rate limit. Repeat in five minutes." diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index c8fae381..6ff47115 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -617,6 +617,7 @@ var Podkop; AvailableMethods2["SHOW_SING_BOX_CONFIG"] = "show_sing_box_config"; AvailableMethods2["CHECK_LOGS"] = "check_logs"; AvailableMethods2["GET_SYSTEM_INFO"] = "get_system_info"; + AvailableMethods2["SUBSCRIPTION_UPDATE"] = "subscription_update"; })(AvailableMethods = Podkop2.AvailableMethods || (Podkop2.AvailableMethods = {})); let AvailableClashAPIMethods; ((AvailableClashAPIMethods2) => { @@ -691,7 +692,8 @@ var PodkopShellMethods = { checkLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS), getSystemInfo: async () => callBaseMethod( Podkop.AvailableMethods.GET_SYSTEM_INFO - ) + ), + subscriptionUpdate: async () => callBaseMethod(Podkop.AvailableMethods.SUBSCRIPTION_UPDATE) }; // src/podkop/methods/custom/getDashboardSections.ts @@ -811,6 +813,36 @@ async function getDashboardSections() { ] }; } + if (section.proxy_config_type === "subscription") { + const selector = proxies.find( + (proxy) => proxy.code === `${section[".name"]}-out` + ); + const outbound = proxies.find( + (proxy) => proxy.code === `${section[".name"]}-urltest-out` + ); + const outbounds = (outbound?.value?.all ?? []).map((code) => proxies.find((item) => item.code === code)).map((item) => ({ + code: item?.code || "", + displayName: item?.value?.name || "", + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || "", + selected: selector?.value?.now === item?.code + })); + return { + withTagSelect: true, + code: selector?.code || section[".name"], + displayName: section[".name"], + outbounds: [ + { + code: outbound?.code || "", + displayName: _("Fastest"), + latency: outbound?.value?.history?.[0]?.delay || 0, + type: outbound?.value?.type || "", + selected: selector?.value?.now === outbound?.code + }, + ...outbounds + ] + }; + } } if (section.connection_type === "vpn") { const outbound = proxies.find( @@ -921,6 +953,14 @@ var UPDATE_INTERVAL_OPTIONS = { "1d": "Every day", "3d": "Every 3 days" }; +var SUBSCRIPTION_UPDATE_INTERVAL_OPTIONS = { + "30m": "Every 30 minutes", + "1h": "Every hour", + "3h": "Every 3 hours", + "6h": "Every 6 hours", + "12h": "Every 12 hours", + "1d": "Every day" +}; var DNS_SERVER_OPTIONS = { "1.1.1.1": "1.1.1.1 (Cloudflare)", "8.8.8.8": "8.8.8.8 (Google)", diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js index dc619bcb..16f71fd4 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js @@ -26,6 +26,7 @@ function createSectionContent(section) { o.value("url", _("Connection URL")); o.value("selector", _("Selector")); o.value("urltest", _("URLTest")); + o.value("subscription", _("Subscription")); o.value("outbound", _("Outbound Config")); o.default = "url"; o.depends("connection_type", "proxy"); @@ -82,6 +83,44 @@ function createSectionContent(section) { return validation.message; }; + o = section.option( + form.Value, + "subscription_url", + _("Subscription URL"), + _("Enter the subscription URL to fetch proxy configurations from your provider"), + ); + o.depends("proxy_config_type", "subscription"); + o.placeholder = "https://example.com/api/sub"; + o.rmempty = false; + o.validate = function (section_id, value) { + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateUrl(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + o = section.option( + form.ListValue, + "subscription_update_interval", + _("Subscription Update Interval"), + _("How often to automatically update the subscription"), + ); + o.value("30m", _("Every 30 minutes")); + o.value("1h", _("Every hour")); + o.value("3h", _("Every 3 hours")); + o.value("6h", _("Every 6 hours")); + o.value("12h", _("Every 12 hours")); + o.value("1d", _("Every day")); + o.default = "1h"; + o.depends("proxy_config_type", "subscription"); + o = section.option( form.DynamicList, "selector_proxy_links", @@ -140,6 +179,7 @@ function createSectionContent(section) { o.value("5m", _("Every 5 minutes")); o.default = "3m"; o.depends("proxy_config_type", "urltest"); + o.depends("proxy_config_type", "subscription"); o = section.option( form.Value, @@ -150,6 +190,7 @@ function createSectionContent(section) { o.default = "50"; o.rmempty = false; o.depends("proxy_config_type", "urltest"); + o.depends("proxy_config_type", "subscription"); o.validate = function (section_id, value) { if (!value || value.length === 0) { return true; @@ -177,6 +218,7 @@ function createSectionContent(section) { o.default = "https://www.gstatic.com/generate_204"; o.rmempty = false; o.depends("proxy_config_type", "urltest"); + o.depends("proxy_config_type", "subscription"); o.validate = function (section_id, value) { if (!value || value.length === 0) { @@ -298,7 +340,7 @@ function createSectionContent(section) { "community_lists", _("Community Lists"), _("Select a predefined list for routing") + - ' github.com/itdoginfo/allow-domains', + ' github.com/itdoginfo/allow-domains', ); o.placeholder = "Service list"; Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { @@ -505,7 +547,7 @@ function createSectionContent(section) { _("User Subnets List"), _( "Enter subnets in CIDR notation or single IP addresses, separated by commas, spaces, or newlines. " + - "You can add comments using //", + "You can add comments using //", ), ); o.placeholder = @@ -678,7 +720,7 @@ function createSectionContent(section) { _("Mixed Proxy Port"), _( "Specify the port number on which the mixed proxy will run for this section. " + - "Make sure the selected port is not used by another service", + "Make sure the selected port is not used by another service", ), ); o.rmempty = false; diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop index 27003cfa..ff8d9766 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -37,4 +37,14 @@ config section 'main' #list remote_subnet_lists 'https://example.com/subnets.srs' #list fully_routed_ips '192.168.1.2' #option mixed_proxy_enabled '1' - #option mixed_proxy_port '2080' \ No newline at end of file + #option mixed_proxy_port '2080' + +#config section 'subscription_example' +# option connection_type 'proxy' +# option proxy_config_type 'subscription' +# option subscription_url 'https://example.com/api/sub' +# option subscription_update_interval '1h' +# #option urltest_check_interval '3m' +# #option urltest_tolerance '50' +# #option urltest_testing_url 'https://www.gstatic.com/generate_204' +# list community_lists 'russia_inside' \ No newline at end of file diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index e26164a0..7dd2c579 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -125,6 +125,7 @@ start_main() { mkdir -p "$TMP_SING_BOX_FOLDER" mkdir -p "$TMP_RULESET_FOLDER" + mkdir -p "$TMP_SUBSCRIPTION_FOLDER" # base route_table_rule_mark @@ -134,6 +135,7 @@ start_main() { # sing-box sing_box_init_config config_foreach add_cron_job "section" + config_foreach add_subscription_cron_job "section" /etc/init.d/sing-box start log "Nice" @@ -474,10 +476,54 @@ add_cron_job() { } remove_cron_job() { - (crontab -l | grep -v "/usr/bin/podkop list_update") | crontab - + (crontab -l | grep -v "/usr/bin/podkop list_update" | grep -v "/usr/bin/podkop subscription_update") | crontab - log "The cron job removed" } +add_subscription_cron_job() { + local section="$1" + local proxy_config_type subscription_update_interval cron_job + + config_get proxy_config_type "$section" "proxy_config_type" + if [ "$proxy_config_type" != "subscription" ]; then + return + fi + + config_get subscription_update_interval "$section" "subscription_update_interval" "1h" + + case "$subscription_update_interval" in + "30m") + cron_job="*/30 * * * * /usr/bin/podkop subscription_update" + ;; + "1h") + cron_job="17 * * * * /usr/bin/podkop subscription_update" + ;; + "3h") + cron_job="17 */3 * * * /usr/bin/podkop subscription_update" + ;; + "6h") + cron_job="17 */6 * * * /usr/bin/podkop subscription_update" + ;; + "12h") + cron_job="17 */12 * * * /usr/bin/podkop subscription_update" + ;; + "1d") + cron_job="17 9 * * * /usr/bin/podkop subscription_update" + ;; + *) + log "Invalid subscription_update_interval value: $subscription_update_interval" + return + ;; + esac + + # Avoid duplicate subscription cron + (crontab -l | grep -v "/usr/bin/podkop subscription_update") | { + cat + echo "$cron_job" + } | crontab - + log "The subscription cron job has been created: $cron_job" +} + list_update() { echolog "🔄 Starting lists update..." @@ -546,6 +592,76 @@ list_update() { fi } +subscription_update() { + echolog "🔄 Starting subscription update..." + + local has_subscription=0 + + _check_subscription_section() { + local section="$1" + local proxy_config_type + config_get proxy_config_type "$section" "proxy_config_type" + if [ "$proxy_config_type" = "subscription" ]; then + has_subscription=1 + fi + } + config_foreach _check_subscription_section "section" + + if [ "$has_subscription" -eq 0 ]; then + echolog "ℹ️ No subscription sections found, nothing to update" + return 0 + fi + + _update_subscription_for_section() { + local section="$1" + local proxy_config_type subscription_url subscription_json_path + + config_get proxy_config_type "$section" "proxy_config_type" + if [ "$proxy_config_type" != "subscription" ]; then + return + fi + + config_get subscription_url "$section" "subscription_url" + if [ -z "$subscription_url" ]; then + echolog "❌ Subscription URL not set for section '$section'" + return + fi + + mkdir -p "$TMP_SUBSCRIPTION_FOLDER" + subscription_json_path="$TMP_SUBSCRIPTION_FOLDER/${section}.json" + + echolog "📥 Updating subscription for section '$section'..." + + local service_proxy_address + service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')" + + # Remove old cached file to force re-download + rm -f "$subscription_json_path" + download_subscription "$subscription_url" "$subscription_json_path" "$service_proxy_address" + + if [ ! -f "$subscription_json_path" ] || [ ! -s "$subscription_json_path" ]; then + echolog "❌ Failed to download subscription for section '$section'" + return + fi + + local outbounds_count + outbounds_count=$(jq -r '[.outbounds[] | select( + .type != "selector" and + .type != "urltest" and + .type != "direct" and + .type != "dns" and + .type != "block" + )] | length' "$subscription_json_path" 2>/dev/null) + + echolog "✅ Subscription updated for section '$section': $outbounds_count outbounds" + } + config_foreach _update_subscription_for_section "section" + + echolog "🔄 Restarting podkop to apply updated subscriptions..." + restart + echolog "✅ Subscription update completed" +} + # sing-box funcs sing_box_configure_service() { local sing_box_enabled sing_box_user sing_box_config_path sing_box_conffile @@ -712,6 +828,54 @@ configure_outbound_handler() { "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag")" ;; + subscription) + log "Detected proxy configuration type: subscription" "debug" + local subscription_url subscription_json_path urltest_tag selector_tag \ + urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance urltest_testing_url + + config_get subscription_url "$section" "subscription_url" + config_get urltest_check_interval "$section" "urltest_check_interval" "3m" + config_get urltest_tolerance "$section" "urltest_tolerance" 50 + config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204" + + if [ -z "$subscription_url" ]; then + log "Subscription URL is not set. Aborted." "fatal" + exit 1 + fi + + mkdir -p "$TMP_SUBSCRIPTION_FOLDER" + subscription_json_path="$TMP_SUBSCRIPTION_FOLDER/${section}.json" + + # Download subscription if not cached or force update + if [ ! -f "$subscription_json_path" ]; then + log "Downloading subscription for section '$section'" + local service_proxy_address + service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')" + download_subscription "$subscription_url" "$subscription_json_path" "$service_proxy_address" + + if [ ! -f "$subscription_json_path" ] || [ ! -s "$subscription_json_path" ]; then + log "Failed to download subscription for section '$section'. Aborted." "fatal" + exit 1 + fi + fi + + # Parse subscription outbounds + config="$(sing_box_cf_add_subscription_outbounds "$config" "$section" "$subscription_json_path")" + + if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then + log "No proxy outbounds found in subscription for section '$section'. Aborted." "fatal" + exit 1 + fi + + # Create urltest + selector (like urltest proxy_config_type) + urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" + selector_tag="$(get_outbound_tag_by_section "$section")" + urltest_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" + selector_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS,$urltest_tag")" + config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ + "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag")" + ;; *) log "Unknown proxy configuration type: '$proxy_config_type'. Aborted." "fatal" exit 1 @@ -1594,7 +1758,7 @@ get_service_listen_address() { network_get_ipaddr service_listen_address "$interface" if [ -z "$service_listen_address" ]; then - log "Failed to determine the listening IP address. Please open an issue to report this problem: https://github.com/itdoginfo/podkop/issues" "error" + log "Failed to determine the listening IP address. Please open an issue to report this problem: https://github.com/yandexru45/podkop-evolution/issues" "error" return 1 fi @@ -1867,7 +2031,7 @@ get_system_info() { podkop_version="$PODKOP_VERSION" - podkop_latest_version=$(curl -m 3 -s https://api.github.com/repos/itdoginfo/podkop/releases/latest | grep '"tag_name":' | cut -d'"' -f4) + podkop_latest_version=$(curl -m 3 -s https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest | grep '"tag_name":' | cut -d'"' -f4) [ -z "$podkop_latest_version" ] && podkop_latest_version="unknown" if [ -f /www/luci-static/resources/view/podkop/main.js ]; then @@ -2662,6 +2826,7 @@ Available commands: restart Restart podkop service main Run main podkop process list_update Update domain lists + subscription_update Update subscription proxies check_proxy Check proxy connectivity check_nft Check NFT rules check_nft_rules Check NFT rules status @@ -2702,6 +2867,9 @@ main) list_update) list_update ;; +subscription_update) + subscription_update + ;; check_proxy) check_proxy ;; diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index 563ad0c5..68bc3b51 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -9,6 +9,7 @@ CHECK_PROXY_IP_DOMAIN="ip.podkop.fyi" FAKEIP_TEST_DOMAIN="fakeip.podkop.fyi" TMP_SING_BOX_FOLDER="/tmp/sing-box" TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets" +TMP_SUBSCRIPTION_FOLDER="$TMP_SING_BOX_FOLDER/subscriptions" CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker JQ_REQUIRED_VERSION="1.7.1" COREUTILS_BASE64_REQUIRED_VERSION="9.7" diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index c25edb89..24f656c9 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -353,4 +353,95 @@ parse_domain_or_subnet_file_to_comma_string() { done < "$filepath" echo "$result" +} + +# Returns the device model from OpenWrt sysinfo, or "OpenWrt Router" as fallback +get_device_model() { + local model="" + if [ -f /tmp/sysinfo/model ]; then + model="$(cat /tmp/sysinfo/model 2>/dev/null)" + fi + echo "${model:-OpenWrt Router}" +} + +# Returns the Linux kernel version +get_kernel_version() { + uname -r +} + +# Returns the sing-box version number (e.g. "1.12.0") +get_sing_box_version() { + local version="" + if command -v sing-box >/dev/null 2>&1; then + version="$(sing-box version 2>/dev/null | head -1 | awk '{print $NF}')" + fi + echo "${version:-1.0}" +} + +# Generates a deterministic HWID based on WAN MAC address and device model +# Format: xxxx-xxxx-xxxx-xxxx +# Same router always produces the same HWID +generate_hwid() { + local mac="" model="" raw_hash="" + + # Try to get WAN MAC address + if [ -f /sys/class/net/eth0/address ]; then + mac="$(cat /sys/class/net/eth0/address 2>/dev/null)" + elif [ -f /sys/class/net/br-lan/address ]; then + mac="$(cat /sys/class/net/br-lan/address 2>/dev/null)" + fi + + model="$(get_device_model)" + + # Generate hash from MAC + model + raw_hash="$(printf '%s-%s' "$mac" "$model" | md5sum | cut -c1-16)" + + # Format as xxxx-xxxx-xxxx-xxxx + printf '%s-%s-%s-%s' \ + "$(echo "$raw_hash" | cut -c1-4)" \ + "$(echo "$raw_hash" | cut -c5-8)" \ + "$(echo "$raw_hash" | cut -c9-12)" \ + "$(echo "$raw_hash" | cut -c13-16)" +} + +# Downloads a subscription JSON from the given URL with custom headers +# Arguments: +# $1 - subscription URL +# $2 - output file path +# $3 - http proxy address (optional) +# $4 - retries (optional, default 3) +# $5 - wait between retries (optional, default 2) +download_subscription() { + local url="$1" + local filepath="$2" + local http_proxy_address="$3" + local retries="${4:-3}" + local wait="${5:-2}" + + local sb_version device_model kernel_version hwid + sb_version="$(get_sing_box_version)" + device_model="$(get_device_model)" + kernel_version="$(get_kernel_version)" + hwid="$(generate_hwid)" + + local header_args="" + header_args="--header='User-Agent: singbox/$sb_version'" + header_args="$header_args --header='X-HWID: $hwid'" + header_args="$header_args --header='X-Device-OS: OpenWrt Linux'" + header_args="$header_args --header='X-Device-Model: $device_model'" + header_args="$header_args --header='X-Ver-OS: $kernel_version'" + header_args="$header_args --header='Accept-Language: ru-RU,en,*'" + header_args="$header_args --header='X-Device-Locale: EN'" + + for attempt in $(seq 1 "$retries"); do + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ + eval wget -O "$filepath" $header_args "$url" && break + else + eval wget -O "$filepath" $header_args "$url" && break + fi + + log "Attempt $attempt/$retries to download subscription from $url failed" "warn" + sleep "$wait" + done } \ No newline at end of file diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index 6887e207..7315696d 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -328,3 +328,99 @@ sing_box_cf_add_single_key_reject_rule() { echo "$config" } + +####################################### +# Parse a sing-box subscription JSON and add all proxy outbounds to the configuration. +# Filters out non-proxy types (selector, urltest, direct, dns, block). +# Uses 'tag' field (or 'remark' if present) as display name for each outbound. +# Arguments: +# config: string (JSON), sing-box configuration to modify +# section: string, the UCI section name +# subscription_json_path: string, path to the downloaded subscription JSON file +# Outputs: +# Writes updated JSON configuration to stdout +# Sets global variable SUBSCRIPTION_OUTBOUND_TAGS (comma-separated list of tags) +# Sets global variable SUBSCRIPTION_OUTBOUND_NAMES (newline-separated list of display names) +####################################### +sing_box_cf_add_subscription_outbounds() { + local config="$1" + local section="$2" + local subscription_json_path="$3" + + SUBSCRIPTION_OUTBOUND_TAGS="" + SUBSCRIPTION_OUTBOUND_NAMES="" + + if [ ! -f "$subscription_json_path" ]; then + log "Subscription JSON file not found: $subscription_json_path" "error" + echo "$config" + return 1 + fi + + # Extract proxy outbounds from subscription JSON + # Filter out non-proxy types: selector, urltest, direct, dns, block + local outbounds_count + outbounds_count=$(jq -r '[.outbounds[] | select( + .type != "selector" and + .type != "urltest" and + .type != "direct" and + .type != "dns" and + .type != "block" + )] | length' "$subscription_json_path" 2>/dev/null) + + if [ -z "$outbounds_count" ] || [ "$outbounds_count" -eq 0 ]; then + log "No proxy outbounds found in subscription JSON" "error" + echo "$config" + return 1 + fi + + log "Found $outbounds_count proxy outbounds in subscription" "info" + + local i=1 + local outbound_json display_name outbound_tag + + while [ "$i" -le "$outbounds_count" ]; do + # Extract the i-th proxy outbound as raw JSON + outbound_json=$(jq -c "[.outbounds[] | select( + .type != \"selector\" and + .type != \"urltest\" and + .type != \"direct\" and + .type != \"dns\" and + .type != \"block\" + )][$i - 1]" "$subscription_json_path" 2>/dev/null) + + if [ -z "$outbound_json" ] || [ "$outbound_json" = "null" ]; then + i=$((i + 1)) + continue + fi + + # Get display name: prefer remark, then tag, then fallback + display_name=$(echo "$outbound_json" | jq -r '.remark // .tag // "server-'"$i"'"' 2>/dev/null) + + # Create the tag in podkop format + outbound_tag="$(get_outbound_tag_by_section "$section-$i")" + + # Remove tag from raw outbound (it will be set by sing_box_cm_add_raw_outbound) + local clean_outbound + clean_outbound=$(echo "$outbound_json" | jq -c 'del(.tag) | del(.remark)' 2>/dev/null) + + config=$(sing_box_cm_add_raw_outbound "$config" "$outbound_tag" "$clean_outbound") + + if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then + SUBSCRIPTION_OUTBOUND_TAGS="$outbound_tag" + else + SUBSCRIPTION_OUTBOUND_TAGS="$SUBSCRIPTION_OUTBOUND_TAGS,$outbound_tag" + fi + + if [ -z "$SUBSCRIPTION_OUTBOUND_NAMES" ]; then + SUBSCRIPTION_OUTBOUND_NAMES="$display_name" + else + SUBSCRIPTION_OUTBOUND_NAMES="$(printf '%s\n%s' "$SUBSCRIPTION_OUTBOUND_NAMES" "$display_name")" + fi + + i=$((i + 1)) + done + + log "Added $((i - 1)) subscription outbounds for section '$section'" "info" + + echo "$config" +} From e8a764bbb34783f4e4f66329eefe3fd89e1dcd15 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 7 Mar 2026 16:09:55 +0300 Subject: [PATCH 02/75] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/view/podkop/section.js | 29 ++--- podkop/files/usr/bin/podkop | 104 ++++++++++++++---- 2 files changed, 101 insertions(+), 32 deletions(-) diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js index 16f71fd4..d5ddc196 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js @@ -37,7 +37,7 @@ function createSectionContent(section) { _("Proxy Configuration URL"), _("vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") ); - o.depends("proxy_config_type", "url"); + o.depends({ connection_type: "proxy", proxy_config_type: "url" }); o.rows = 5; // Enable soft wrapping for multi-line proxy URLs (e.g., for URLTest proxy links) o.wrap = "soft"; @@ -66,7 +66,7 @@ function createSectionContent(section) { _("Outbound Configuration"), _("Enter complete outbound configuration in JSON format"), ); - o.depends("proxy_config_type", "outbound"); + o.depends({ connection_type: "proxy", proxy_config_type: "outbound" }); o.rows = 10; o.validate = function (section_id, value) { // Optional @@ -89,7 +89,7 @@ function createSectionContent(section) { _("Subscription URL"), _("Enter the subscription URL to fetch proxy configurations from your provider"), ); - o.depends("proxy_config_type", "subscription"); + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o.placeholder = "https://example.com/api/sub"; o.rmempty = false; o.validate = function (section_id, value) { @@ -119,7 +119,7 @@ function createSectionContent(section) { o.value("12h", _("Every 12 hours")); o.value("1d", _("Every day")); o.default = "1h"; - o.depends("proxy_config_type", "subscription"); + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o = section.option( form.DynamicList, @@ -127,7 +127,7 @@ function createSectionContent(section) { _("Selector Proxy Links"), _("vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") ); - o.depends("proxy_config_type", "selector"); + o.depends({ connection_type: "proxy", proxy_config_type: "selector" }); o.rmempty = false; o.validate = function (section_id, value) { // Optional @@ -150,7 +150,7 @@ function createSectionContent(section) { _("URLTest Proxy Links"), _("vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") ); - o.depends("proxy_config_type", "urltest"); + o.depends({ connection_type: "proxy", proxy_config_type: "urltest" }); o.rmempty = false; o.validate = function (section_id, value) { // Optional @@ -178,8 +178,8 @@ function createSectionContent(section) { o.value("3m", _("Every 3 minutes")); o.value("5m", _("Every 5 minutes")); o.default = "3m"; - o.depends("proxy_config_type", "urltest"); - o.depends("proxy_config_type", "subscription"); + o.depends({ connection_type: "proxy", proxy_config_type: "urltest" }); + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o = section.option( form.Value, @@ -189,8 +189,8 @@ function createSectionContent(section) { ); o.default = "50"; o.rmempty = false; - o.depends("proxy_config_type", "urltest"); - o.depends("proxy_config_type", "subscription"); + o.depends({ connection_type: "proxy", proxy_config_type: "urltest" }); + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o.validate = function (section_id, value) { if (!value || value.length === 0) { return true; @@ -217,8 +217,8 @@ function createSectionContent(section) { o.value("https://connectivity-check.ubuntu.com", "https://connectivity-check.ubuntu.com (Ubuntu)") o.default = "https://www.gstatic.com/generate_204"; o.rmempty = false; - o.depends("proxy_config_type", "urltest"); - o.depends("proxy_config_type", "subscription"); + o.depends({ connection_type: "proxy", proxy_config_type: "urltest" }); + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o.validate = function (section_id, value) { if (!value || value.length === 0) { @@ -723,7 +723,10 @@ function createSectionContent(section) { "Make sure the selected port is not used by another service", ), ); - o.rmempty = false; + o.default = "2080"; + o.placeholder = "2080"; + o.datatype = "port"; + o.rmempty = true; o.depends("mixed_proxy_enabled", "1"); } diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 7dd2c579..3e07bb17 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -78,23 +78,63 @@ check_requirements() { if has_outbound_section; then log "Outbound section found" "debug" else - log "Outbound section not found. Please check your configuration file (missing proxy_string, selector_proxy_links, urltest_proxy_links, outbound_json, or interface). Aborted." "error" + log "Outbound section not found. Please check your configuration file (missing proxy_string, selector_proxy_links, urltest_proxy_links, subscription_url, outbound_json, or interface according to connection_type/proxy_config_type). Aborted." "error" exit 1 fi } -_check_outbound_section() { +section_has_configured_outbound() { local section="$1" - local proxy_string interface outbound_json urltest_proxy_links + local connection_type proxy_config_type + + config_get connection_type "$section" "connection_type" + + case "$connection_type" in + proxy) + config_get proxy_config_type "$section" "proxy_config_type" "url" + + case "$proxy_config_type" in + url) + local proxy_string + config_get proxy_string "$section" "proxy_string" + [ -n "$proxy_string" ] && return 0 + ;; + selector) + local selector_proxy_links + config_get selector_proxy_links "$section" "selector_proxy_links" + [ -n "$selector_proxy_links" ] && return 0 + ;; + urltest) + local urltest_proxy_links + config_get urltest_proxy_links "$section" "urltest_proxy_links" + [ -n "$urltest_proxy_links" ] && return 0 + ;; + outbound) + local outbound_json + config_get outbound_json "$section" "outbound_json" + [ -n "$outbound_json" ] && return 0 + ;; + subscription) + local subscription_url + config_get subscription_url "$section" "subscription_url" + [ -n "$subscription_url" ] && return 0 + ;; + esac + ;; + vpn) + local interface + config_get interface "$section" "interface" + [ -n "$interface" ] && return 0 + ;; + esac - config_get proxy_string "$section" "proxy_string" - config_get selector_proxy_links "$section" "selector_proxy_links" - config_get urltest_proxy_links "$section" "urltest_proxy_links" - config_get outbound_json "$section" "outbound_json" - config_get interface "$section" "interface" + return 1 +} - if [ -n "$proxy_string" ] || [ -n "$selector_proxy_links" ] || [ -n "$urltest_proxy_links" ] || - [ -n "$outbound_json" ] || [ -n "$interface" ]; then +_check_outbound_section() { + local section="$1" + + if section_has_configured_outbound "$section"; then section_exists=0 fi } @@ -482,7 +522,12 @@ remove_cron_job() { add_subscription_cron_job() { local section="$1" - local proxy_config_type subscription_update_interval cron_job + local connection_type proxy_config_type subscription_update_interval cron_job + + config_get connection_type "$section" "connection_type" + if [ "$connection_type" != "proxy" ]; then + return + fi config_get proxy_config_type "$section" "proxy_config_type" if [ "$proxy_config_type" != "subscription" ]; then @@ -599,7 +644,13 @@ subscription_update() { _check_subscription_section() { local section="$1" - local proxy_config_type + local connection_type proxy_config_type + + config_get connection_type "$section" "connection_type" + if [ "$connection_type" != "proxy" ]; then + return + fi + config_get proxy_config_type "$section" "proxy_config_type" if [ "$proxy_config_type" = "subscription" ]; then has_subscription=1 @@ -614,7 +665,13 @@ subscription_update() { _update_subscription_for_section() { local section="$1" - local proxy_config_type subscription_url subscription_json_path + local connection_type proxy_config_type subscription_url subscription_json_path + + config_get connection_type "$section" "connection_type" + if [ "$connection_type" != "proxy" ]; then + return + fi + config_get proxy_config_type "$section" "proxy_config_type" if [ "$proxy_config_type" != "subscription" ]; then @@ -1385,7 +1442,19 @@ configure_section_mixed_proxy() { log "Could not determine the listening IP address for the Mixed Proxy. The proxy will not be created." "warn" return 1 fi - config_get mixed_proxy_port "$section" "mixed_proxy_port" + config_get mixed_proxy_port "$section" "mixed_proxy_port" "2080" + + case "$mixed_proxy_port" in + '' | *[!0-9]*) + log "Invalid mixed_proxy_port '$mixed_proxy_port' for section '$section'. Falling back to 2080." "warn" + mixed_proxy_port="2080" + ;; + esac + + if [ "$mixed_proxy_port" -lt 1 ] || [ "$mixed_proxy_port" -gt 65535 ]; then + log "mixed_proxy_port '$mixed_proxy_port' for section '$section' is out of range (1-65535). Falling back to 2080." "warn" + mixed_proxy_port="2080" + fi if [ "$mixed_inbound_enabled" -eq 1 ]; then mixed_inbound_tag="$(get_inbound_tag_by_section "$section-mixed")" mixed_outbound_tag="$(get_outbound_tag_by_section "$section")" @@ -1686,11 +1755,8 @@ get_download_detour_tag() { _determine_first_outbound_section() { local section="$1" - local connection_type - config_get connection_type "$section" "connection_type" - - if [ "$connection_type" = "proxy" ] || [ "$connection_type" = "vpn" ]; then - [ -z "$first_section" ] && first_section="$1" + if section_has_configured_outbound "$section"; then + [ -z "$first_section" ] && first_section="$section" fi } From 676936650d46663b8446e9487a29130555043912 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 7 Mar 2026 16:39:34 +0300 Subject: [PATCH 03/75] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20out?= =?UTF-8?q?bound=20=D0=B2=20podkop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- podkop/files/usr/bin/podkop | 6 +++++- podkop/files/usr/lib/sing_box_config_facade.sh | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 3e07bb17..8e9c1107 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -917,7 +917,11 @@ configure_outbound_handler() { fi # Parse subscription outbounds - config="$(sing_box_cf_add_subscription_outbounds "$config" "$section" "$subscription_json_path")" + if ! sing_box_cf_add_subscription_outbounds "$config" "$section" "$subscription_json_path" > /dev/null; then + log "No proxy outbounds found in subscription for section '$section'. Aborted." "fatal" + exit 1 + fi + config="$SING_BOX_CF_LAST_CONFIG" if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then log "No proxy outbounds found in subscription for section '$section'. Aborted." "fatal" diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index 7315696d..a4e7b390 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -349,6 +349,7 @@ sing_box_cf_add_subscription_outbounds() { SUBSCRIPTION_OUTBOUND_TAGS="" SUBSCRIPTION_OUTBOUND_NAMES="" + SING_BOX_CF_LAST_CONFIG="$config" if [ ! -f "$subscription_json_path" ]; then log "Subscription JSON file not found: $subscription_json_path" "error" @@ -421,6 +422,7 @@ sing_box_cf_add_subscription_outbounds() { done log "Added $((i - 1)) subscription outbounds for section '$section'" "info" + SING_BOX_CF_LAST_CONFIG="$config" echo "$config" } From d7e39316bca98437981ab745b7563bd773ffffdf Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 7 Mar 2026 17:09:27 +0300 Subject: [PATCH 04/75] =?UTF-8?q?Fix=20vpn=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=20luci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../files/usr/lib/sing_box_config_facade.sh | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index a4e7b390..89f6d208 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -377,7 +377,8 @@ sing_box_cf_add_subscription_outbounds() { log "Found $outbounds_count proxy outbounds in subscription" "info" local i=1 - local outbound_json display_name outbound_tag + local added_count=0 + local outbound_json display_name outbound_tag outbound_type outbound_tls_enabled while [ "$i" -le "$outbounds_count" ]; do # Extract the i-th proxy outbound as raw JSON @@ -397,6 +398,16 @@ sing_box_cf_add_subscription_outbounds() { # Get display name: prefer remark, then tag, then fallback display_name=$(echo "$outbound_json" | jq -r '.remark // .tag // "server-'"$i"'"' 2>/dev/null) + outbound_type=$(echo "$outbound_json" | jq -r '.type // ""' 2>/dev/null) + outbound_tls_enabled=$(echo "$outbound_json" | jq -r '.tls.enabled // false' 2>/dev/null) + + # sing-box does not support top-level tls field for shadowsocks outbound. + if [ "$outbound_type" = "shadowsocks" ] && [ "$outbound_tls_enabled" = "true" ]; then + log "Skip unsupported Shadowsocks outbound with tls: '$display_name'" "warn" + i=$((i + 1)) + continue + fi + # Create the tag in podkop format outbound_tag="$(get_outbound_tag_by_section "$section-$i")" @@ -404,7 +415,14 @@ sing_box_cf_add_subscription_outbounds() { local clean_outbound clean_outbound=$(echo "$outbound_json" | jq -c 'del(.tag) | del(.remark)' 2>/dev/null) - config=$(sing_box_cm_add_raw_outbound "$config" "$outbound_tag" "$clean_outbound") + local updated_config + updated_config=$(sing_box_cm_add_raw_outbound "$config" "$outbound_tag" "$clean_outbound" 2>/dev/null) + if [ -z "$updated_config" ]; then + log "Skip invalid outbound from subscription: '$display_name'" "warn" + i=$((i + 1)) + continue + fi + config="$updated_config" if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then SUBSCRIPTION_OUTBOUND_TAGS="$outbound_tag" @@ -418,10 +436,11 @@ sing_box_cf_add_subscription_outbounds() { SUBSCRIPTION_OUTBOUND_NAMES="$(printf '%s\n%s' "$SUBSCRIPTION_OUTBOUND_NAMES" "$display_name")" fi + added_count=$((added_count + 1)) i=$((i + 1)) done - log "Added $((i - 1)) subscription outbounds for section '$section'" "info" + log "Added $added_count subscription outbounds for section '$section'" "info" SING_BOX_CF_LAST_CONFIG="$config" echo "$config" From eb3250776038aadce56779e5aab0654ff0f1fd86 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 7 Mar 2026 17:38:22 +0300 Subject: [PATCH 05/75] Fix podkop VPN subscription UI --- .../files/usr/lib/sing_box_config_facade.sh | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index 89f6d208..da5285fd 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -378,7 +378,7 @@ sing_box_cf_add_subscription_outbounds() { local i=1 local added_count=0 - local outbound_json display_name outbound_tag outbound_type outbound_tls_enabled + local outbound_json display_name outbound_tag outbound_type outbound_tls_enabled preferred_tag base_tag tag_suffix while [ "$i" -le "$outbounds_count" ]; do # Extract the i-th proxy outbound as raw JSON @@ -408,8 +408,19 @@ sing_box_cf_add_subscription_outbounds() { continue fi - # Create the tag in podkop format - outbound_tag="$(get_outbound_tag_by_section "$section-$i")" + # Keep original tag from the subscription for dashboard readability. + preferred_tag=$(echo "$outbound_json" | jq -r '.tag // .remark // "server-'"$i"'"' 2>/dev/null) + if [ -z "$preferred_tag" ] || [ "$preferred_tag" = "null" ]; then + preferred_tag="server-$i" + fi + + base_tag="$preferred_tag" + outbound_tag="$base_tag" + tag_suffix=1 + while printf '%s' "$config" | jq -e --arg tag "$outbound_tag" '.outbounds[]? | select(.tag == $tag)' > /dev/null 2>&1; do + outbound_tag="${base_tag}-$tag_suffix" + tag_suffix=$((tag_suffix + 1)) + done # Remove tag from raw outbound (it will be set by sing_box_cm_add_raw_outbound) local clean_outbound @@ -422,6 +433,19 @@ sing_box_cf_add_subscription_outbounds() { i=$((i + 1)) continue fi + + # Validate against current sing-box version and skip unsupported outbounds. + local validation_tmp + validation_tmp="$(mktemp)" + sing_box_cm_save_config_to_file "$updated_config" "$validation_tmp" + if ! sing-box -c "$validation_tmp" check > /dev/null 2>&1; then + rm -f "$validation_tmp" + log "Skip unsupported outbound for current sing-box: '$display_name'" "warn" + i=$((i + 1)) + continue + fi + rm -f "$validation_tmp" + config="$updated_config" if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then From 8d091b84754820d0cd61f72f8c2b300103e0bdda Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 7 Mar 2026 18:25:43 +0300 Subject: [PATCH 06/75] Fix podkop startup errors --- .../luci-static/resources/view/podkop/main.js | 21 ++++++-- podkop/files/usr/bin/podkop | 54 +++++++++++++++---- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index 6ff47115..dfeb8976 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -579,9 +579,17 @@ async function callBaseMethod(method, args = [], command = "/usr/bin/podkop") { }); if (response.stdout) { try { + const data = JSON.parse(response.stdout); + if (data && typeof data === "object" && data.success === false) { + return { + success: false, + data, + error: data.message || data.error || "" + }; + } return { success: true, - data: JSON.parse(response.stdout) + data }; } catch (_e) { return { @@ -778,7 +786,7 @@ async function getDashboardSections() { })); return { withTagSelect: true, - code: selector?.code || section[".name"], + code: selector?.code || section[".name"] + "-out", displayName: section[".name"], outbounds }; @@ -799,7 +807,7 @@ async function getDashboardSections() { })); return { withTagSelect: true, - code: selector?.code || section[".name"], + code: selector?.code || section[".name"] + "-out", displayName: section[".name"], outbounds: [ { @@ -829,7 +837,7 @@ async function getDashboardSections() { })); return { withTagSelect: true, - code: selector?.code || section[".name"], + code: selector?.code || section[".name"] + "-out", displayName: section[".name"], outbounds: [ { @@ -2115,7 +2123,10 @@ async function connectToClashSockets() { ); } async function handleChooseOutbound(selector, tag) { - await PodkopShellMethods.setClashApiGroupProxy(selector, tag); + const response = await PodkopShellMethods.setClashApiGroupProxy(selector, tag); + if (!response.success || response.data?.success === false) { + showToast(response.data?.message || _("Failed to switch proxy"), "error"); + } await fetchDashboardSections(); } async function handleTestGroupLatency(tag) { diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 8e9c1107..33af0318 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -686,6 +686,8 @@ subscription_update() { mkdir -p "$TMP_SUBSCRIPTION_FOLDER" subscription_json_path="$TMP_SUBSCRIPTION_FOLDER/${section}.json" + local subscription_url_cache_path + subscription_url_cache_path="$TMP_SUBSCRIPTION_FOLDER/${section}.url" echolog "📥 Updating subscription for section '$section'..." @@ -701,6 +703,7 @@ subscription_update() { return fi + printf '%s' "$subscription_url" > "$subscription_url_cache_path" local outbounds_count outbounds_count=$(jq -r '[.outbounds[] | select( .type != "selector" and @@ -902,9 +905,29 @@ configure_outbound_handler() { mkdir -p "$TMP_SUBSCRIPTION_FOLDER" subscription_json_path="$TMP_SUBSCRIPTION_FOLDER/${section}.json" + local subscription_url_cache_path cached_subscription_url should_download + subscription_url_cache_path="$TMP_SUBSCRIPTION_FOLDER/${section}.url" + should_download=0 - # Download subscription if not cached or force update - if [ ! -f "$subscription_json_path" ]; then + if [ ! -f "$subscription_json_path" ] || [ ! -s "$subscription_json_path" ]; then + should_download=1 + fi + + if [ -f "$subscription_url_cache_path" ]; then + cached_subscription_url="$(cat "$subscription_url_cache_path" 2>/dev/null)" + else + cached_subscription_url="" + fi + + if [ "$cached_subscription_url" != "$subscription_url" ]; then + if [ -n "$cached_subscription_url" ]; then + log "Subscription URL changed for section '$section', refreshing cache" "debug" + fi + should_download=1 + rm -f "$subscription_json_path" + fi + + if [ "$should_download" -eq 1 ]; then log "Downloading subscription for section '$section'" local service_proxy_address service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')" @@ -914,6 +937,8 @@ configure_outbound_handler() { log "Failed to download subscription for section '$section'. Aborted." "fatal" exit 1 fi + + printf '%s' "$subscription_url" > "$subscription_url_cache_path" fi # Parse subscription outbounds @@ -2477,6 +2502,7 @@ clash_api() { get_proxy_latency) local proxy_tag="$2" + local encoded_proxy_tag local timeout="${3:-2000}" if [ -z "$proxy_tag" ]; then @@ -2484,7 +2510,9 @@ clash_api() { return 1 fi - curl -G -s "$CLASH_URL/proxies/$proxy_tag/delay" \ + encoded_proxy_tag=$(printf '%s' "$proxy_tag" | jq -sRr @uri) + + curl -G -s "$CLASH_URL/proxies/$encoded_proxy_tag/delay" \ --header "$auth_header" \ --data-urlencode "url=$TEST_URL" \ --data-urlencode "timeout=$timeout" | jq . @@ -2492,6 +2520,7 @@ clash_api() { get_group_latency) local group_tag="$2" + local encoded_group_tag local timeout="${3:-5000}" if [ -z "$group_tag" ]; then @@ -2499,7 +2528,9 @@ clash_api() { return 1 fi - curl -G -s "$CLASH_URL/group/$group_tag/delay" \ + encoded_group_tag=$(printf '%s' "$group_tag" | jq -sRr @uri) + + curl -G -s "$CLASH_URL/group/$encoded_group_tag/delay" \ --header "$auth_header" \ --data-urlencode "url=$TEST_URL" \ --data-urlencode "timeout=$timeout" | jq . @@ -2509,6 +2540,10 @@ clash_api() { local group_tag="$2" local proxy_tag="$3" + local encoded_group_tag payload + encoded_group_tag=$(printf '%s' "$group_tag" | jq -sRr @uri) + payload=$(jq -cn --arg name "$proxy_tag" '{name:$name}') + if [ -z "$group_tag" ] || [ -z "$proxy_tag" ]; then echo '{"error":"group_tag and proxy_tag required"}' | jq . return 1 @@ -2516,9 +2551,10 @@ clash_api() { local response response=$( - curl -X PUT -s -w "\n%{http_code}" "$CLASH_URL/proxies/$group_tag" \ + curl -X PUT -s -w "\n%{http_code}" "$CLASH_URL/proxies/$encoded_group_tag" \ --header "$auth_header" \ - --data-raw "{\"name\":\"$proxy_tag\"}" + --header "Content-Type: application/json" \ + --data-raw "$payload" ) local http_code @@ -2528,15 +2564,15 @@ clash_api() { case "$http_code" in 204) - echo "{\"success\":true,\"group\":\"$group_tag\",\"proxy\":\"$proxy_tag\"}" | jq . + jq -n --arg group "$group_tag" --arg proxy "$proxy_tag" '{success:true,group:$group,proxy:$proxy}' ;; 404) - echo "{\"success\":false,\"error\":\"group_not_found\",\"message\":\"$group_tag does not exist\"}" | jq . + jq -n --arg group "$group_tag" '{success:false,error:"group_not_found",message:($group + " does not exist")}' return 1 ;; 400) if echo "$body" | grep -q "not found"; then - echo "{\"success\":false,\"error\":\"proxy_not_found\",\"message\":\"$proxy_tag not found in group $group_tag\"}" | jq . + jq -n --arg proxy "$proxy_tag" --arg group "$group_tag" '{success:false,error:"proxy_not_found",message:($proxy + " not found in group " + $group)}' else echo '{"success":false,"error":"bad_request","message":"Invalid request"}' | jq . fi From 357fa269873b26b08874cc9f8490eb438f816ba0 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 7 Mar 2026 19:06:46 +0300 Subject: [PATCH 07/75] Review podkop startup logs --- podkop/files/usr/bin/podkop | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 33af0318..79b2fd37 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -851,7 +851,7 @@ configure_outbound_handler() { selector_tag="$(get_outbound_tag_by_section "$section")" selector_outbounds="$(comma_string_to_json_array "$outbound_tags")" config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" \ - "$default_outbound")" + "$default_outbound" "true")" ;; urltest) log "Detected proxy configuration type: urltest" "debug" @@ -886,7 +886,7 @@ configure_outbound_handler() { selector_outbounds="$(comma_string_to_json_array "$outbound_tags,$urltest_tag")" config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" - config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" ;; subscription) log "Detected proxy configuration type: subscription" "debug" @@ -960,7 +960,7 @@ configure_outbound_handler() { selector_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS,$urltest_tag")" config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" - config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" ;; *) log "Unknown proxy configuration type: '$proxy_config_type'. Aborted." "fatal" From 617080e0bcf476e48cb8ba7c11567bccbd1837d9 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 7 Mar 2026 19:28:33 +0300 Subject: [PATCH 08/75] =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BD=D1=83=D0=BB=20?= =?UTF-8?q?=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B8,=20=D0=BA=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=B1=D1=8B=D0=BB=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 55 ++++++++---------------------------------------------- install.sh | 10 +++++----- 2 files changed, 13 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 2b8ac9ca..a938bc1f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,3 @@ -# Podkop Evolution - -> **Podkop's fork with HWID and Subscription URL support** -> -> Этот форк добавляет поддержку ссылок подписки (subscription URL) с кастомными заголовками (HWID, Device-OS, Device-Model) и автоматическим обновлением. Основан на [itdoginfo/podkop](https://github.com/itdoginfo/podkop). - ---- - # Вещи, которые вам нужно знать перед установкой - Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться. @@ -24,45 +16,12 @@ # Документация https://podkop.net/ -# Установка Podkop Evolution +# Установка Podkop Полная информация в [документации](https://podkop.net/docs/install/) Вкратце, достаточно одного скрипта для установки и обновления: ``` -sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/install.sh) -``` - -## Новое в этом форке: Подписки (Subscription) - -Добавлена поддержка subscription URL — ссылки подписки от провайдера прокси. При выборе типа конфигурации **Subscription** в LuCI: - -- Введите URL подписки от вашего провайдера -- Выберите интервал автообновления (от 30 минут до 1 дня) -- Все серверы из подписки автоматически появятся в дашборде -- Автоматический выбор лучшего сервера по задержке (URLTest) -- Ручное переключение между серверами через дашборд - -При скачивании подписки отправляются заголовки: -- `User-Agent: singbox/<версия>` -- `X-HWID` — уникальный идентификатор роутера -- `X-Device-OS: OpenWrt Linux` -- `X-Device-Model` — модель роутера -- `X-Ver-OS` — версия ядра - -Пример конфигурации через UCI: -``` -uci set podkop.my_sub=section -uci set podkop.my_sub.connection_type='proxy' -uci set podkop.my_sub.proxy_config_type='subscription' -uci set podkop.my_sub.subscription_url='https://your-provider.com/api/sub' -uci set podkop.my_sub.subscription_update_interval='1h' -uci add_list podkop.my_sub.community_lists='russia_inside' -uci commit podkop -``` - -Ручное обновление подписки: -``` -/usr/bin/podkop subscription_update +sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh) ``` ## Изменения 0.7.0 @@ -79,7 +38,7 @@ mv /etc/config/podkop /etc/config/podkop-070 ``` 2. Стянуть новый дефолтный конфиг: ``` -wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/podkop/files/etc/config/podkop +wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop ``` 3. Настроить заново ваш Podkop через Luci или UCI. @@ -89,12 +48,14 @@ wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-e > PR принимаются только по issues, у которых стоит label "enhancement". Либо по согласованию с авторами в ТГ-чате. Остальные PR на данный момент не рассматриваются. ## Будущее -- [x] [Подписка](https://github.com/itdoginfo/podkop/issues/118) — **реализовано в этом форке!** +- [ ] [Подписка](https://github.com/itdoginfo/podkop/issues/118). Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров. - [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне. -- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. [Issue](https://github.com/itdoginfo/podkop/issues/111) +- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусственно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111) - [ ] Галочка, которая режет доступ к doh серверам. - [ ] IPv6. Только после наполнения Wiki. ## Тесты - [ ] Unit тесты (BATS) -- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS) \ No newline at end of file +- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS) + +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop) \ No newline at end of file diff --git a/install.sh b/install.sh index 6fde5176..6376d7c2 100755 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ #!/bin/sh # shellcheck shell=dash -REPO="https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest" +REPO="https://api.github.com/repos/itdoginfo/podkop/releases/latest" DOWNLOAD_DIR="/tmp/podkop" COUNT=3 @@ -66,7 +66,7 @@ update_config() { printf "\033[48;5;196m\033[1m║ ! Обнаружена старая версия podkop. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Если продолжите обновление, вам потребуется настроить Podkop заново. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Старая конфигурация будет сохранена в /etc/config/podkop-070 ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/itdoginfo/podkop ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -76,7 +76,7 @@ update_config() { printf "\033[48;5;196m\033[1m║ ! Detected old podkop version. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ If you continue the update, you will need to RECONFIGURE podkop. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Your old configuration will be saved to /etc/config/podkop-070 ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Details: https://github.com/itdoginfo/podkop ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -88,7 +88,7 @@ update_config() { yes|y|Y) mv /etc/config/podkop /etc/config/podkop-070 - wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/podkop/files/etc/config/podkop + wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop msg "Podkop config has been reset to default. Your old config saved in /etc/config/podkop-070" break ;; @@ -115,7 +115,7 @@ main() { fi if command -v curl >/dev/null 2>&1; then - check_response=$(curl -s "https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest") + check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest") if echo "$check_response" | grep -q 'API rate limit '; then msg "You've reached the GitHub rate limit. Repeat in five minutes." From 5c7e3dce316de27842cbc8ed374a89b9e094cd99 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sun, 8 Mar 2026 13:04:47 +0300 Subject: [PATCH 09/75] =?UTF-8?q?=D0=BF=D0=BE=D1=87=D0=B8=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=BC=D1=83=D0=BB=D1=8C=D1=82=D0=B8=D1=81=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D1=85=20+=20=D0=B2=D0=B5=D1=80=D0=BD=D1=83?= =?UTF-8?q?=D0=BB=20readme=20&=20install=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 55 +++++++++++++++++++++++++++++++------ install.sh | 10 +++---- podkop/files/usr/bin/podkop | 45 ++++++++++++++++++++++++------ 3 files changed, 89 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a938bc1f..2b8ac9ca 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ +# Podkop Evolution + +> **Podkop's fork with HWID and Subscription URL support** +> +> Этот форк добавляет поддержку ссылок подписки (subscription URL) с кастомными заголовками (HWID, Device-OS, Device-Model) и автоматическим обновлением. Основан на [itdoginfo/podkop](https://github.com/itdoginfo/podkop). + +--- + # Вещи, которые вам нужно знать перед установкой - Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться. @@ -16,12 +24,45 @@ # Документация https://podkop.net/ -# Установка Podkop +# Установка Podkop Evolution Полная информация в [документации](https://podkop.net/docs/install/) Вкратце, достаточно одного скрипта для установки и обновления: ``` -sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh) +sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/install.sh) +``` + +## Новое в этом форке: Подписки (Subscription) + +Добавлена поддержка subscription URL — ссылки подписки от провайдера прокси. При выборе типа конфигурации **Subscription** в LuCI: + +- Введите URL подписки от вашего провайдера +- Выберите интервал автообновления (от 30 минут до 1 дня) +- Все серверы из подписки автоматически появятся в дашборде +- Автоматический выбор лучшего сервера по задержке (URLTest) +- Ручное переключение между серверами через дашборд + +При скачивании подписки отправляются заголовки: +- `User-Agent: singbox/<версия>` +- `X-HWID` — уникальный идентификатор роутера +- `X-Device-OS: OpenWrt Linux` +- `X-Device-Model` — модель роутера +- `X-Ver-OS` — версия ядра + +Пример конфигурации через UCI: +``` +uci set podkop.my_sub=section +uci set podkop.my_sub.connection_type='proxy' +uci set podkop.my_sub.proxy_config_type='subscription' +uci set podkop.my_sub.subscription_url='https://your-provider.com/api/sub' +uci set podkop.my_sub.subscription_update_interval='1h' +uci add_list podkop.my_sub.community_lists='russia_inside' +uci commit podkop +``` + +Ручное обновление подписки: +``` +/usr/bin/podkop subscription_update ``` ## Изменения 0.7.0 @@ -38,7 +79,7 @@ mv /etc/config/podkop /etc/config/podkop-070 ``` 2. Стянуть новый дефолтный конфиг: ``` -wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop +wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/podkop/files/etc/config/podkop ``` 3. Настроить заново ваш Podkop через Luci или UCI. @@ -48,14 +89,12 @@ wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/re > PR принимаются только по issues, у которых стоит label "enhancement". Либо по согласованию с авторами в ТГ-чате. Остальные PR на данный момент не рассматриваются. ## Будущее -- [ ] [Подписка](https://github.com/itdoginfo/podkop/issues/118). Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров. +- [x] [Подписка](https://github.com/itdoginfo/podkop/issues/118) — **реализовано в этом форке!** - [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне. -- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусственно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111) +- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. [Issue](https://github.com/itdoginfo/podkop/issues/111) - [ ] Галочка, которая режет доступ к doh серверам. - [ ] IPv6. Только после наполнения Wiki. ## Тесты - [ ] Unit тесты (BATS) -- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS) - -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop) \ No newline at end of file +- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS) \ No newline at end of file diff --git a/install.sh b/install.sh index 6376d7c2..6fde5176 100755 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ #!/bin/sh # shellcheck shell=dash -REPO="https://api.github.com/repos/itdoginfo/podkop/releases/latest" +REPO="https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest" DOWNLOAD_DIR="/tmp/podkop" COUNT=3 @@ -66,7 +66,7 @@ update_config() { printf "\033[48;5;196m\033[1m║ ! Обнаружена старая версия podkop. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Если продолжите обновление, вам потребуется настроить Podkop заново. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Старая конфигурация будет сохранена в /etc/config/podkop-070 ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/itdoginfo/podkop ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -76,7 +76,7 @@ update_config() { printf "\033[48;5;196m\033[1m║ ! Detected old podkop version. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ If you continue the update, you will need to RECONFIGURE podkop. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Your old configuration will be saved to /etc/config/podkop-070 ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Details: https://github.com/itdoginfo/podkop ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -88,7 +88,7 @@ update_config() { yes|y|Y) mv /etc/config/podkop /etc/config/podkop-070 - wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop + wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/podkop/files/etc/config/podkop msg "Podkop config has been reset to default. Your old config saved in /etc/config/podkop-070" break ;; @@ -115,7 +115,7 @@ main() { fi if command -v curl >/dev/null 2>&1; then - check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest") + check_response=$(curl -s "https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest") if echo "$check_response" | grep -q 'API rate limit '; then msg "You've reached the GitHub rate limit. Repeat in five minutes." diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 79b2fd37..3d1a2141 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -568,6 +568,23 @@ add_subscription_cron_job() { } | crontab - log "The subscription cron job has been created: $cron_job" } +ensure_nft_ready_for_list_update() { + if nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then + return 0 + fi + + log "NFT table '$NFT_TABLE_NAME' is missing before lists update, recreating nft rules" "warn" + route_table_rule_mark + create_nft_rules + + if ! nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then + log "Failed to recreate NFT table '$NFT_TABLE_NAME'" "error" + return 1 + fi + + return 0 +} + list_update() { echolog "🔄 Starting lists update..." @@ -600,7 +617,7 @@ list_update() { local service_proxy_address service_proxy_address="$(get_service_proxy_address)" - if [ -n "$http_proxy_address" ]; then + if [ -n "$service_proxy_address" ]; then if curl -s -x "http://$service_proxy_address" -m $curl_timeout https://github.com > /dev/null; then echolog "✅ GitHub connection check passed (via proxy)" break @@ -624,16 +641,23 @@ list_update() { return 1 fi + if ! ensure_nft_ready_for_list_update; then + echolog "❌ NFT table is unavailable, cannot update lists" + return 1 + fi + echolog "📥 Downloading and processing lists..." - config_foreach import_community_subnet_lists "section" - config_foreach import_domains_from_remote_domain_lists "section" - config_foreach import_subnets_from_remote_subnet_lists "section" + local update_failed=0 + config_foreach import_community_subnet_lists "section" || update_failed=1 + config_foreach import_domains_from_remote_domain_lists "section" || update_failed=1 + config_foreach import_subnets_from_remote_subnet_lists "section" || update_failed=1 - if [ $? -eq 0 ]; then + if [ "$update_failed" -eq 0 ]; then echolog "✅ Lists update completed successfully" else echolog "❌ Lists update failed" + return 1 fi } @@ -1570,9 +1594,14 @@ import_community_service_subnet_list_handler() { ;; "discord") URL=$SUBNETS_DISCORD - nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" - nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr \ - "@$NFT_DISCORD_SET_NAME" udp dport '{ 50000-65535 }' meta mark set "$NFT_FAKEIP_MARK" counter + if ! nft list set inet "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" > /dev/null 2>&1; then + nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" + fi + if ! nft list chain inet "$NFT_TABLE_NAME" mangle 2> /dev/null | \ + grep -Fq "@$NFT_DISCORD_SET_NAME udp dport { 50000-65535 }"; then + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr \ + "@$NFT_DISCORD_SET_NAME" udp dport '{ 50000-65535 }' meta mark set "$NFT_FAKEIP_MARK" counter + fi ;; "roblox") URL=$SUBNETS_ROBLOX From d02ee70f30fa4e5bd0b1833543838a750204ccea Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Fri, 13 Mar 2026 19:29:37 +0300 Subject: [PATCH 10/75] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=20=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../methods/custom/getDashboardSections.ts | 78 ++++++++---- fe-app-podkop/src/podkop/types.ts | 1 + .../luci-static/resources/view/podkop/main.js | 66 ++++++++--- .../resources/view/podkop/section.js | 10 ++ podkop/files/etc/config/podkop | 3 +- podkop/files/usr/bin/podkop | 112 ++++++++++++++++-- 6 files changed, 223 insertions(+), 47 deletions(-) diff --git a/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts b/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts index 554caa6d..771ae8a4 100644 --- a/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts +++ b/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts @@ -159,34 +159,72 @@ export async function getDashboardSections(): Promise proxy.code === `${section['.name']}-out`, ); - const outbound = proxies.find( + const fallbackUrltest = proxies.find( (proxy) => proxy.code === `${section['.name']}-urltest-out`, ); + const selectorOutbounds = (selector?.value?.all ?? []).flatMap((code) => { + const item = proxies.find((proxy) => proxy.code === code); + if (!item) { + return []; + } - const outbounds = (outbound?.value?.all ?? []) - .map((code) => proxies.find((item) => item.code === code)) - .map((item) => ({ - code: item?.code || '', - displayName: item?.value?.name || '', - latency: item?.value?.history?.[0]?.delay || 0, - type: item?.value?.type || '', - selected: selector?.value?.now === item?.code, - })); + const isLegacyFastest = item.code === `${section['.name']}-urltest-out`; + + return [ + { + code: item.code, + displayName: isLegacyFastest + ? _('Fastest') + : item?.value?.name || '', + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || '', + selected: selector?.value?.now === item.code, + }, + ]; + }); + + const outbounds = [ + ...selectorOutbounds.filter( + (item) => item.type?.toLowerCase() === 'urltest', + ), + ...selectorOutbounds.filter( + (item) => item.type?.toLowerCase() !== 'urltest', + ), + ]; + + if (outbounds.length === 0 && fallbackUrltest) { + const fallbackOutbounds = (fallbackUrltest?.value?.all ?? []) + .map((code) => proxies.find((item) => item.code === code)) + .map((item) => ({ + code: item?.code || '', + displayName: item?.value?.name || '', + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || '', + selected: selector?.value?.now === item?.code, + })); + + return { + withTagSelect: true, + code: selector?.code || section['.name'], + displayName: section['.name'], + outbounds: [ + { + code: fallbackUrltest?.code || '', + displayName: _('Fastest'), + latency: fallbackUrltest?.value?.history?.[0]?.delay || 0, + type: fallbackUrltest?.value?.type || '', + selected: selector?.value?.now === fallbackUrltest?.code, + }, + ...fallbackOutbounds, + ], + }; + } return { withTagSelect: true, code: selector?.code || section['.name'], displayName: section['.name'], - outbounds: [ - { - code: outbound?.code || '', - displayName: _('Fastest'), - latency: outbound?.value?.history?.[0]?.delay || 0, - type: outbound?.value?.type || '', - selected: selector?.value?.now === outbound?.code, - }, - ...outbounds, - ], + outbounds, }; } } diff --git a/fe-app-podkop/src/podkop/types.ts b/fe-app-podkop/src/podkop/types.ts index e93ab032..98673bd8 100644 --- a/fe-app-podkop/src/podkop/types.ts +++ b/fe-app-podkop/src/podkop/types.ts @@ -119,6 +119,7 @@ export namespace Podkop { proxy_config_type: 'subscription'; subscription_url: string; subscription_update_interval?: string; + subscription_group_by_countries?: '0' | '1'; } export interface ConfigVpnSection { diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index dfeb8976..21ca9153 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -825,30 +825,60 @@ async function getDashboardSections() { const selector = proxies.find( (proxy) => proxy.code === `${section[".name"]}-out` ); - const outbound = proxies.find( + const fallbackUrltest = proxies.find( (proxy) => proxy.code === `${section[".name"]}-urltest-out` ); - const outbounds = (outbound?.value?.all ?? []).map((code) => proxies.find((item) => item.code === code)).map((item) => ({ - code: item?.code || "", - displayName: item?.value?.name || "", - latency: item?.value?.history?.[0]?.delay || 0, - type: item?.value?.type || "", - selected: selector?.value?.now === item?.code - })); + const selectorOutbounds = (selector?.value?.all ?? []).flatMap((code) => { + const item = proxies.find((proxy) => proxy.code === code); + if (!item) { + return []; + } + const isLegacyFastest = item.code === `${section[".name"]}-urltest-out`; + return [{ + code: item.code, + displayName: isLegacyFastest ? _("Fastest") : item?.value?.name || "", + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || "", + selected: selector?.value?.now === item.code + }]; + }); + const outbounds = [ + ...selectorOutbounds.filter( + (item) => item.type?.toLowerCase() === "urltest" + ), + ...selectorOutbounds.filter( + (item) => item.type?.toLowerCase() !== "urltest" + ) + ]; + if (outbounds.length === 0 && fallbackUrltest) { + const fallbackOutbounds = (fallbackUrltest?.value?.all ?? []).map((code) => proxies.find((item) => item.code === code)).map((item) => ({ + code: item?.code || "", + displayName: item?.value?.name || "", + latency: item?.value?.history?.[0]?.delay || 0, + type: item?.value?.type || "", + selected: selector?.value?.now === item?.code + })); + return { + withTagSelect: true, + code: selector?.code || section[".name"] + "-out", + displayName: section[".name"], + outbounds: [ + { + code: fallbackUrltest?.code || "", + displayName: _("Fastest"), + latency: fallbackUrltest?.value?.history?.[0]?.delay || 0, + type: fallbackUrltest?.value?.type || "", + selected: selector?.value?.now === fallbackUrltest?.code + }, + ...fallbackOutbounds + ] + }; + } return { withTagSelect: true, code: selector?.code || section[".name"] + "-out", displayName: section[".name"], - outbounds: [ - { - code: outbound?.code || "", - displayName: _("Fastest"), - latency: outbound?.value?.history?.[0]?.delay || 0, - type: outbound?.value?.type || "", - selected: selector?.value?.now === outbound?.code - }, - ...outbounds - ] + outbounds }; } } diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js index d5ddc196..b09bed6f 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js @@ -121,6 +121,16 @@ function createSectionContent(section) { o.default = "1h"; o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o = section.option( + form.Flag, + "subscription_group_by_countries", + _("Группировать по странам"), + _("Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы"), + ); + o.default = "0"; + o.rmempty = false; + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o = section.option( form.DynamicList, "selector_proxy_links", diff --git a/podkop/files/etc/config/podkop b/podkop/files/etc/config/podkop index ff8d9766..53e2ed95 100644 --- a/podkop/files/etc/config/podkop +++ b/podkop/files/etc/config/podkop @@ -44,7 +44,8 @@ config section 'main' # option proxy_config_type 'subscription' # option subscription_url 'https://example.com/api/sub' # option subscription_update_interval '1h' +# #option subscription_group_by_countries '0' # #option urltest_check_interval '3m' # #option urltest_tolerance '50' # #option urltest_testing_url 'https://www.gstatic.com/generate_204' -# list community_lists 'russia_inside' \ No newline at end of file +# list community_lists 'russia_inside' diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 3d1a2141..b88fa6ba 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -818,6 +818,52 @@ sing_box_configure_outbounds() { config_foreach configure_outbound_handler "section" } +sing_box_get_unique_outbound_tag() { + local config="$1" + local base_tag="$2" + local candidate="$base_tag" + local tag_suffix=1 + + while printf '%s' "$config" | jq -e --arg tag "$candidate" '.outbounds[]? | select(.tag == $tag)' > /dev/null 2>&1; do + candidate="${base_tag}-${tag_suffix}" + tag_suffix=$((tag_suffix + 1)) + done + + echo "$candidate" +} + +sing_box_build_subscription_country_groups() { + local subscription_outbound_tags="$1" + + printf '%s' "$subscription_outbound_tags" | jq -Rrc ' + def is_regional_indicator: . >= 127462 and . <= 127487; + def extract_country_flag: + (. | explode) as $codepoints + | if ($codepoints | length) >= 2 + and ($codepoints[0] | is_regional_indicator) + and ($codepoints[1] | is_regional_indicator) + then ($codepoints[0:2] | implode) + else "" + end; + + (split(",") | map(select(length > 0))) as $tags + | reduce $tags[] as $tag ( + {country_order: [], country_groups: {}, ungrouped: []}; + ($tag | extract_country_flag) as $country_flag + | if $country_flag == "" then + .ungrouped += [$tag] + else + .country_groups[$country_flag] = ((.country_groups[$country_flag] // []) + [$tag]) + | if (.country_order | index($country_flag)) == null then + .country_order += [$country_flag] + else + . + end + end + ) + ' 2>/dev/null +} + configure_outbound_handler() { local section="$1" @@ -915,12 +961,14 @@ configure_outbound_handler() { subscription) log "Detected proxy configuration type: subscription" "debug" local subscription_url subscription_json_path urltest_tag selector_tag \ - urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance urltest_testing_url + urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance \ + urltest_testing_url subscription_group_by_countries config_get subscription_url "$section" "subscription_url" config_get urltest_check_interval "$section" "urltest_check_interval" "3m" config_get urltest_tolerance "$section" "urltest_tolerance" 50 config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204" + config_get_bool subscription_group_by_countries "$section" "subscription_group_by_countries" 0 if [ -z "$subscription_url" ]; then log "Subscription URL is not set. Aborted." "fatal" @@ -977,14 +1025,62 @@ configure_outbound_handler() { exit 1 fi - # Create urltest + selector (like urltest proxy_config_type) - urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" selector_tag="$(get_outbound_tag_by_section "$section")" - urltest_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" - selector_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS,$urltest_tag")" - config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ - "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" - config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" + + if [ "$subscription_group_by_countries" -eq 1 ]; then + local grouping_json country_flag country_group_outbounds country_group_tag \ + selector_outbound_tags selector_default ungrouped_outbound_tags + + grouping_json="$(sing_box_build_subscription_country_groups "$SUBSCRIPTION_OUTBOUND_TAGS")" + if [ -z "$grouping_json" ]; then + log "Failed to build grouped subscription outbounds for section '$section'. Aborted." "fatal" + exit 1 + fi + + for country_flag in $(echo "$grouping_json" | jq -r '.country_order[]' 2>/dev/null); do + country_group_outbounds="$(echo "$grouping_json" | jq -c --arg country_flag "$country_flag" '.country_groups[$country_flag] // []' 2>/dev/null)" + if [ -z "$country_group_outbounds" ] || [ "$country_group_outbounds" = "[]" ]; then + continue + fi + + country_group_tag="$(sing_box_get_unique_outbound_tag "$config" "$country_flag Fastest")" + config="$(sing_box_cm_add_urltest_outbound "$config" "$country_group_tag" "$country_group_outbounds" \ + "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" + + if [ -z "$selector_outbound_tags" ]; then + selector_outbound_tags="$country_group_tag" + selector_default="$country_group_tag" + else + selector_outbound_tags="$selector_outbound_tags,$country_group_tag" + fi + done + + ungrouped_outbound_tags="$(echo "$grouping_json" | jq -r '.ungrouped | join(",")' 2>/dev/null)" + if [ -n "$ungrouped_outbound_tags" ]; then + if [ -z "$selector_outbound_tags" ]; then + selector_outbound_tags="$ungrouped_outbound_tags" + selector_default="${ungrouped_outbound_tags%%,*}" + else + selector_outbound_tags="$selector_outbound_tags,$ungrouped_outbound_tags" + fi + fi + + if [ -z "$selector_outbound_tags" ]; then + log "No selector outbounds available after grouping subscription outbounds for section '$section'. Aborted." "fatal" + exit 1 + fi + + selector_outbounds="$(comma_string_to_json_array "$selector_outbound_tags")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$selector_default" "true")" + else + # Create urltest + selector (default subscription behaviour) + urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" + urltest_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" + selector_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS,$urltest_tag")" + config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ + "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" + fi ;; *) log "Unknown proxy configuration type: '$proxy_config_type'. Aborted." "fatal" From 8f2b11f672c3d8387938bfab930951c17f8efdcc Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 14 Mar 2026 14:56:04 +0300 Subject: [PATCH 11/75] =?UTF-8?q?=D0=BF=D0=BE=D1=87=D0=B8=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- podkop/files/usr/bin/podkop | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index b88fa6ba..d3d2f30b 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -864,6 +864,19 @@ sing_box_build_subscription_country_groups() { ' 2>/dev/null } +is_truthy_option() { + local value="$1" + + case "$value" in + 1|true|TRUE|True|on|ON|yes|YES|enabled|ENABLED) + return 0 + ;; + *) + return 1 + ;; + esac +} + configure_outbound_handler() { local section="$1" @@ -962,13 +975,25 @@ configure_outbound_handler() { log "Detected proxy configuration type: subscription" "debug" local subscription_url subscription_json_path urltest_tag selector_tag \ urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance \ - urltest_testing_url subscription_group_by_countries + urltest_testing_url subscription_group_by_countries subscription_group_by_countries_raw config_get subscription_url "$section" "subscription_url" config_get urltest_check_interval "$section" "urltest_check_interval" "3m" config_get urltest_tolerance "$section" "urltest_tolerance" 50 config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204" - config_get_bool subscription_group_by_countries "$section" "subscription_group_by_countries" 0 + config_get subscription_group_by_countries_raw "$section" "subscription_group_by_countries" "" + if [ -z "$subscription_group_by_countries_raw" ]; then + # Backward-compatible alias in case custom builds used another key + config_get subscription_group_by_countries_raw "$section" "group_by_countries" "" + fi + + if is_truthy_option "$subscription_group_by_countries_raw"; then + subscription_group_by_countries=1 + else + subscription_group_by_countries=0 + fi + + log "Subscription country grouping for section '$section': raw='${subscription_group_by_countries_raw:-}', enabled=$subscription_group_by_countries" "debug" if [ -z "$subscription_url" ]; then log "Subscription URL is not set. Aborted." "fatal" @@ -1029,7 +1054,7 @@ configure_outbound_handler() { if [ "$subscription_group_by_countries" -eq 1 ]; then local grouping_json country_flag country_group_outbounds country_group_tag \ - selector_outbound_tags selector_default ungrouped_outbound_tags + selector_outbound_tags selector_default ungrouped_outbound_tags grouped_count ungrouped_count grouping_json="$(sing_box_build_subscription_country_groups "$SUBSCRIPTION_OUTBOUND_TAGS")" if [ -z "$grouping_json" ]; then @@ -1037,6 +1062,10 @@ configure_outbound_handler() { exit 1 fi + grouped_count="$(echo "$grouping_json" | jq -r '.country_order | length' 2>/dev/null)" + ungrouped_count="$(echo "$grouping_json" | jq -r '.ungrouped | length' 2>/dev/null)" + log "Country grouping prepared for section '$section': groups=$grouped_count, ungrouped=$ungrouped_count" "debug" + for country_flag in $(echo "$grouping_json" | jq -r '.country_order[]' 2>/dev/null); do country_group_outbounds="$(echo "$grouping_json" | jq -c --arg country_flag "$country_flag" '.country_groups[$country_flag] // []' 2>/dev/null)" if [ -z "$country_group_outbounds" ] || [ "$country_group_outbounds" = "[]" ]; then From 078aecc9a113e806d3f2199fd1a5f9d194847496 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 14 Mar 2026 15:52:28 +0300 Subject: [PATCH 12/75] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B0=D1=83=D1=82=D0=B1=D0=B0=D1=83?= =?UTF-8?q?=D0=BD=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- podkop/files/usr/bin/podkop | 69 ++++++++++++------- .../files/usr/lib/sing_box_config_facade.sh | 10 +++ 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index d3d2f30b..b23481ec 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -833,9 +833,9 @@ sing_box_get_unique_outbound_tag() { } sing_box_build_subscription_country_groups() { - local subscription_outbound_tags="$1" + local subscription_outbound_tags_json="$1" - printf '%s' "$subscription_outbound_tags" | jq -Rrc ' + printf '%s' "$subscription_outbound_tags_json" | jq -c ' def is_regional_indicator: . >= 127462 and . <= 127487; def extract_country_flag: (. | explode) as $codepoints @@ -846,7 +846,7 @@ sing_box_build_subscription_country_groups() { else "" end; - (split(",") | map(select(length > 0))) as $tags + (if type == "array" then . else [] end) as $tags | reduce $tags[] as $tag ( {country_order: [], country_groups: {}, ungrouped: []}; ($tag | extract_country_flag) as $country_flag @@ -975,7 +975,8 @@ configure_outbound_handler() { log "Detected proxy configuration type: subscription" "debug" local subscription_url subscription_json_path urltest_tag selector_tag \ urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance \ - urltest_testing_url subscription_group_by_countries subscription_group_by_countries_raw + urltest_testing_url subscription_group_by_countries subscription_group_by_countries_raw \ + subscription_outbound_tags_json config_get subscription_url "$section" "subscription_url" config_get urltest_check_interval "$section" "urltest_check_interval" "3m" @@ -1050,13 +1051,19 @@ configure_outbound_handler() { exit 1 fi + subscription_outbound_tags_json="$SUBSCRIPTION_OUTBOUND_TAGS_JSON" + if [ -z "$subscription_outbound_tags_json" ] || [ "$subscription_outbound_tags_json" = "[]" ]; then + # Fallback for backward compatibility with older facade versions. + subscription_outbound_tags_json="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" + fi + selector_tag="$(get_outbound_tag_by_section "$section")" if [ "$subscription_group_by_countries" -eq 1 ]; then local grouping_json country_flag country_group_outbounds country_group_tag \ - selector_outbound_tags selector_default ungrouped_outbound_tags grouped_count ungrouped_count + selector_outbounds_json selector_default ungrouped_outbounds_json grouped_count ungrouped_count - grouping_json="$(sing_box_build_subscription_country_groups "$SUBSCRIPTION_OUTBOUND_TAGS")" + grouping_json="$(sing_box_build_subscription_country_groups "$subscription_outbound_tags_json")" if [ -z "$grouping_json" ]; then log "Failed to build grouped subscription outbounds for section '$section'. Aborted." "fatal" exit 1 @@ -1066,6 +1073,8 @@ configure_outbound_handler() { ungrouped_count="$(echo "$grouping_json" | jq -r '.ungrouped | length' 2>/dev/null)" log "Country grouping prepared for section '$section': groups=$grouped_count, ungrouped=$ungrouped_count" "debug" + selector_outbounds_json="[]" + for country_flag in $(echo "$grouping_json" | jq -r '.country_order[]' 2>/dev/null); do country_group_outbounds="$(echo "$grouping_json" | jq -c --arg country_flag "$country_flag" '.country_groups[$country_flag] // []' 2>/dev/null)" if [ -z "$country_group_outbounds" ] || [ "$country_group_outbounds" = "[]" ]; then @@ -1076,36 +1085,48 @@ configure_outbound_handler() { config="$(sing_box_cm_add_urltest_outbound "$config" "$country_group_tag" "$country_group_outbounds" \ "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" - if [ -z "$selector_outbound_tags" ]; then - selector_outbound_tags="$country_group_tag" - selector_default="$country_group_tag" - else - selector_outbound_tags="$selector_outbound_tags,$country_group_tag" - fi + selector_outbounds_json=$( + printf '%s' "$selector_outbounds_json" | jq -ac --arg tag "$country_group_tag" '. + [$tag]' 2>/dev/null + ) done - ungrouped_outbound_tags="$(echo "$grouping_json" | jq -r '.ungrouped | join(",")' 2>/dev/null)" - if [ -n "$ungrouped_outbound_tags" ]; then - if [ -z "$selector_outbound_tags" ]; then - selector_outbound_tags="$ungrouped_outbound_tags" - selector_default="${ungrouped_outbound_tags%%,*}" - else - selector_outbound_tags="$selector_outbound_tags,$ungrouped_outbound_tags" - fi + if [ -z "$selector_outbounds_json" ]; then + selector_outbounds_json="[]" + fi + + ungrouped_outbounds_json="$(echo "$grouping_json" | jq -c '.ungrouped // []' 2>/dev/null)" + if [ -n "$ungrouped_outbounds_json" ] && [ "$ungrouped_outbounds_json" != "[]" ]; then + selector_outbounds_json=$( + jq -acn --argjson selector "$selector_outbounds_json" --argjson ungrouped "$ungrouped_outbounds_json" \ + '$selector + $ungrouped' 2>/dev/null + ) fi - if [ -z "$selector_outbound_tags" ]; then + if [ -z "$selector_outbounds_json" ] || [ "$selector_outbounds_json" = "[]" ]; then log "No selector outbounds available after grouping subscription outbounds for section '$section'. Aborted." "fatal" exit 1 fi - selector_outbounds="$(comma_string_to_json_array "$selector_outbound_tags")" + selector_default="$(echo "$selector_outbounds_json" | jq -r '.[0] // ""' 2>/dev/null)" + if [ -z "$selector_default" ] || [ "$selector_default" = "null" ]; then + log "Unable to determine default selector outbound for section '$section'. Aborted." "fatal" + exit 1 + fi + + selector_outbounds="$selector_outbounds_json" config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$selector_default" "true")" else # Create urltest + selector (default subscription behaviour) urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" - urltest_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" - selector_outbounds="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS,$urltest_tag")" + urltest_outbounds="$subscription_outbound_tags_json" + selector_outbounds=$( + jq -acn --argjson outbounds "$subscription_outbound_tags_json" --arg tag "$urltest_tag" \ + '$outbounds + [$tag]' 2>/dev/null + ) + if [ -z "$selector_outbounds" ]; then + log "Failed to build selector outbounds for subscription section '$section'. Aborted." "fatal" + exit 1 + fi config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index da5285fd..c27f97fc 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -340,6 +340,7 @@ sing_box_cf_add_single_key_reject_rule() { # Outputs: # Writes updated JSON configuration to stdout # Sets global variable SUBSCRIPTION_OUTBOUND_TAGS (comma-separated list of tags) +# Sets global variable SUBSCRIPTION_OUTBOUND_TAGS_JSON (JSON array of tags, ASCII-escaped) # Sets global variable SUBSCRIPTION_OUTBOUND_NAMES (newline-separated list of display names) ####################################### sing_box_cf_add_subscription_outbounds() { @@ -348,6 +349,7 @@ sing_box_cf_add_subscription_outbounds() { local subscription_json_path="$3" SUBSCRIPTION_OUTBOUND_TAGS="" + SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]" SUBSCRIPTION_OUTBOUND_NAMES="" SING_BOX_CF_LAST_CONFIG="$config" @@ -454,6 +456,14 @@ sing_box_cf_add_subscription_outbounds() { SUBSCRIPTION_OUTBOUND_TAGS="$SUBSCRIPTION_OUTBOUND_TAGS,$outbound_tag" fi + # Keep a JSON representation to avoid Unicode corruption in shell string processing. + SUBSCRIPTION_OUTBOUND_TAGS_JSON=$( + printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -ac --arg tag "$outbound_tag" '. + [$tag]' 2>/dev/null + ) + if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" ]; then + SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]" + fi + if [ -z "$SUBSCRIPTION_OUTBOUND_NAMES" ]; then SUBSCRIPTION_OUTBOUND_NAMES="$display_name" else From c9cf5cb6244f20490ae0bade4631350c6e234732 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sun, 5 Apr 2026 15:09:38 +0300 Subject: [PATCH 13/75] =?UTF-8?q?=D0=A3=D1=81=D0=B8=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=D0=BE=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B9=D1=87=D0=B8=D0=B2=D0=BE=D1=81=D1=82=D1=8C=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- podkop/files/usr/bin/podkop | 526 ++++++++++++++++++++++++-------- podkop/files/usr/lib/helpers.sh | 99 +++++- 2 files changed, 487 insertions(+), 138 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index b23481ec..8abf2529 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -147,6 +147,213 @@ has_outbound_section() { return $section_exists } +get_subscription_json_path() { + local section="$1" + + echo "$TMP_SUBSCRIPTION_FOLDER/${section}.json" +} + +get_subscription_url_cache_path() { + local section="$1" + + echo "$TMP_SUBSCRIPTION_FOLDER/${section}.url" +} + +subscription_cache_is_usable() { + local subscription_json_path="$1" + + [ -s "$subscription_json_path" ] || return 1 + + validate_subscription_file "$subscription_json_path" +} + +wait_for_subscription_connectivity() { + local section="$1" + local subscription_url="$2" + local service_proxy_address="$3" + local attempts="${4:-12}" + local wait="${5:-5}" + local timeout="${6:-5}" + local attempt + + for attempt in $(seq 1 "$attempts"); do + if check_subscription_connectivity "$subscription_url" "$service_proxy_address" 1 0 "$timeout"; then + log "Subscription connectivity check passed for section '$section'" "info" + return 0 + fi + + log "Subscription source is unavailable for section '$section' [$attempt/$attempts]" "warn" + [ "$attempt" -lt "$attempts" ] && sleep "$wait" + done + + log "Subscription connectivity check failed for section '$section' after $attempts attempts" "error" + return 1 +} + +download_subscription_into_cache() { + local section="$1" + local subscription_url="$2" + local subscription_json_path="$3" + local subscription_url_cache_path="$4" + local service_proxy_address="$5" + local tmpfile + + mkdir -p "$TMP_SUBSCRIPTION_FOLDER" + tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.download.XXXXXX")" || return 1 + + if ! download_subscription "$subscription_url" "$tmpfile" "$service_proxy_address" 3 2 10; then + rm -f "$tmpfile" + return 1 + fi + + if ! validate_subscription_file "$tmpfile"; then + log "Downloaded subscription for section '$section' is invalid" "error" + rm -f "$tmpfile" + return 1 + fi + + if [ -f "$subscription_json_path" ] && cmp -s "$tmpfile" "$subscription_json_path"; then + rm -f "$tmpfile" + printf '%s' "$subscription_url" > "$subscription_url_cache_path" + log "Subscription for section '$section' is unchanged" "info" + return 2 + fi + + mv "$tmpfile" "$subscription_json_path" || { + rm -f "$tmpfile" + return 1 + } + + printf '%s' "$subscription_url" > "$subscription_url_cache_path" + return 0 +} + +prepare_subscription_cache_for_startup() { + local section="$1" + local connection_type proxy_config_type subscription_url subscription_json_path subscription_url_cache_path + local cached_subscription_url service_proxy_address had_usable_cache cache_needs_refresh + + config_get connection_type "$section" "connection_type" + [ "$connection_type" = "proxy" ] || return 0 + + config_get proxy_config_type "$section" "proxy_config_type" + [ "$proxy_config_type" = "subscription" ] || return 0 + + config_get subscription_url "$section" "subscription_url" + if [ -z "$subscription_url" ]; then + log "Subscription URL is not set for section '$section'. Aborted." "fatal" + exit 1 + fi + + subscription_json_path="$(get_subscription_json_path "$section")" + subscription_url_cache_path="$(get_subscription_url_cache_path "$section")" + cached_subscription_url="" + had_usable_cache=0 + cache_needs_refresh=0 + + if subscription_cache_is_usable "$subscription_json_path"; then + had_usable_cache=1 + else + rm -f "$subscription_json_path" + fi + + if [ -f "$subscription_url_cache_path" ]; then + cached_subscription_url="$(cat "$subscription_url_cache_path" 2> /dev/null)" + fi + + if [ "$had_usable_cache" -eq 0 ] || [ "$cached_subscription_url" != "$subscription_url" ]; then + cache_needs_refresh=1 + fi + + if [ "$cache_needs_refresh" -eq 0 ]; then + return 0 + fi + + service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')" + + if wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address"; then + if download_subscription_into_cache \ + "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address"; then + return 0 + fi + fi + + if [ "$had_usable_cache" -eq 1 ]; then + log "Keeping cached subscription for section '$section' until a fresh download succeeds" "warn" + return 0 + fi + + log "No usable subscription cache for section '$section'; podkop startup will wait for internet connectivity" "warn" + subscription_startup_blocked=1 + return 1 +} + +prepare_subscription_caches_for_startup() { + subscription_startup_blocked=0 + config_foreach prepare_subscription_cache_for_startup "section" + + [ "$subscription_startup_blocked" -eq 0 ] +} + +stop_subscription_startup_retry_worker() { + local pidfile="/var/run/podkop_subscription_retry.pid" + + if [ -f "$pidfile" ]; then + pid="$(cat "$pidfile" 2> /dev/null)" + if [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null; then + kill "$pid" 2> /dev/null + log "Stopped deferred startup recovery worker" + fi + rm -f "$pidfile" + fi +} + +start_subscription_startup_retry_worker() { + local pidfile="/var/run/podkop_subscription_retry.pid" + + if [ -f "$pidfile" ]; then + pid="$(cat "$pidfile" 2> /dev/null)" + if [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null; then + log "Deferred startup recovery worker is already running" "debug" + return 0 + fi + rm -f "$pidfile" + fi + + ( + trap 'rm -f "'"$pidfile"'"' EXIT INT TERM + + while true; do + config_load "$PODKOP_CONFIG" + + if prepare_subscription_caches_for_startup; then + log "Subscription cache is ready, resuming deferred podkop startup" "info" + rm -f "$pidfile" + start_main + start_rc=$? + + if [ "$start_rc" -eq 0 ]; then + config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 + if [ "$dont_touch_dhcp" -eq 0 ]; then + dnsmasq_configure + fi + + uci_set "podkop" "settings" "shutdown_correctly" 0 + uci commit "podkop" && config_load "$PODKOP_CONFIG" + fi + + exit "$start_rc" + fi + + log "Deferred podkop startup is still waiting for subscription connectivity" "warn" + sleep 10 + done + ) & + + echo $! > "$pidfile" + log "Started deferred startup recovery worker with PID $!" "warn" +} + start_main() { log "Starting podkop" @@ -167,6 +374,14 @@ start_main() { mkdir -p "$TMP_RULESET_FOLDER" mkdir -p "$TMP_SUBSCRIPTION_FOLDER" + if ! prepare_subscription_caches_for_startup; then + log "Podkop startup is deferred until the subscription source becomes reachable" "warn" + start_subscription_startup_retry_worker + return 2 + fi + + stop_subscription_startup_retry_worker + # base route_table_rule_mark create_nft_rules @@ -186,6 +401,8 @@ start_main() { stop_main() { log "Stopping the podkop" + stop_subscription_startup_retry_worker + if [ -f /var/run/podkop_list_update.pid ]; then pid=$(cat /var/run/podkop_list_update.pid) if kill -0 "$pid" 2> /dev/null; then @@ -220,6 +437,15 @@ stop_main() { start() { start_main + start_rc=$? + + if [ "$start_rc" -eq 2 ]; then + return 0 + fi + + if [ "$start_rc" -ne 0 ]; then + return "$start_rc" + fi config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 if [ "$dont_touch_dhcp" -eq 0 ]; then @@ -245,8 +471,8 @@ stop() { reload() { log "Podkop reload" - stop_main - start_main + stop + start } restart() { @@ -587,7 +813,7 @@ ensure_nft_ready_for_list_update() { list_update() { - echolog "🔄 Starting lists update..." + echolog "рџ”„ Starting lists update..." local nslookup_timeout=3 local nslookup_attempts=10 @@ -600,7 +826,7 @@ list_update() { # DNS Check for i in $(seq 1 $nslookup_attempts); do if nslookup -timeout=$nslookup_timeout openwrt.org > /dev/null 2>&1; then - echolog "✅ DNS check passed" + echolog "вњ… DNS check passed" break fi echolog "DNS is unavailable [$i/$nslookup_attempts]" @@ -608,7 +834,7 @@ list_update() { done if [ "$i" -eq $nslookup_attempts ]; then - echolog "❌ DNS check failed after $nslookup_attempts attempts" + echolog "вќЊ DNS check failed after $nslookup_attempts attempts" return 1 fi @@ -619,12 +845,12 @@ list_update() { if [ -n "$service_proxy_address" ]; then if curl -s -x "http://$service_proxy_address" -m $curl_timeout https://github.com > /dev/null; then - echolog "✅ GitHub connection check passed (via proxy)" + echolog "вњ… GitHub connection check passed (via proxy)" break fi else if curl -s -m $curl_timeout https://github.com > /dev/null; then - echolog "✅ GitHub connection check passed" + echolog "вњ… GitHub connection check passed" break fi fi @@ -637,16 +863,16 @@ list_update() { done if [ "$i" -eq $curl_attempts ]; then - echolog "❌ GitHub connection check failed after $curl_attempts attempts" + echolog "вќЊ GitHub connection check failed after $curl_attempts attempts" return 1 fi if ! ensure_nft_ready_for_list_update; then - echolog "❌ NFT table is unavailable, cannot update lists" + echolog "вќЊ NFT table is unavailable, cannot update lists" return 1 fi - echolog "📥 Downloading and processing lists..." + echolog "рџ“Ґ Downloading and processing lists..." local update_failed=0 config_foreach import_community_subnet_lists "section" || update_failed=1 @@ -654,17 +880,19 @@ list_update() { config_foreach import_subnets_from_remote_subnet_lists "section" || update_failed=1 if [ "$update_failed" -eq 0 ]; then - echolog "✅ Lists update completed successfully" + echolog "вњ… Lists update completed successfully" else - echolog "❌ Lists update failed" + echolog "вќЊ Lists update failed" return 1 fi } subscription_update() { - echolog "🔄 Starting subscription update..." + echolog "рџ”„ Starting subscription update..." local has_subscription=0 + local updated_sections=0 + local failed_sections=0 _check_subscription_section() { local section="$1" @@ -683,13 +911,14 @@ subscription_update() { config_foreach _check_subscription_section "section" if [ "$has_subscription" -eq 0 ]; then - echolog "ℹ️ No subscription sections found, nothing to update" + echolog "в„№пёЏ No subscription sections found, nothing to update" return 0 fi _update_subscription_for_section() { local section="$1" local connection_type proxy_config_type subscription_url subscription_json_path + local subscription_url_cache_path service_proxy_address update_result outbounds_count config_get connection_type "$section" "connection_type" if [ "$connection_type" != "proxy" ]; then @@ -704,31 +933,44 @@ subscription_update() { config_get subscription_url "$section" "subscription_url" if [ -z "$subscription_url" ]; then - echolog "❌ Subscription URL not set for section '$section'" + echolog "вќЊ Subscription URL not set for section '$section'" + failed_sections=$((failed_sections + 1)) return fi mkdir -p "$TMP_SUBSCRIPTION_FOLDER" - subscription_json_path="$TMP_SUBSCRIPTION_FOLDER/${section}.json" - local subscription_url_cache_path - subscription_url_cache_path="$TMP_SUBSCRIPTION_FOLDER/${section}.url" + subscription_json_path="$(get_subscription_json_path "$section")" + subscription_url_cache_path="$(get_subscription_url_cache_path "$section")" - echolog "📥 Updating subscription for section '$section'..." + echolog "рџ“Ґ Updating subscription for section '$section'..." - local service_proxy_address service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')" - # Remove old cached file to force re-download - rm -f "$subscription_json_path" - download_subscription "$subscription_url" "$subscription_json_path" "$service_proxy_address" - - if [ ! -f "$subscription_json_path" ] || [ ! -s "$subscription_json_path" ]; then - echolog "❌ Failed to download subscription for section '$section'" + if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 6 5 5; then + echolog "вќЊ Failed to download subscription for section '$section'" + failed_sections=$((failed_sections + 1)) return fi - printf '%s' "$subscription_url" > "$subscription_url_cache_path" - local outbounds_count + download_subscription_into_cache \ + "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address" + update_result=$? + + case "$update_result" in + 0) + updated_sections=$((updated_sections + 1)) + ;; + 2) + echolog "в„№пёЏ Subscription for section '$section' is unchanged" + return + ;; + *) + echolog "вќЊ Failed to download subscription for section '$section'" + failed_sections=$((failed_sections + 1)) + return + ;; + esac + outbounds_count=$(jq -r '[.outbounds[] | select( .type != "selector" and .type != "urltest" and @@ -737,13 +979,33 @@ subscription_update() { .type != "block" )] | length' "$subscription_json_path" 2>/dev/null) - echolog "✅ Subscription updated for section '$section': $outbounds_count outbounds" + echolog "вњ… Subscription updated for section '$section': $outbounds_count outbounds" } config_foreach _update_subscription_for_section "section" - echolog "🔄 Restarting podkop to apply updated subscriptions..." + if [ "$updated_sections" -eq 0 ]; then + if [ "$failed_sections" -gt 0 ]; then + echolog "вќЊ Subscription update finished with errors; keeping the last working cache" + return 1 + fi + + echolog "в„№пёЏ Subscription update completed: no changes detected" + return 0 + fi + + echolog "рџ”„ Restarting podkop to apply updated subscriptions..." restart - echolog "✅ Subscription update completed" + restart_rc=$? + if [ "$restart_rc" -ne 0 ]; then + echolog "вќЊ Subscription was downloaded, but podkop restart failed" + return "$restart_rc" + fi + + if [ "$failed_sections" -gt 0 ]; then + echolog "вњ… Subscription update applied for changed sections; failed sections kept their previous cache" + else + echolog "вњ… Subscription update completed" + fi } # sing-box funcs @@ -1002,12 +1264,16 @@ configure_outbound_handler() { fi mkdir -p "$TMP_SUBSCRIPTION_FOLDER" - subscription_json_path="$TMP_SUBSCRIPTION_FOLDER/${section}.json" - local subscription_url_cache_path cached_subscription_url should_download - subscription_url_cache_path="$TMP_SUBSCRIPTION_FOLDER/${section}.url" + subscription_json_path="$(get_subscription_json_path "$section")" + local subscription_url_cache_path cached_subscription_url should_download had_usable_cache + subscription_url_cache_path="$(get_subscription_url_cache_path "$section")" should_download=0 + had_usable_cache=0 - if [ ! -f "$subscription_json_path" ] || [ ! -s "$subscription_json_path" ]; then + if subscription_cache_is_usable "$subscription_json_path"; then + had_usable_cache=1 + else + rm -f "$subscription_json_path" should_download=1 fi @@ -1019,24 +1285,30 @@ configure_outbound_handler() { if [ "$cached_subscription_url" != "$subscription_url" ]; then if [ -n "$cached_subscription_url" ]; then - log "Subscription URL changed for section '$section', refreshing cache" "debug" + log "Subscription URL changed for section '$section'" "warn" + fi + if [ "$had_usable_cache" -eq 0 ]; then + should_download=1 + else + log "Using cached subscription for section '$section' until a fresh download succeeds" "warn" fi - should_download=1 - rm -f "$subscription_json_path" fi if [ "$should_download" -eq 1 ]; then log "Downloading subscription for section '$section'" local service_proxy_address service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')" - download_subscription "$subscription_url" "$subscription_json_path" "$service_proxy_address" - if [ ! -f "$subscription_json_path" ] || [ ! -s "$subscription_json_path" ]; then - log "Failed to download subscription for section '$section'. Aborted." "fatal" - exit 1 + if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 6 5 5 || + ! download_subscription_into_cache \ + "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address"; then + if [ "$had_usable_cache" -eq 1 ]; then + log "Failed to refresh subscription for section '$section', continuing with cached data" "warn" + else + log "Failed to download subscription for section '$section'. Aborted." "fatal" + exit 1 + fi fi - - printf '%s' "$subscription_url" > "$subscription_url_cache_path" fi # Parse subscription outbounds @@ -1169,10 +1441,10 @@ configure_outbound_handler() { config=$(sing_box_cm_add_interface_outbound "$config" "$outbound_tag" "$interface_name" "$domain_resolver_tag") ;; block) - log "Connection type 'block' detected for the $section section – no outbound will be created (handled via reject route rules)" + log "Connection type 'block' detected for the $section section – no outbound will be created (handled via reject route rules)" ;; exclusion) - log "Connection type 'exclusion' detected for the $section section – no outbound will be created (handled via route rules)" + log "Connection type 'exclusion' detected for the $section section – no outbound will be created (handled via route rules)" ;; *) log "Unknown connection type '$connection_type' for the $section section. Aborted." "fatal" @@ -2133,7 +2405,7 @@ check_nft() { # Check if table exists if ! nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then - nolog "❌ $NFT_TABLE_NAME not found" + nolog "вќЊ $NFT_TABLE_NAME not found" return 1 fi @@ -2782,9 +3054,9 @@ global_check() { local PODKOP_LUCI_VERSION="Unknown" [ -n "$1" ] && PODKOP_LUCI_VERSION="$1" - print_global "📡 Global check run!" - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "🛠️ System info" + print_global "рџ“Ў Global check run!" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "рџ› пёЏ System info" local system_info_json system_info_json=$(get_system_info) @@ -2799,17 +3071,17 @@ global_check() { openwrt_version=$(echo "$system_info_json" | jq -r '.openwrt_version // "unknown"') device_model=$(echo "$system_info_json" | jq -r '.device_model // "unknown"') - print_global "🕳️ Podkop: $podkop_version (latest: $podkop_latest_version)" - print_global "🕳️ LuCI App: $luci_app_version" - print_global "📦 Sing-box: $sing_box_version" - print_global "🛜 OpenWrt: $openwrt_version" - print_global "🛜 Device: $device_model" + print_global "рџ•іпёЏ Podkop: $podkop_version (latest: $podkop_latest_version)" + print_global "рџ•іпёЏ LuCI App: $luci_app_version" + print_global "📦 Sing-box: $sing_box_version" + print_global "рџ›њ OpenWrt: $openwrt_version" + print_global "рџ›њ Device: $device_model" else - print_global "❌ Failed to get system info" + print_global "вќЊ Failed to get system info" fi - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "➡️ DNS status" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "вћЎпёЏ DNS status" local dns_check_json dns_check_json=$(check_dns_available) @@ -2828,24 +3100,24 @@ global_check() { # Bootstrap DNS if [ -n "$bootstrap_dns_server" ]; then if [ "$bootstrap_dns_status" -eq 1 ]; then - print_global "✅ Bootstrap DNS: $bootstrap_dns_server" + print_global "вњ… Bootstrap DNS: $bootstrap_dns_server" else - print_global "❌ Bootstrap DNS: $bootstrap_dns_server" + print_global "вќЊ Bootstrap DNS: $bootstrap_dns_server" fi fi # DNS server status if [ "$dns_status" -eq 1 ]; then - print_global "✅ Main DNS: $dns_server [$dns_type]" + print_global "вњ… Main DNS: $dns_server [$dns_type]" else - print_global "❌ Main DNS: $dns_server [$dns_type]" + print_global "вќЊ Main DNS: $dns_server [$dns_type]" fi # DNS on router if [ "$dns_on_router" -eq 1 ]; then - print_global "✅ DNS on router" + print_global "вњ… DNS on router" else - print_global "❌ DNS on router" + print_global "вќЊ DNS on router" fi # DHCP configuration check @@ -2853,20 +3125,20 @@ global_check() { config_get dont_touch_dhcp "settings" "dont_touch_dhcp" if [ "$dont_touch_dhcp" = "1" ]; then - print_global "⚠️ dont_touch_dhcp is enabled. 📄 DHCP config:" + print_global "вљ пёЏ dont_touch_dhcp is enabled. рџ“„ DHCP config:" awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp elif [ "$dhcp_config_status" -eq 0 ]; then - print_global "❌ DHCP configuration differs from template. 📄 DHCP config:" + print_global "вќЊ DHCP configuration differs from template. рџ“„ DHCP config:" awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp else - print_global "✅ /etc/config/dhcp" + print_global "вњ… /etc/config/dhcp" fi else - print_global "❌ Failed to get DNS info" + print_global "вќЊ Failed to get DNS info" fi - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "📦 Sing-box status" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "📦 Sing-box status" local singbox_check_json singbox_check_json=$(check_sing_box) @@ -2882,46 +3154,46 @@ global_check() { sing_box_ports_listening=$(echo "$singbox_check_json" | jq -r '.sing_box_ports_listening // 0') if [ "$sing_box_installed" -eq 1 ]; then - print_global "✅ Sing-box installed" + print_global "вњ… Sing-box installed" else - print_global "❌ Sing-box installed" + print_global "вќЊ Sing-box installed" fi if [ "$sing_box_version_ok" -eq 1 ]; then - print_global "✅ Sing-box version is compatible (newer than 1.12.4)" + print_global "вњ… Sing-box version is compatible (newer than 1.12.4)" else - print_global "❌ Sing-box version is not compatible (older than 1.12.4)" + print_global "вќЊ Sing-box version is not compatible (older than 1.12.4)" fi if [ "$sing_box_service_exist" -eq 1 ]; then - print_global "✅ Sing-box service exist" + print_global "вњ… Sing-box service exist" else - print_global "❌ Sing-box service exist" + print_global "вќЊ Sing-box service exist" fi if [ "$sing_box_autostart_disabled" -eq 1 ]; then - print_global "✅ Sing-box autostart disabled" + print_global "вњ… Sing-box autostart disabled" else - print_global "❌ Sing-box autostart disabled" + print_global "вќЊ Sing-box autostart disabled" fi if [ "$sing_box_process_running" -eq 1 ]; then - print_global "✅ Sing-box process running" + print_global "вњ… Sing-box process running" else - print_global "❌ Sing-box process running" + print_global "вќЊ Sing-box process running" fi if [ "$sing_box_ports_listening" -eq 1 ]; then - print_global "✅ Sing-box listening ports" + print_global "вњ… Sing-box listening ports" else - print_global "❌ Sing-box listening ports" + print_global "вќЊ Sing-box listening ports" fi else - print_global "❌ Failed to get sing-box info" + print_global "вќЊ Failed to get sing-box info" fi - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "🧱 NFT rules status" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "рџ§± NFT rules status" local nft_check_json nft_check_json=$(check_nft_rules) @@ -2939,78 +3211,78 @@ global_check() { rules_other_mark_exist=$(echo "$nft_check_json" | jq -r '.rules_other_mark_exist // 0') if [ "$table_exist" -eq 1 ]; then - print_global "✅ Table exist" + print_global "вњ… Table exist" else - print_global "❌ Table exist" + print_global "вќЊ Table exist" fi if [ "$rules_mangle_exist" -eq 1 ]; then - print_global "✅ Rules mangle exist" + print_global "вњ… Rules mangle exist" else - print_global "❌ Rules mangle exist" + print_global "вќЊ Rules mangle exist" fi if [ "$rules_mangle_counters" -eq 1 ]; then - print_global "✅ Rules mangle counters" + print_global "вњ… Rules mangle counters" else - print_global "⚠️ Rules mangle counters" + print_global "вљ пёЏ Rules mangle counters" fi if [ "$rules_mangle_output_exist" -eq 1 ]; then - print_global "✅ Rules mangle output exist" + print_global "вњ… Rules mangle output exist" else - print_global "❌ Rules mangle output exist" + print_global "вќЊ Rules mangle output exist" fi if [ "$rules_mangle_output_counters" -eq 1 ]; then - print_global "✅ Rules mangle output counters" + print_global "вњ… Rules mangle output counters" else - print_global "⚠️ Rules mangle output counters" + print_global "вљ пёЏ Rules mangle output counters" fi if [ "$rules_proxy_exist" -eq 1 ]; then - print_global "✅ Rules proxy exist" + print_global "вњ… Rules proxy exist" else - print_global "❌ Rules proxy exist" + print_global "вќЊ Rules proxy exist" fi if [ "$rules_proxy_counters" -eq 1 ]; then - print_global "✅ Rules proxy counters" + print_global "вњ… Rules proxy counters" else - print_global "⚠️ Rules proxy counters" + print_global "вљ пёЏ Rules proxy counters" fi if [ "$rules_other_mark_exist" -eq 1 ]; then - print_global "⚠️ Additional marking rules found:" + print_global "вљ пёЏ Additional marking rules found:" nft list ruleset | awk '/table inet '"$NFT_TABLE_NAME"'/{flag=1; next} /^table/{flag=0} !flag' | grep -E "mark set|meta mark" else - print_global "✅ Additional marking rules found" + print_global "вњ… Additional marking rules found" fi else - print_global "❌ Failed to get NFT rules info" + print_global "вќЊ Failed to get NFT rules info" fi - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "📄 Podkop config" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "рџ“„ Podkop config" show_config - # print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - # print_global "🔧 System check" + # print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + # print_global "рџ”§ System check" # if grep -E "^nameserver\s+([0-9]{1,3}\.){3}[0-9]{1,3}" "$RESOLV_CONF" | grep -vqE "127\.0\.0\.1|0\.0\.0\.0"; then - # print_global "❌ /etc/resolv.conf contains external nameserver:" + # print_global "вќЊ /etc/resolv.conf contains external nameserver:" # cat /etc/resolv.conf # echo "" # else - # print_global "✅ /etc/resolv.conf" + # print_global "вњ… /etc/resolv.conf" # fi - # print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - # print_global "🧱 NFT table" + # print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + # print_global "рџ§± NFT table" # check_nft - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "📄 WAN config" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "рџ“„ WAN config" if uci show network.wan > /dev/null 2>&1; then awk ' /^config / { @@ -3031,20 +3303,20 @@ global_check() { } ' /etc/config/network else - print_global "❌ WAN configuration not found" + print_global "вќЊ WAN configuration not found" fi if uci show network | grep -q endpoint_host; then uci show network | grep endpoint_host | cut -d'=' -f2 | tr -d "'\" " | while read -r host; do if [ "$host" = "engage.cloudflareclient.com" ]; then - print_global "⚠️ WARP detected: $host" + print_global "вљ пёЏ WARP detected: $host" continue fi ip_prefix=$(echo "$host" | cut -d'.' -f1,2) if echo "$CLOUDFLARE_OCTETS" | grep -wq "$ip_prefix"; then - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "⚠️ WARP detected: $host" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "вљ пёЏ WARP detected: $host" fi done fi @@ -3055,19 +3327,19 @@ global_check() { allowed_ips=$(uci get "${peer_section}.allowed_ips" 2> /dev/null) if [ "$allowed_ips" = "0.0.0.0/0" ]; then - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "⚠️ WG Route allowed IP enabled with 0.0.0.0/0" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "вљ пёЏ WG Route allowed IP enabled with 0.0.0.0/0" fi done fi if [ -f "/etc/init.d/zapret" ]; then - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "⚠️ Zapret detected" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "вљ пёЏ Zapret detected" fi - print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" - print_global "🥸 FakeIP status" + print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" + print_global "🥸 FakeIP status" local fakeip_check_json fakeip_check_json=$(check_fakeip) @@ -3078,21 +3350,21 @@ global_check() { fakeip_status=$(echo "$fakeip_check_json" | jq -r '.fakeip // false') if [ "$fakeip_status" = "true" ]; then - print_global "✅ Router DNS is routed through sing-box" + print_global "вњ… Router DNS is routed through sing-box" else - print_global "⚠️ Router DNS is NOT routed through sing-box" + print_global "вљ пёЏ Router DNS is NOT routed through sing-box" fi else - print_global "❌ Failed to get FakeIP info" + print_global "вќЊ Failed to get FakeIP info" fi local fakeip_address fakeip_address=$(dig +short @127.0.0.42 $FAKEIP_TEST_DOMAIN) if echo "$fakeip_address" | grep -q "^198\.18\."; then - print_global "✅ Sing-box works with FakeIP: $fakeip_address" + print_global "вњ… Sing-box works with FakeIP: $fakeip_address" else - print_global "❌ Sing-box does NOT work with FakeIP: $fakeip_address" + print_global "вќЊ Sing-box does NOT work with FakeIP: $fakeip_address" fi } diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 24f656c9..dda8ea80 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -417,6 +417,7 @@ download_subscription() { local http_proxy_address="$3" local retries="${4:-3}" local wait="${5:-2}" + local timeout="${6:-10}" local sb_version device_model kernel_version hwid sb_version="$(get_sing_box_version)" @@ -424,24 +425,100 @@ download_subscription() { kernel_version="$(get_kernel_version)" hwid="$(generate_hwid)" - local header_args="" - header_args="--header='User-Agent: singbox/$sb_version'" - header_args="$header_args --header='X-HWID: $hwid'" - header_args="$header_args --header='X-Device-OS: OpenWrt Linux'" - header_args="$header_args --header='X-Device-Model: $device_model'" - header_args="$header_args --header='X-Ver-OS: $kernel_version'" - header_args="$header_args --header='Accept-Language: ru-RU,en,*'" - header_args="$header_args --header='X-Device-Locale: EN'" + local tmpfile + tmpfile="${filepath}.part.$$" + rm -f "$tmpfile" for attempt in $(seq 1 "$retries"); do if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ - eval wget -O "$filepath" $header_args "$url" && break + wget -T "$timeout" -t 1 -O "$tmpfile" \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" else - eval wget -O "$filepath" $header_args "$url" && break + wget -T "$timeout" -t 1 -O "$tmpfile" \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" + fi + + if [ $? -eq 0 ] && [ -s "$tmpfile" ]; then + mv "$tmpfile" "$filepath" + return 0 fi + rm -f "$tmpfile" log "Attempt $attempt/$retries to download subscription from $url failed" "warn" sleep "$wait" done -} \ No newline at end of file + + rm -f "$tmpfile" + return 1 +} + +check_subscription_connectivity() { + local url="$1" + local http_proxy_address="$2" + local retries="${3:-3}" + local wait="${4:-2}" + local timeout="${5:-5}" + + local sb_version device_model kernel_version hwid + sb_version="$(get_sing_box_version)" + device_model="$(get_device_model)" + kernel_version="$(get_kernel_version)" + hwid="$(generate_hwid)" + + local attempt + for attempt in $(seq 1 "$retries"); do + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ + wget -q -T "$timeout" -t 1 -O /dev/null \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" && return 0 + else + wget -q -T "$timeout" -t 1 -O /dev/null \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" && return 0 + fi + + [ "$attempt" -lt "$retries" ] && sleep "$wait" + done + + return 1 +} + +validate_subscription_file() { + local filepath="$1" + + [ -s "$filepath" ] || return 1 + + jq -e ' + type == "object" and + (.outbounds | type == "array") and + ((.outbounds | length) > 0) + ' "$filepath" > /dev/null 2>&1 +} From bec1f4c10e40ae88f586412d28e63c4d7d3d1457 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sun, 5 Apr 2026 15:51:24 +0300 Subject: [PATCH 14/75] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=81=D0=BE=D0=B2=D0=BC=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20wget=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B5=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- podkop/files/usr/lib/helpers.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index dda8ea80..c16b9382 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -432,7 +432,7 @@ download_subscription() { for attempt in $(seq 1 "$retries"); do if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ - wget -T "$timeout" -t 1 -O "$tmpfile" \ + wget -T "$timeout" -O "$tmpfile" \ --header "User-Agent: singbox/$sb_version" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ @@ -442,7 +442,7 @@ download_subscription() { --header "X-Device-Locale: EN" \ "$url" else - wget -T "$timeout" -t 1 -O "$tmpfile" \ + wget -T "$timeout" -O "$tmpfile" \ --header "User-Agent: singbox/$sb_version" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ @@ -484,7 +484,7 @@ check_subscription_connectivity() { for attempt in $(seq 1 "$retries"); do if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ - wget -q -T "$timeout" -t 1 -O /dev/null \ + wget -q -T "$timeout" -O /dev/null \ --header "User-Agent: singbox/$sb_version" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ @@ -494,7 +494,7 @@ check_subscription_connectivity() { --header "X-Device-Locale: EN" \ "$url" && return 0 else - wget -q -T "$timeout" -t 1 -O /dev/null \ + wget -q -T "$timeout" -O /dev/null \ --header "User-Agent: singbox/$sb_version" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ From 7d551f9bfdb498fb2ce3279051cda971af1c5313 Mon Sep 17 00:00:00 2001 From: Artem Kireev Date: Wed, 13 May 2026 10:32:48 +0300 Subject: [PATCH 15/75] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20deadlock=20=D0=BF=D1=80=D0=B8=20=D1=85?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B4=D0=BD=D0=BE=D0=BC=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D1=82=D0=B5=20=D1=81=20download=5Flists=5Fvia=5Fproxy=3D?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При холодном старте подкоп вызывает prepare_subscription_caches_for_startup до запуска sing-box. Если включена опция download_lists_via_proxy=1, функция пытается скачать подписку через прокси 127.0.0.1:4534 (SB_SERVICE_MIXED_INBOUND), который ещё не существует, потому что sing-box запускается только после prepare_subscription_caches_for_startup. В результате wait_for_subscription_connectivity безуспешно делает 12 попыток по 5 секунд (60 секунд ожидания), после чего стартует retry worker, который крутится в цикле каждые 10 секунд и тоже использует прокси — но sing-box никогда не запустится без подписки. Получается deadlock: подписка нужна для старта sing-box, а sing-box нужен для скачивания подписки через прокси. Ситуация усугубляется тем, что /tmp/sing-box/subscriptions лежит в tmpfs, поэтому на холодном старте кэш всегда пустой и cache_needs_refresh=1. На этапе bootstrap скачиваем подписку напрямую, игнорируя download_lists_via_proxy. После старта sing-box subscription_update и list_update продолжают использовать прокси согласно настройке. --- podkop/files/usr/bin/podkop | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 8abf2529..d1125c65 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -269,7 +269,14 @@ prepare_subscription_cache_for_startup() { return 0 fi - service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')" + # Bootstrap subscription directly: sing-box (and its service proxy) is not + # running yet at this stage, so download_lists_via_proxy would deadlock here. + service_proxy_address="" + local _download_lists_via_proxy + config_get_bool _download_lists_via_proxy "settings" "download_lists_via_proxy" 0 + if [ "$_download_lists_via_proxy" -eq 1 ]; then + log "download_lists_via_proxy is set, but sing-box is not running yet during startup. Bootstrapping subscription for section '$section' over a direct connection" "info" + fi if wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address"; then if download_subscription_into_cache \ From 0f44d09b5e9b46bb08eb85bed60793f881e343df Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 26 May 2026 22:56:45 +0300 Subject: [PATCH 16/75] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D0=BF=D1=80=D0=B8=20=D1=85?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B4=D0=BD=D0=BE=D0=BC=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- podkop/files/usr/bin/podkop | 476 +++++++++++++++++++++--------- podkop/files/usr/lib/constants.sh | 5 +- podkop/files/usr/lib/helpers.sh | 396 ++++++++++++++++++++++--- 3 files changed, 692 insertions(+), 185 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index d1125c65..f0d75a2c 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -150,21 +150,148 @@ has_outbound_section() { get_subscription_json_path() { local section="$1" - echo "$TMP_SUBSCRIPTION_FOLDER/${section}.json" + echo "$SUBSCRIPTION_CACHE_FOLDER/${section}.json" } get_subscription_url_cache_path() { local section="$1" - echo "$TMP_SUBSCRIPTION_FOLDER/${section}.url" + echo "$SUBSCRIPTION_CACHE_FOLDER/${section}.url" +} + +get_subscription_rejected_cache_path() { + local section="$1" + + echo "$SUBSCRIPTION_CACHE_FOLDER/${section}.rejected" +} + +ensure_subscription_cache_dir() { + local state_dir_created=0 cache_dir_created=0 + + [ -d "$PODKOP_STATE_DIR" ] || state_dir_created=1 + [ -d "$SUBSCRIPTION_CACHE_FOLDER" ] || cache_dir_created=1 + mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" || return 1 + [ "$state_dir_created" -eq 1 ] && chmod 700 "$PODKOP_STATE_DIR" 2>/dev/null + [ "$cache_dir_created" -eq 1 ] && chmod 700 "$SUBSCRIPTION_CACHE_FOLDER" 2>/dev/null +} + +migrate_subscription_cache_from_tmp() { + local src_json src_url dst_json dst_url + + [ -d "$TMP_SUBSCRIPTION_FOLDER" ] || return 0 + ensure_subscription_cache_dir || return 0 + + for src_json in "$TMP_SUBSCRIPTION_FOLDER"/*.json; do + [ -e "$src_json" ] || continue + src_url="${src_json%.json}.url" + dst_json="$(get_subscription_json_path "$(basename "${src_json%.json}")")" + dst_url="$(get_subscription_url_cache_path "$(basename "${src_json%.json}")")" + + if [ ! -e "$dst_json" ] && subscription_cache_is_usable "$src_json"; then + cp "$src_json" "$dst_json" 2>/dev/null + [ -f "$src_url" ] && cp "$src_url" "$dst_url" 2>/dev/null + chmod 600 "$dst_json" "$dst_url" 2>/dev/null + log "Migrated subscription cache for section '$(basename "${src_json%.json}")' to persistent storage" "info" + fi + done +} + +mark_subscription_outbound_unavailable() { + local section="$1" + local subscription_json_path rejected_cache_path rejected_hash + + case " $SUBSCRIPTION_UNAVAILABLE_SECTIONS " in + *" $section "*) ;; + *) SUBSCRIPTION_UNAVAILABLE_SECTIONS="$SUBSCRIPTION_UNAVAILABLE_SECTIONS $section" ;; + esac + + log "Subscription cache for section '$section' is unavailable; matching traffic for this section will be rejected until refresh succeeds" "warn" + # A structurally valid subscription can still contain no sing-box usable + # proxy outbounds. Remember its hash so the retry worker does not persist, + # restart and reject exactly the same unusable feed in a flash-writing loop. + subscription_json_path="$(get_subscription_json_path "$section")" + rejected_cache_path="$(get_subscription_rejected_cache_path "$section")" + rejected_hash="$(md5sum "$subscription_json_path" 2>/dev/null | awk '{print $1}')" + if [ -n "$rejected_hash" ] && [ "$(cat "$rejected_cache_path" 2>/dev/null)" != "$rejected_hash" ]; then + printf '%s' "$rejected_hash" > "${rejected_cache_path}.tmp.$$" && mv "${rejected_cache_path}.tmp.$$" "$rejected_cache_path" + chmod 600 "$rejected_cache_path" 2>/dev/null + fi + subscription_startup_blocked=1 +} + +subscription_outbound_is_unavailable() { + local section="$1" + + case " $SUBSCRIPTION_UNAVAILABLE_SECTIONS " in + *" $section "*) return 0 ;; + esac + return 1 +} + +get_subscription_download_proxy_address() { + local section="$1" + local phase="$2" + local download_lists_via_proxy download_lists_via_proxy_section + + config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" 0 + [ "$download_lists_via_proxy" -eq 1 ] || return 0 + + config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section" + if [ -z "$download_lists_via_proxy_section" ]; then + log "download_lists_via_proxy is enabled but no proxy section is selected; using direct mode for $phase subscription download" "warn" + return 0 + fi + + if [ "$phase" = "bootstrap" ]; then + if [ "$download_lists_via_proxy_section" = "$section" ]; then + log "Cannot bootstrap subscription for section '$section' through itself before cache exists; using direct mode" "warn" + else + log "download_lists_via_proxy is configured, but bootstrap subscription download for section '$section' must use direct mode until sing-box starts" "info" + fi + return 0 + fi + + if [ "$download_lists_via_proxy_section" = "$section" ]; then + if subscription_cache_is_usable "$(get_subscription_json_path "$section")"; then + log "Updating subscription for section '$section' through its currently active cached proxy" "info" + echo "$(get_service_proxy_address 2>/dev/null || echo '')" + return 0 + fi + + log "Subscription section '$section' is selected as its own download proxy, but no usable cache exists yet; falling back to direct mode for bootstrap" "warn" + return 0 + fi + + local selected_connection_type selected_proxy_config_type + config_get selected_connection_type "$download_lists_via_proxy_section" "connection_type" + config_get selected_proxy_config_type "$download_lists_via_proxy_section" "proxy_config_type" + if [ "$selected_connection_type" = "proxy" ] && [ "$selected_proxy_config_type" = "subscription" ] && \ + ! subscription_cache_is_usable "$(get_subscription_json_path "$download_lists_via_proxy_section")"; then + log "Selected download proxy section '$download_lists_via_proxy_section' has no usable subscription cache; using direct mode for recovery of '$section'" "warn" + return 0 + fi + + echo "$(get_service_proxy_address 2>/dev/null || echo '')" } subscription_cache_is_usable() { local subscription_json_path="$1" + local rejected_cache_path current_hash rejected_hash [ -s "$subscription_json_path" ] || return 1 - validate_subscription_file "$subscription_json_path" + validate_subscription_file "$subscription_json_path" || return 1 + + rejected_cache_path="${subscription_json_path%.json}.rejected" + if [ -s "$rejected_cache_path" ]; then + current_hash="$(md5sum "$subscription_json_path" 2>/dev/null | awk '{print $1}')" + rejected_hash="$(cat "$rejected_cache_path" 2>/dev/null)" + if [ -n "$current_hash" ] && [ "$current_hash" = "$rejected_hash" ]; then + return 1 + fi + fi + + return 0 } wait_for_subscription_connectivity() { @@ -196,10 +323,11 @@ download_subscription_into_cache() { local subscription_json_path="$3" local subscription_url_cache_path="$4" local service_proxy_address="$5" - local tmpfile + local tmpfile persist_tmpfile url_tmpfile rejected_cache_path tmp_hash rejected_hash - mkdir -p "$TMP_SUBSCRIPTION_FOLDER" - tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.download.XXXXXX")" || return 1 + ensure_subscription_cache_dir || return 1 + mkdir -p "$TMP_SUBSCRIPTION_DOWNLOAD_FOLDER" || return 1 + tmpfile="$(mktemp "$TMP_SUBSCRIPTION_DOWNLOAD_FOLDER/${section}.download.XXXXXX")" || return 1 if ! download_subscription "$subscription_url" "$tmpfile" "$service_proxy_address" 3 2 10; then rm -f "$tmpfile" @@ -212,19 +340,37 @@ download_subscription_into_cache() { return 1 fi + rejected_cache_path="$(get_subscription_rejected_cache_path "$section")" + tmp_hash="$(md5sum "$tmpfile" 2>/dev/null | awk '{print $1}')" + rejected_hash="$(cat "$rejected_cache_path" 2>/dev/null)" + if [ -n "$tmp_hash" ] && [ "$tmp_hash" = "$rejected_hash" ]; then + log "Downloaded subscription for section '$section' is unchanged and was previously rejected because it contains no usable sing-box outbounds" "warn" + rm -f "$tmpfile" + return 1 + fi + if [ -f "$subscription_json_path" ] && cmp -s "$tmpfile" "$subscription_json_path"; then rm -f "$tmpfile" - printf '%s' "$subscription_url" > "$subscription_url_cache_path" + if [ "$(cat "$subscription_url_cache_path" 2>/dev/null)" != "$subscription_url" ]; then + url_tmpfile="${subscription_url_cache_path}.tmp.$$" + printf '%s' "$subscription_url" > "$url_tmpfile" && mv "$url_tmpfile" "$subscription_url_cache_path" + chmod 600 "$subscription_url_cache_path" 2>/dev/null + fi log "Subscription for section '$section' is unchanged" "info" return 2 fi - mv "$tmpfile" "$subscription_json_path" || { - rm -f "$tmpfile" + persist_tmpfile="${subscription_json_path}.tmp.$$" + cp "$tmpfile" "$persist_tmpfile" && mv "$persist_tmpfile" "$subscription_json_path" || { + rm -f "$tmpfile" "$persist_tmpfile" return 1 } + rm -f "$tmpfile" + rm -f "$rejected_cache_path" - printf '%s' "$subscription_url" > "$subscription_url_cache_path" + url_tmpfile="${subscription_url_cache_path}.tmp.$$" + printf '%s' "$subscription_url" > "$url_tmpfile" && mv "$url_tmpfile" "$subscription_url_cache_path" + chmod 600 "$subscription_json_path" "$subscription_url_cache_path" 2>/dev/null return 0 } @@ -251,6 +397,9 @@ prepare_subscription_cache_for_startup() { had_usable_cache=0 cache_needs_refresh=0 + ensure_subscription_cache_dir || true + migrate_subscription_cache_from_tmp + if subscription_cache_is_usable "$subscription_json_path"; then had_usable_cache=1 else @@ -270,7 +419,7 @@ prepare_subscription_cache_for_startup() { fi # Bootstrap subscription directly: sing-box (and its service proxy) is not - # running yet at this stage, so download_lists_via_proxy would deadlock here. + # running yet at this stage, so proxy-based bootstrap would deadlock here. service_proxy_address="" local _download_lists_via_proxy config_get_bool _download_lists_via_proxy "settings" "download_lists_via_proxy" 0 @@ -278,7 +427,7 @@ prepare_subscription_cache_for_startup() { log "download_lists_via_proxy is set, but sing-box is not running yet during startup. Bootstrapping subscription for section '$section' over a direct connection" "info" fi - if wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address"; then + if wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 2 2 5; then if download_subscription_into_cache \ "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address"; then return 0 @@ -287,12 +436,12 @@ prepare_subscription_cache_for_startup() { if [ "$had_usable_cache" -eq 1 ]; then log "Keeping cached subscription for section '$section' until a fresh download succeeds" "warn" - return 0 + else + log "No usable subscription cache for section '$section'; startup will continue with a temporary blocked outbound until subscription becomes reachable" "warn" + subscription_startup_blocked=1 fi - log "No usable subscription cache for section '$section'; podkop startup will wait for internet connectivity" "warn" - subscription_startup_blocked=1 - return 1 + return 0 } prepare_subscription_caches_for_startup() { @@ -330,30 +479,23 @@ start_subscription_startup_retry_worker() { ( trap 'rm -f "'"$pidfile"'"' EXIT INT TERM + # Let the primary sing-box instance come up first, so a configured + # service proxy can be used for the post-bootstrap refresh. + sleep 10 + while true; do config_load "$PODKOP_CONFIG" - if prepare_subscription_caches_for_startup; then - log "Subscription cache is ready, resuming deferred podkop startup" "info" + # Run in a child process: a successful subscription_update performs + # its own restart, which stops this worker via the pidfile safely. + if /usr/bin/podkop subscription_update >/dev/null 2>&1; then + log "Deferred subscription refresh succeeded; updated configuration is being applied" "info" rm -f "$pidfile" - start_main - start_rc=$? - - if [ "$start_rc" -eq 0 ]; then - config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 - if [ "$dont_touch_dhcp" -eq 0 ]; then - dnsmasq_configure - fi - - uci_set "podkop" "settings" "shutdown_correctly" 0 - uci commit "podkop" && config_load "$PODKOP_CONFIG" - fi - - exit "$start_rc" + exit 0 fi - log "Deferred podkop startup is still waiting for subscription connectivity" "warn" - sleep 10 + log "Deferred subscription refresh is still waiting for connectivity" "warn" + sleep 30 done ) & @@ -380,13 +522,13 @@ start_main() { mkdir -p "$TMP_SING_BOX_FOLDER" mkdir -p "$TMP_RULESET_FOLDER" mkdir -p "$TMP_SUBSCRIPTION_FOLDER" + ensure_subscription_cache_dir + migrate_subscription_cache_from_tmp - if ! prepare_subscription_caches_for_startup; then - log "Podkop startup is deferred until the subscription source becomes reachable" "warn" - start_subscription_startup_retry_worker - return 2 + prepare_subscription_caches_for_startup + if [ "$subscription_startup_blocked" -ne 0 ]; then + log "Podkop startup continues with temporary blocked subscription outbounds until sources become reachable" "warn" fi - stop_subscription_startup_retry_worker # base @@ -400,6 +542,10 @@ start_main() { config_foreach add_subscription_cron_job "section" /etc/init.d/sing-box start + if [ "$subscription_startup_blocked" -ne 0 ]; then + start_subscription_startup_retry_worker + fi + log "Nice" list_update & echo $! > /var/run/podkop_list_update.pid @@ -946,12 +1092,13 @@ subscription_update() { fi mkdir -p "$TMP_SUBSCRIPTION_FOLDER" + ensure_subscription_cache_dir subscription_json_path="$(get_subscription_json_path "$section")" subscription_url_cache_path="$(get_subscription_url_cache_path "$section")" echolog "рџ“Ґ Updating subscription for section '$section'..." - service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')" + service_proxy_address="$(get_subscription_download_proxy_address "$section" "runtime" 2>/dev/null || echo '')" if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 6 5 5; then echolog "вќЊ Failed to download subscription for section '$section'" @@ -1082,6 +1229,7 @@ sing_box_configure_inbounds() { sing_box_configure_outbounds() { log "Configure the outbounds section of a sing-box JSON configuration" + SUBSCRIPTION_UNAVAILABLE_SECTIONS="" config=$(sing_box_cm_add_direct_outbound "$config" "$SB_DIRECT_OUTBOUND_TAG") config_foreach configure_outbound_handler "section" @@ -1245,7 +1393,7 @@ configure_outbound_handler() { local subscription_url subscription_json_path urltest_tag selector_tag \ urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance \ urltest_testing_url subscription_group_by_countries subscription_group_by_countries_raw \ - subscription_outbound_tags_json + subscription_outbound_tags_json service_proxy_address subscription_ready config_get subscription_url "$section" "subscription_url" config_get urltest_check_interval "$section" "urltest_check_interval" "3m" @@ -1303,112 +1451,113 @@ configure_outbound_handler() { if [ "$should_download" -eq 1 ]; then log "Downloading subscription for section '$section'" - local service_proxy_address - service_proxy_address="$(get_service_proxy_address 2>/dev/null || echo '')" + # Config generation runs before sing-box is started. Never use + # its local download proxy here; runtime refresh will use it. + service_proxy_address="$(get_subscription_download_proxy_address "$section" "bootstrap" 2>/dev/null || echo '')" - if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 6 5 5 || + if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 1 0 5 || ! download_subscription_into_cache \ "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address"; then if [ "$had_usable_cache" -eq 1 ]; then log "Failed to refresh subscription for section '$section', continuing with cached data" "warn" else - log "Failed to download subscription for section '$section'. Aborted." "fatal" - exit 1 + log "Failed to download subscription for section '$section'; using a temporary blocked outbound" "warn" fi fi fi - # Parse subscription outbounds - if ! sing_box_cf_add_subscription_outbounds "$config" "$section" "$subscription_json_path" > /dev/null; then - log "No proxy outbounds found in subscription for section '$section'. Aborted." "fatal" - exit 1 + subscription_ready=0 + if subscription_cache_is_usable "$subscription_json_path"; then + if sing_box_cf_add_subscription_outbounds "$config" "$section" "$subscription_json_path" > /dev/null; then + if [ -n "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then + config="$SING_BOX_CF_LAST_CONFIG" + subscription_ready=1 + fi + fi fi - config="$SING_BOX_CF_LAST_CONFIG" - if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then - log "No proxy outbounds found in subscription for section '$section'. Aborted." "fatal" - exit 1 - fi + if [ "$subscription_ready" -eq 0 ]; then + log "Subscription cache for section '$section' is unavailable or empty; using a temporary blocked outbound" "warn" + mark_subscription_outbound_unavailable "$section" + else + selector_tag="$(get_outbound_tag_by_section "$section")" + subscription_outbound_tags_json="$SUBSCRIPTION_OUTBOUND_TAGS_JSON" + if [ -z "$subscription_outbound_tags_json" ] || [ "$subscription_outbound_tags_json" = "[]" ]; then + subscription_outbound_tags_json="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" + fi - subscription_outbound_tags_json="$SUBSCRIPTION_OUTBOUND_TAGS_JSON" - if [ -z "$subscription_outbound_tags_json" ] || [ "$subscription_outbound_tags_json" = "[]" ]; then - # Fallback for backward compatibility with older facade versions. - subscription_outbound_tags_json="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" - fi + if [ "$subscription_group_by_countries" -eq 1 ]; then + local grouping_json country_flag country_group_outbounds country_group_tag \ + selector_outbounds_json selector_default ungrouped_outbounds_json grouped_count ungrouped_count - selector_tag="$(get_outbound_tag_by_section "$section")" + grouping_json="$(sing_box_build_subscription_country_groups "$subscription_outbound_tags_json")" + if [ -z "$grouping_json" ]; then + log "Failed to build grouped subscription outbounds for section '$section'. Aborted." "fatal" + exit 1 + fi - if [ "$subscription_group_by_countries" -eq 1 ]; then - local grouping_json country_flag country_group_outbounds country_group_tag \ - selector_outbounds_json selector_default ungrouped_outbounds_json grouped_count ungrouped_count + grouped_count="$(echo "$grouping_json" | jq -r '.country_order | length' 2>/dev/null)" + ungrouped_count="$(echo "$grouping_json" | jq -r '.ungrouped | length' 2>/dev/null)" + log "Country grouping prepared for section '$section': groups=$grouped_count, ungrouped=$ungrouped_count" "debug" - grouping_json="$(sing_box_build_subscription_country_groups "$subscription_outbound_tags_json")" - if [ -z "$grouping_json" ]; then - log "Failed to build grouped subscription outbounds for section '$section'. Aborted." "fatal" - exit 1 - fi + selector_outbounds_json="[]" - grouped_count="$(echo "$grouping_json" | jq -r '.country_order | length' 2>/dev/null)" - ungrouped_count="$(echo "$grouping_json" | jq -r '.ungrouped | length' 2>/dev/null)" - log "Country grouping prepared for section '$section': groups=$grouped_count, ungrouped=$ungrouped_count" "debug" + for country_flag in $(echo "$grouping_json" | jq -r '.country_order[]' 2>/dev/null); do + country_group_outbounds="$(echo "$grouping_json" | jq -c --arg country_flag "$country_flag" '.country_groups[$country_flag] // []' 2>/dev/null)" + if [ -z "$country_group_outbounds" ] || [ "$country_group_outbounds" = "[]" ]; then + continue + fi - selector_outbounds_json="[]" + country_group_tag="$(sing_box_get_unique_outbound_tag "$config" "$country_flag Fastest")" + config="$(sing_box_cm_add_urltest_outbound "$config" "$country_group_tag" "$country_group_outbounds" \ + "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" - for country_flag in $(echo "$grouping_json" | jq -r '.country_order[]' 2>/dev/null); do - country_group_outbounds="$(echo "$grouping_json" | jq -c --arg country_flag "$country_flag" '.country_groups[$country_flag] // []' 2>/dev/null)" - if [ -z "$country_group_outbounds" ] || [ "$country_group_outbounds" = "[]" ]; then - continue + selector_outbounds_json=$( + printf '%s' "$selector_outbounds_json" | jq -ac --arg tag "$country_group_tag" '. + [$tag]' 2>/dev/null + ) + done + + if [ -z "$selector_outbounds_json" ]; then + selector_outbounds_json="[]" fi - country_group_tag="$(sing_box_get_unique_outbound_tag "$config" "$country_flag Fastest")" - config="$(sing_box_cm_add_urltest_outbound "$config" "$country_group_tag" "$country_group_outbounds" \ - "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" + ungrouped_outbounds_json="$(echo "$grouping_json" | jq -c '.ungrouped // []' 2>/dev/null)" + if [ -n "$ungrouped_outbounds_json" ] && [ "$ungrouped_outbounds_json" != "[]" ]; then + selector_outbounds_json=$( + jq -acn --argjson selector "$selector_outbounds_json" --argjson ungrouped "$ungrouped_outbounds_json" \ + '$selector + $ungrouped' 2>/dev/null + ) + fi - selector_outbounds_json=$( - printf '%s' "$selector_outbounds_json" | jq -ac --arg tag "$country_group_tag" '. + [$tag]' 2>/dev/null - ) - done + if [ -z "$selector_outbounds_json" ] || [ "$selector_outbounds_json" = "[]" ]; then + log "No selector outbounds available after grouping subscription outbounds for section '$section'. Aborted." "fatal" + exit 1 + fi - if [ -z "$selector_outbounds_json" ]; then - selector_outbounds_json="[]" - fi + selector_default="$(echo "$selector_outbounds_json" | jq -r '.[0] // ""' 2>/dev/null)" + if [ -z "$selector_default" ] || [ "$selector_default" = "null" ]; then + log "Unable to determine default selector outbound for section '$section'. Aborted." "fatal" + exit 1 + fi - ungrouped_outbounds_json="$(echo "$grouping_json" | jq -c '.ungrouped // []' 2>/dev/null)" - if [ -n "$ungrouped_outbounds_json" ] && [ "$ungrouped_outbounds_json" != "[]" ]; then - selector_outbounds_json=$( - jq -acn --argjson selector "$selector_outbounds_json" --argjson ungrouped "$ungrouped_outbounds_json" \ - '$selector + $ungrouped' 2>/dev/null + selector_outbounds="$selector_outbounds_json" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$selector_default" "true")" + else + # Create urltest + selector (default subscription behaviour) + urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" + urltest_outbounds="$subscription_outbound_tags_json" + selector_outbounds=$( + jq -acn --argjson outbounds "$subscription_outbound_tags_json" --arg tag "$urltest_tag" \ + '$outbounds + [$tag]' 2>/dev/null ) + if [ -z "$selector_outbounds" ]; then + log "Failed to build selector outbounds for subscription section '$section'. Aborted." "fatal" + exit 1 + fi + config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ + "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" fi - - if [ -z "$selector_outbounds_json" ] || [ "$selector_outbounds_json" = "[]" ]; then - log "No selector outbounds available after grouping subscription outbounds for section '$section'. Aborted." "fatal" - exit 1 - fi - - selector_default="$(echo "$selector_outbounds_json" | jq -r '.[0] // ""' 2>/dev/null)" - if [ -z "$selector_default" ] || [ "$selector_default" = "null" ]; then - log "Unable to determine default selector outbound for section '$section'. Aborted." "fatal" - exit 1 - fi - - selector_outbounds="$selector_outbounds_json" - config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$selector_default" "true")" - else - # Create urltest + selector (default subscription behaviour) - urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" - urltest_outbounds="$subscription_outbound_tags_json" - selector_outbounds=$( - jq -acn --argjson outbounds "$subscription_outbound_tags_json" --arg tag "$urltest_tag" \ - '$outbounds + [$tag]' 2>/dev/null - ) - if [ -z "$selector_outbounds" ]; then - log "Failed to build selector outbounds for subscription section '$section'. Aborted." "fatal" - exit 1 - fi - config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ - "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" - config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" fi ;; *) @@ -1517,8 +1666,17 @@ sing_box_configure_route() { local first_outbound_section first_outbound_section="$(get_first_outbound_section)" - first_outbound_tag="$(get_outbound_tag_by_section "$first_outbound_section")" - config=$(sing_box_cf_proxy_domain "$config" "$SB_TPROXY_INBOUND_TAG" "$CHECK_PROXY_IP_DOMAIN" "$first_outbound_tag") + if subscription_outbound_is_unavailable "$first_outbound_section"; then + log "First configured outbound section '$first_outbound_section' is unavailable; proxy test traffic will be rejected until its subscription refresh succeeds" "warn" + first_outbound_tag="" + else + first_outbound_tag="$(get_outbound_tag_by_section "$first_outbound_section")" + fi + if [ -n "$first_outbound_tag" ]; then + config=$(sing_box_cf_proxy_domain "$config" "$SB_TPROXY_INBOUND_TAG" "$CHECK_PROXY_IP_DOMAIN" "$first_outbound_tag") + else + config=$(sing_box_cf_add_single_key_reject_rule "$config" "$SB_TPROXY_INBOUND_TAG" "domain" "$CHECK_PROXY_IP_DOMAIN") + fi config=$(sing_box_cf_override_domain_port "$config" "$FAKEIP_TEST_DOMAIN" 8443) configure_common_reject_route_rule @@ -1544,10 +1702,14 @@ include_source_ips_in_routing_handler() { config_get fully_routed_ips "$section" "fully_routed_ips" if [ -n "$fully_routed_ips" ]; then rule_tag="$(gen_id)" - config=$( - sing_box_cm_add_route_rule \ - "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$(get_outbound_tag_by_section "$section")" - ) + if subscription_outbound_is_unavailable "$section"; then + config="$(sing_box_cm_add_reject_route_rule "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG")" + else + config=$( + sing_box_cm_add_route_rule \ + "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$(get_outbound_tag_by_section "$section")" + ) + fi config_list_foreach "$section" "fully_routed_ips" include_source_ip_in_routing_handler "$rule_tag" fi } @@ -1630,8 +1792,12 @@ configure_routing_for_section_lists() { case "$section_connection_type" in proxy | vpn) route_rule_tag="$(gen_id)" - outbound_tag=$(get_outbound_tag_by_section "$section") - config=$(sing_box_cm_add_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag") + if subscription_outbound_is_unavailable "$section"; then + config="$(sing_box_cm_add_reject_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG")" + else + outbound_tag=$(get_outbound_tag_by_section "$section") + config=$(sing_box_cm_add_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag") + fi ;; block) route_rule_tag="$SB_REJECT_RULE_TAG" @@ -1896,15 +2062,21 @@ sing_box_additional_inbounds() { if [ "$download_lists_via_proxy" -eq 1 ]; then local download_lists_via_proxy_section section_outbound_tag config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section" - section_outbound_tag="$(get_outbound_tag_by_section "$download_lists_via_proxy_section")" - config=$( - sing_box_cf_add_mixed_inbound_and_route_rule \ - "$config" \ - "$SB_SERVICE_MIXED_INBOUND_TAG" \ - "$SB_SERVICE_MIXED_INBOUND_ADDRESS" \ - "$SB_SERVICE_MIXED_INBOUND_PORT" \ - "$section_outbound_tag" - ) + if subscription_outbound_is_unavailable "$download_lists_via_proxy_section"; then + log "Service download proxy section '$download_lists_via_proxy_section' is unavailable; rejecting proxy requests until its subscription refresh succeeds" "warn" + config="$(sing_box_cm_add_mixed_inbound "$config" "$SB_SERVICE_MIXED_INBOUND_TAG" "$SB_SERVICE_MIXED_INBOUND_ADDRESS" "$SB_SERVICE_MIXED_INBOUND_PORT")" + config="$(sing_box_cm_add_reject_route_rule "$config" "$(gen_id)" "$SB_SERVICE_MIXED_INBOUND_TAG")" + else + section_outbound_tag="$(get_outbound_tag_by_section "$download_lists_via_proxy_section")" + config=$( + sing_box_cf_add_mixed_inbound_and_route_rule \ + "$config" \ + "$SB_SERVICE_MIXED_INBOUND_TAG" \ + "$SB_SERVICE_MIXED_INBOUND_ADDRESS" \ + "$SB_SERVICE_MIXED_INBOUND_PORT" \ + "$section_outbound_tag" + ) + fi fi config_foreach configure_section_mixed_proxy "section" @@ -1935,15 +2107,20 @@ configure_section_mixed_proxy() { fi if [ "$mixed_inbound_enabled" -eq 1 ]; then mixed_inbound_tag="$(get_inbound_tag_by_section "$section-mixed")" - mixed_outbound_tag="$(get_outbound_tag_by_section "$section")" - config=$( - sing_box_cf_add_mixed_inbound_and_route_rule \ - "$config" \ - "$mixed_inbound_tag" \ - "$mixed_proxy_address" \ - "$mixed_proxy_port" \ - "$mixed_outbound_tag" - ) + if subscription_outbound_is_unavailable "$section"; then + config="$(sing_box_cm_add_mixed_inbound "$config" "$mixed_inbound_tag" "$mixed_proxy_address" "$mixed_proxy_port")" + config="$(sing_box_cm_add_reject_route_rule "$config" "$(gen_id)" "$mixed_inbound_tag")" + else + mixed_outbound_tag="$(get_outbound_tag_by_section "$section")" + config=$( + sing_box_cf_add_mixed_inbound_and_route_rule \ + "$config" \ + "$mixed_inbound_tag" \ + "$mixed_proxy_address" \ + "$mixed_proxy_port" \ + "$mixed_outbound_tag" + ) + fi fi } @@ -2228,6 +2405,11 @@ get_download_detour_tag() { if [ "$download_lists_via_proxy" -eq 1 ]; then local download_lists_via_proxy_section section_outbound_tag config_get download_lists_via_proxy_section "settings" "download_lists_via_proxy_section" + if subscription_outbound_is_unavailable "$download_lists_via_proxy_section"; then + log "Download detour section '$download_lists_via_proxy_section' is unavailable; using direct detour until subscription refresh succeeds" "warn" + echo "" + return 0 + fi section_outbound_tag="$(get_outbound_tag_by_section "$download_lists_via_proxy_section")" echo "$section_outbound_tag" else diff --git a/podkop/files/usr/lib/constants.sh b/podkop/files/usr/lib/constants.sh index 68bc3b51..db40974e 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/podkop/files/usr/lib/constants.sh @@ -3,6 +3,7 @@ PODKOP_VERSION="__COMPILED_VERSION_VARIABLE__" ## Common PODKOP_CONFIG="/etc/config/podkop" +PODKOP_STATE_DIR="/etc/podkop" RESOLV_CONF="/etc/resolv.conf" DNS_RESOLVERS="1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 9.9.9.9 9.9.9.11 94.140.14.14 94.140.15.15 208.67.220.220 208.67.222.222 77.88.8.1 77.88.8.8" CHECK_PROXY_IP_DOMAIN="ip.podkop.fyi" @@ -10,6 +11,8 @@ FAKEIP_TEST_DOMAIN="fakeip.podkop.fyi" TMP_SING_BOX_FOLDER="/tmp/sing-box" TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets" TMP_SUBSCRIPTION_FOLDER="$TMP_SING_BOX_FOLDER/subscriptions" +SUBSCRIPTION_CACHE_FOLDER="$PODKOP_STATE_DIR/subscriptions" +TMP_SUBSCRIPTION_DOWNLOAD_FOLDER="$TMP_SING_BOX_FOLDER/subscription-downloads" CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker JQ_REQUIRED_VERSION="1.7.1" COREUTILS_BASE64_REQUIRED_VERSION="9.7" @@ -64,4 +67,4 @@ SUBNETS_HETZNER="${GITHUB_RAW_URL}/Subnets/IPv4/hetzner.lst" SUBNETS_OVH="${GITHUB_RAW_URL}/Subnets/IPv4/ovh.lst" SUBNETS_DIGITALOCEAN="${GITHUB_RAW_URL}/Subnets/IPv4/digitalocean.lst" SUBNETS_CLOUDFRONT="${GITHUB_RAW_URL}/Subnets/IPv4/cloudfront.lst" -COMMUNITY_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube hdrezka tiktok google_ai google_play hodca discord meta twitter cloudflare cloudfront digitalocean hetzner ovh telegram roblox" \ No newline at end of file +COMMUNITY_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube hdrezka tiktok google_ai google_play hodca discord meta twitter cloudflare cloudfront digitalocean hetzner ovh telegram roblox" diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index c16b9382..77eb9195 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -254,23 +254,203 @@ migration_rename_config_key() { } # Download URL to file +redact_url_for_log() { + local url="$1" + local sanitized + + sanitized="${url%%#*}" + sanitized="${sanitized%%\?*}" + + case "$sanitized" in + *://*@*) + sanitized="${sanitized%%://*}://***@${sanitized#*@}" + ;; + esac + + case "$url" in + *\?*) sanitized="$sanitized?" ;; + esac + + printf '%s\n' "$sanitized" +} + +url_host_for_log() { + local url="$1" + local host + + host="${url#*://}" + host="${host%%/*}" + host="${host%%\?*}" + host="${host%%#*}" + host="${host##*@}" + + case "$host" in + \[*\]*) + host="${host#\[}" + host="${host%%\]*}" + ;; + *) + host="${host%%:*}" + ;; + esac + + printf '%s\n' "$host" +} + +url_is_ipv6_literal() { + case "$1" in + *://\[*\]*) return 0 ;; + esac + return 1 +} + +wget_supports_ipv4_flag() { + wget --help 2>&1 | grep -Eq -- 'Use IPv4 only|(^|[[:space:]])-4([[:space:],]|$)' +} + +has_ipv4_default_route() { + ip -4 route show default 2>/dev/null | grep -q '^default' +} + +has_ipv6_default_route() { + ip -6 route show default 2>/dev/null | grep -q '^default' +} + +has_global_ipv6_addr() { + ip -6 addr show scope global 2>/dev/null | grep -q 'inet6 ' +} + +ipv6_route_usable() { + ip -6 route get 2606:4700:4700::1111 >/dev/null 2>&1 +} + +ipv6_appears_usable() { + has_ipv6_default_route && has_global_ipv6_addr && ipv6_route_usable +} + +get_wget_ipv4_mode() { + local mode + config_get mode "settings" "wget_ipv4_mode" "auto" 2>/dev/null + case "$mode" in + off | force | auto) echo "$mode" ;; + *) echo "auto" ;; + esac +} + +should_force_wget_ipv4() { + local url="$1" + local mode + + url_is_ipv6_literal "$url" && return 1 + wget_supports_ipv4_flag || return 1 + + mode="$(get_wget_ipv4_mode)" + case "$mode" in + off) + return 1 + ;; + force) + has_ipv4_default_route + return $? + ;; + auto | *) + has_ipv4_default_route || return 1 + ipv6_appears_usable && return 1 + return 0 + ;; + esac +} + +format_wget_error() { + local errfile="$1" + local message + + message="$(tr '\n' ' ' < "$errfile" 2>/dev/null | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//' | cut -c1-220)" + [ -n "$message" ] || message="no stderr from wget" + printf '%s\n' "$message" +} + +log_wget_failure() { + local operation="$1" + local url="$2" + local errfile="$3" + local rc="$4" + local attempt="$5" + local retries="$6" + local timeout="$7" + local http_proxy_address="$8" + local family="$9" + local mode err host + + if [ -n "$http_proxy_address" ]; then + mode="proxy $http_proxy_address" + else + mode="direct" + fi + + err="$(format_wget_error "$errfile")" + host="$(url_host_for_log "$url")" + + log "$operation failed [$attempt/$retries]: wget rc=$rc, mode=$mode, family=$family, timeout=${timeout}s, host=${host:-unknown}, url=$(redact_url_for_log "$url"), error=\"$err\"" "warn" + if echo "$err" | grep -qi 'Operation not permitted'; then + log "$operation got 'Operation not permitted'. On OpenWrt this often means broken IPv6/default route/firewall while WAN is IPv4-only; podkop will prefer IPv4 when wget supports -4." "warn" + fi +} + download_to_file() { local url="$1" local filepath="$2" local http_proxy_address="$3" local retries="${4:-3}" local wait="${5:-2}" + local timeout="${6:-10}" + local attempt errfile rc family for attempt in $(seq 1 "$retries"); do - if [ -n "$http_proxy_address" ]; then - http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -O "$filepath" "$url" && break + errfile="${filepath}.wget.err.$$" + family="any" + if should_force_wget_ipv4 "$url"; then + family="ipv4" + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -4 -T "$timeout" -O "$filepath" "$url" 2>"$errfile" + else + wget -4 -T "$timeout" -O "$filepath" "$url" 2>"$errfile" + fi + elif [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -T "$timeout" -O "$filepath" "$url" 2>"$errfile" else - wget -O "$filepath" "$url" && break + wget -T "$timeout" -O "$filepath" "$url" 2>"$errfile" + fi + rc=$? + if [ "$rc" -eq 0 ]; then + rm -f "$errfile" + return 0 fi - log "Attempt $attempt/$retries to download $url failed" "warn" - sleep "$wait" + log_wget_failure "Download" "$url" "$errfile" "$rc" "$attempt" "$retries" "$timeout" "$http_proxy_address" "$family" + rm -f "$errfile" + + if [ "$family" != "ipv4" ] && has_ipv4_default_route && wget_supports_ipv4_flag; then + errfile="${filepath}.wget.err.$$" + log "Retrying download over IPv4-only after generic wget failure" "warn" + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -4 -T "$timeout" -O "$filepath" "$url" 2>"$errfile" + else + wget -4 -T "$timeout" -O "$filepath" "$url" 2>"$errfile" + fi + rc=$? + if [ "$rc" -eq 0 ]; then + rm -f "$errfile" + return 0 + fi + log_wget_failure "Download IPv4 retry" "$url" "$errfile" "$rc" "$attempt" "$retries" "$timeout" "$http_proxy_address" "ipv4" + rm -f "$errfile" + fi + + [ "$attempt" -lt "$retries" ] && sleep "$wait" done + + return 1 } # Converts Windows-style line endings (CRLF) to Unix-style (LF) @@ -425,14 +605,28 @@ download_subscription() { kernel_version="$(get_kernel_version)" hwid="$(generate_hwid)" - local tmpfile + local tmpfile errfile rc family tmpfile="${filepath}.part.$$" - rm -f "$tmpfile" + errfile="${filepath}.err.$$" + rm -f "$tmpfile" "$errfile" for attempt in $(seq 1 "$retries"); do - if [ -n "$http_proxy_address" ]; then - http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ - wget -T "$timeout" -O "$tmpfile" \ + family="any" + if should_force_wget_ipv4 "$url"; then + family="ipv4" + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ + wget -4 -T "$timeout" -O "$tmpfile" \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + else + wget -4 -T "$timeout" -O "$tmpfile" \ --header "User-Agent: singbox/$sb_version" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ @@ -440,30 +634,82 @@ download_subscription() { --header "X-Ver-OS: $kernel_version" \ --header "Accept-Language: ru-RU,en,*" \ --header "X-Device-Locale: EN" \ - "$url" + "$url" 2>"$errfile" + fi else - wget -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: singbox/$sb_version" \ - --header "X-HWID: $hwid" \ - --header "X-Device-OS: OpenWrt Linux" \ - --header "X-Device-Model: $device_model" \ - --header "X-Ver-OS: $kernel_version" \ - --header "Accept-Language: ru-RU,en,*" \ - --header "X-Device-Locale: EN" \ - "$url" + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ + wget -T "$timeout" -O "$tmpfile" \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + else + wget -T "$timeout" -O "$tmpfile" \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + fi fi - if [ $? -eq 0 ] && [ -s "$tmpfile" ]; then + rc=$? + if [ "$rc" -eq 0 ] && [ -s "$tmpfile" ]; then mv "$tmpfile" "$filepath" + rm -f "$errfile" return 0 fi rm -f "$tmpfile" - log "Attempt $attempt/$retries to download subscription from $url failed" "warn" + log_wget_failure "Subscription download" "$url" "$errfile" "$rc" "$attempt" "$retries" "$timeout" "$http_proxy_address" "$family" + + if [ "$family" != "ipv4" ] && has_ipv4_default_route && wget_supports_ipv4_flag; then + family="ipv4" + log "Retrying subscription download over IPv4-only" "warn" + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ + wget -4 -T "$timeout" -O "$tmpfile" \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + else + wget -4 -T "$timeout" -O "$tmpfile" \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + fi + rc=$? + if [ "$rc" -eq 0 ] && [ -s "$tmpfile" ]; then + mv "$tmpfile" "$filepath" + rm -f "$errfile" + return 0 + fi + log_wget_failure "Subscription download IPv4 retry" "$url" "$errfile" "$rc" "$attempt" "$retries" "$timeout" "$http_proxy_address" "$family" + fi + sleep "$wait" done rm -f "$tmpfile" + rm -f "$errfile" return 1 } @@ -480,11 +726,26 @@ check_subscription_connectivity() { kernel_version="$(get_kernel_version)" hwid="$(generate_hwid)" - local attempt + local attempt errfile rc family + errfile="/tmp/podkop-subscription-check.$$" + rm -f "$errfile" for attempt in $(seq 1 "$retries"); do - if [ -n "$http_proxy_address" ]; then - http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ - wget -q -T "$timeout" -O /dev/null \ + family="any" + if should_force_wget_ipv4 "$url"; then + family="ipv4" + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ + wget -q -4 -T "$timeout" -O /dev/null \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + else + wget -q -4 -T "$timeout" -O /dev/null \ --header "User-Agent: singbox/$sb_version" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ @@ -492,22 +753,77 @@ check_subscription_connectivity() { --header "X-Ver-OS: $kernel_version" \ --header "Accept-Language: ru-RU,en,*" \ --header "X-Device-Locale: EN" \ - "$url" && return 0 + "$url" 2>"$errfile" + fi else - wget -q -T "$timeout" -O /dev/null \ - --header "User-Agent: singbox/$sb_version" \ - --header "X-HWID: $hwid" \ - --header "X-Device-OS: OpenWrt Linux" \ - --header "X-Device-Model: $device_model" \ - --header "X-Ver-OS: $kernel_version" \ - --header "Accept-Language: ru-RU,en,*" \ - --header "X-Device-Locale: EN" \ - "$url" && return 0 + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ + wget -q -T "$timeout" -O /dev/null \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + else + wget -q -T "$timeout" -O /dev/null \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + fi + fi + + rc=$? + if [ "$rc" -eq 0 ]; then + rm -f "$errfile" + return 0 + fi + + log_wget_failure "Subscription connectivity" "$url" "$errfile" "$rc" "$attempt" "$retries" "$timeout" "$http_proxy_address" "$family" + + if [ "$family" != "ipv4" ] && has_ipv4_default_route && wget_supports_ipv4_flag; then + family="ipv4" + if [ -n "$http_proxy_address" ]; then + http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ + wget -q -4 -T "$timeout" -O /dev/null \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + else + wget -q -4 -T "$timeout" -O /dev/null \ + --header "User-Agent: singbox/$sb_version" \ + --header "X-HWID: $hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $device_model" \ + --header "X-Ver-OS: $kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$url" 2>"$errfile" + fi + rc=$? + if [ "$rc" -eq 0 ]; then + rm -f "$errfile" + return 0 + fi + log_wget_failure "Subscription connectivity IPv4 retry" "$url" "$errfile" "$rc" "$attempt" "$retries" "$timeout" "$http_proxy_address" "$family" fi [ "$attempt" -lt "$retries" ] && sleep "$wait" done + rm -f "$errfile" return 1 } @@ -519,6 +835,12 @@ validate_subscription_file() { jq -e ' type == "object" and (.outbounds | type == "array") and - ((.outbounds | length) > 0) + ([.outbounds[] | select( + .type != "selector" and + .type != "urltest" and + .type != "direct" and + .type != "dns" and + .type != "block" + )] | length > 0) ' "$filepath" > /dev/null 2>&1 } From 2de71647f015713456a754dcc1e0dbe7227b5b6c Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 26 May 2026 23:56:56 +0300 Subject: [PATCH 17/75] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D1=83=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BE?= =?UTF-8?q?=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + podkop/files/usr/bin/podkop | 108 ++++++++++++++++++------- podkop/files/usr/lib/helpers.sh | 137 ++++++++++++++++++++++++++++---- 3 files changed, 203 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 21b50a0f..4a470951 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ fe-app-podkop/node_modules fe-app-podkop/.env .DS_Store +*.txt diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index f0d75a2c..05dc894b 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -323,21 +323,35 @@ download_subscription_into_cache() { local subscription_json_path="$3" local subscription_url_cache_path="$4" local service_proxy_address="$5" - local tmpfile persist_tmpfile url_tmpfile rejected_cache_path tmp_hash rejected_hash + local tmpfile persist_tmpfile url_tmpfile rejected_cache_path tmp_hash rejected_hash validation_reason file_size - ensure_subscription_cache_dir || return 1 - mkdir -p "$TMP_SUBSCRIPTION_DOWNLOAD_FOLDER" || return 1 - tmpfile="$(mktemp "$TMP_SUBSCRIPTION_DOWNLOAD_FOLDER/${section}.download.XXXXXX")" || return 1 + ensure_subscription_cache_dir || { + log "Failed to prepare persistent subscription cache directory '$SUBSCRIPTION_CACHE_FOLDER' for section '$section'" "error" + return 10 + } + mkdir -p "$TMP_SUBSCRIPTION_DOWNLOAD_FOLDER" || { + log "Failed to create temporary subscription download directory '$TMP_SUBSCRIPTION_DOWNLOAD_FOLDER' for section '$section'" "error" + return 11 + } + tmpfile="$(mktemp "$TMP_SUBSCRIPTION_DOWNLOAD_FOLDER/${section}.download.XXXXXX")" || { + log "Failed to create temporary subscription download file in '$TMP_SUBSCRIPTION_DOWNLOAD_FOLDER' for section '$section'" "error" + return 11 + } if ! download_subscription "$subscription_url" "$tmpfile" "$service_proxy_address" 3 2 10; then + log "Subscription body download failed for section '$section' after retries" "error" rm -f "$tmpfile" - return 1 + return 12 fi + file_size="$(wc -c < "$tmpfile" 2>/dev/null | tr -d ' ')" + log "Downloaded subscription body for section '$section': bytes=${file_size:-unknown}" "debug" + if ! validate_subscription_file "$tmpfile"; then - log "Downloaded subscription for section '$section' is invalid" "error" + validation_reason="$(describe_subscription_validation_failure "$tmpfile")" + log "Downloaded subscription for section '$section' is invalid: ${validation_reason:-unknown validation error}" "error" rm -f "$tmpfile" - return 1 + return 13 fi rejected_cache_path="$(get_subscription_rejected_cache_path "$section")" @@ -346,15 +360,19 @@ download_subscription_into_cache() { if [ -n "$tmp_hash" ] && [ "$tmp_hash" = "$rejected_hash" ]; then log "Downloaded subscription for section '$section' is unchanged and was previously rejected because it contains no usable sing-box outbounds" "warn" rm -f "$tmpfile" - return 1 + return 14 fi if [ -f "$subscription_json_path" ] && cmp -s "$tmpfile" "$subscription_json_path"; then rm -f "$tmpfile" if [ "$(cat "$subscription_url_cache_path" 2>/dev/null)" != "$subscription_url" ]; then url_tmpfile="${subscription_url_cache_path}.tmp.$$" - printf '%s' "$subscription_url" > "$url_tmpfile" && mv "$url_tmpfile" "$subscription_url_cache_path" - chmod 600 "$subscription_url_cache_path" 2>/dev/null + if printf '%s' "$subscription_url" > "$url_tmpfile" && mv "$url_tmpfile" "$subscription_url_cache_path"; then + chmod 600 "$subscription_url_cache_path" 2>/dev/null + else + log "Subscription content for section '$section' is unchanged, but URL metadata could not be persisted; the next refresh may download it again" "warn" + rm -f "$url_tmpfile" + fi fi log "Subscription for section '$section' is unchanged" "info" return 2 @@ -362,14 +380,21 @@ download_subscription_into_cache() { persist_tmpfile="${subscription_json_path}.tmp.$$" cp "$tmpfile" "$persist_tmpfile" && mv "$persist_tmpfile" "$subscription_json_path" || { + log "Failed to persist subscription cache for section '$section' to '$subscription_json_path'" "error" rm -f "$tmpfile" "$persist_tmpfile" - return 1 + return 15 } rm -f "$tmpfile" rm -f "$rejected_cache_path" url_tmpfile="${subscription_url_cache_path}.tmp.$$" - printf '%s' "$subscription_url" > "$url_tmpfile" && mv "$url_tmpfile" "$subscription_url_cache_path" + printf '%s' "$subscription_url" > "$url_tmpfile" && mv "$url_tmpfile" "$subscription_url_cache_path" || { + # The valid JSON cache has already been atomically installed. Treat + # metadata failure as non-fatal so recovery reloads and applies it; + # only refresh deduplication is degraded until metadata can be written. + log "Subscription cache for section '$section' was updated, but URL metadata could not be persisted; applying the cache and retrying metadata on a later refresh" "warn" + rm -f "$url_tmpfile" + } chmod 600 "$subscription_json_path" "$subscription_url_cache_path" 2>/dev/null return 0 } @@ -488,13 +513,15 @@ start_subscription_startup_retry_worker() { # Run in a child process: a successful subscription_update performs # its own restart, which stops this worker via the pidfile safely. - if /usr/bin/podkop subscription_update >/dev/null 2>&1; then + # Keep diagnostics visible in syslog; subscription URLs are redacted + # in the lower-level download helpers. + if /usr/bin/podkop subscription_update; then log "Deferred subscription refresh succeeded; updated configuration is being applied" "info" rm -f "$pidfile" exit 0 fi - log "Deferred subscription refresh is still waiting for connectivity" "warn" + log "Deferred subscription refresh has not completed yet; retrying in background" "warn" sleep 30 done ) & @@ -1098,10 +1125,15 @@ subscription_update() { echolog "рџ“Ґ Updating subscription for section '$section'..." - service_proxy_address="$(get_subscription_download_proxy_address "$section" "runtime" 2>/dev/null || echo '')" + service_proxy_address="$(get_subscription_download_proxy_address "$section" "runtime" || echo '')" + if [ -n "$service_proxy_address" ]; then + log "Updating subscription for section '$section' via service proxy $service_proxy_address" "info" + else + log "Updating subscription for section '$section' directly" "info" + fi if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 6 5 5; then - echolog "вќЊ Failed to download subscription for section '$section'" + echolog "вќЊ Subscription source is not reachable for section '$section'" failed_sections=$((failed_sections + 1)) return fi @@ -1113,27 +1145,47 @@ subscription_update() { case "$update_result" in 0) updated_sections=$((updated_sections + 1)) + outbounds_count=$(jq -r '[.outbounds[] | select( + .type != "selector" and + .type != "urltest" and + .type != "direct" and + .type != "dns" and + .type != "block" + )] | length' "$subscription_json_path" 2>/dev/null) + + echolog "вњ… Subscription updated for section '$section': $outbounds_count outbounds" ;; 2) echolog "в„№пёЏ Subscription for section '$section' is unchanged" return ;; + 10) + echolog "вќЊ Failed to prepare subscription cache for section '$section'" + ;; + 11) + echolog "вќЊ Failed to create temporary subscription download file for section '$section'" + ;; + 12) + echolog "вќЊ Failed to download subscription body for section '$section'" + ;; + 13) + echolog "вќЊ Downloaded subscription for section '$section' is invalid" + ;; + 14) + echolog "вќЊ Downloaded subscription for section '$section' is unchanged and still has no usable outbounds" + ;; + 15) + echolog "вќЊ Failed to persist subscription cache for section '$section'" + ;; *) - echolog "вќЊ Failed to download subscription for section '$section'" - failed_sections=$((failed_sections + 1)) - return + echolog "вќЊ Failed to update subscription for section '$section': internal error rc=$update_result" ;; esac - outbounds_count=$(jq -r '[.outbounds[] | select( - .type != "selector" and - .type != "urltest" and - .type != "direct" and - .type != "dns" and - .type != "block" - )] | length' "$subscription_json_path" 2>/dev/null) - - echolog "вњ… Subscription updated for section '$section': $outbounds_count outbounds" + if [ "$update_result" -ne 0 ]; then + failed_sections=$((failed_sections + 1)) + return + fi } config_foreach _update_subscription_for_section "section" diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 77eb9195..3ce3d3e5 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -256,22 +256,57 @@ migration_rename_config_key() { # Download URL to file redact_url_for_log() { local url="$1" - local sanitized + local scheme rest authority suffix userinfo_flag path_flag query_flag fragment_flag - sanitized="${url%%#*}" - sanitized="${sanitized%%\?*}" + scheme="" + rest="$url" + userinfo_flag=0 + path_flag=0 + query_flag=0 + fragment_flag=0 - case "$sanitized" in - *://*@*) - sanitized="${sanitized%%://*}://***@${sanitized#*@}" - ;; + case "$url" in + *'#'*) fragment_flag=1 ;; esac + rest="${rest%%#*}" case "$url" in - *\?*) sanitized="$sanitized?" ;; + *\?*) query_flag=1 ;; + esac + rest="${rest%%\?*}" + + case "$rest" in + *://*) + scheme="${rest%%://*}://" + rest="${rest#*://}" + ;; esac - printf '%s\n' "$sanitized" + authority="${rest%%/*}" + if [ "$authority" != "$rest" ]; then + path_flag=1 + fi + + case "$authority" in + *@*) + userinfo_flag=1 + authority="${authority##*@}" + ;; + esac + + suffix="" + [ "$path_flag" -eq 1 ] && suffix="$suffix/" + [ "$query_flag" -eq 1 ] && suffix="$suffix?" + [ "$fragment_flag" -eq 1 ] && suffix="$suffix#" + + if [ -z "$authority" ]; then + printf 'redacted-url(has_path=%s,has_query=%s,has_userinfo=%s,has_fragment=%s)\n' \ + "$path_flag" "$query_flag" "$userinfo_flag" "$fragment_flag" + return 0 + fi + + printf '%s%s%s(has_path=%s,has_query=%s,has_userinfo=%s,has_fragment=%s)\n' \ + "$scheme" "$authority" "$suffix" "$path_flag" "$query_flag" "$userinfo_flag" "$fragment_flag" } url_host_for_log() { @@ -363,13 +398,33 @@ should_force_wget_ipv4() { format_wget_error() { local errfile="$1" + local url="$2" local message message="$(tr '\n' ' ' < "$errfile" 2>/dev/null | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//' | cut -c1-220)" + message="$(printf '%s' "$message" | sed 's#[Hh][Tt][Tt][Pp][Ss]\{0,1\}://[^[:space:]]*##g')" [ -n "$message" ] || message="no stderr from wget" printf '%s\n' "$message" } +wget_error_class() { + local err="$1" + + if echo "$err" | grep -qi 'Operation not permitted'; then + echo "operation_not_permitted" + elif echo "$err" | grep -qi 'not an http or ftp url\|bad address\|unable to resolve\|Name or service not known'; then + echo "dns_or_bad_url" + elif echo "$err" | grep -qi 'timed out\|timeout'; then + echo "timeout" + elif echo "$err" | grep -qi 'certificate\|SSL\|TLS'; then + echo "tls" + elif echo "$err" | grep -qi '404\|403\|401\|500\|502\|503\|HTTP'; then + echo "http" + else + echo "unknown" + fi +} + log_wget_failure() { local operation="$1" local url="$2" @@ -380,7 +435,7 @@ log_wget_failure() { local timeout="$7" local http_proxy_address="$8" local family="$9" - local mode err host + local mode err host err_class if [ -n "$http_proxy_address" ]; then mode="proxy $http_proxy_address" @@ -388,12 +443,13 @@ log_wget_failure() { mode="direct" fi - err="$(format_wget_error "$errfile")" + err="$(format_wget_error "$errfile" "$url")" host="$(url_host_for_log "$url")" + err_class="$(wget_error_class "$err")" - log "$operation failed [$attempt/$retries]: wget rc=$rc, mode=$mode, family=$family, timeout=${timeout}s, host=${host:-unknown}, url=$(redact_url_for_log "$url"), error=\"$err\"" "warn" + log "$operation failed [$attempt/$retries]: wget rc=$rc, mode=$mode, family=$family, timeout=${timeout}s, host=${host:-unknown}, url=$(redact_url_for_log "$url"), error_class=$err_class, error=\"$err\"" "warn" if echo "$err" | grep -qi 'Operation not permitted'; then - log "$operation got 'Operation not permitted'. On OpenWrt this often means broken IPv6/default route/firewall while WAN is IPv4-only; podkop will prefer IPv4 when wget supports -4." "warn" + log "$operation got 'Operation not permitted'. On OpenWrt this can indicate firewall, routing, or IPv6 preference issues; podkop will retry with IPv4 when supported." "warn" fi } @@ -663,11 +719,19 @@ download_subscription() { rc=$? if [ "$rc" -eq 0 ] && [ -s "$tmpfile" ]; then - mv "$tmpfile" "$filepath" + if ! mv "$tmpfile" "$filepath"; then + log "Subscription download succeeded but failed to move temporary file to destination" "error" + rm -f "$tmpfile" "$errfile" + return 1 + fi rm -f "$errfile" return 0 fi + if [ "$rc" -eq 0 ] && [ ! -s "$tmpfile" ]; then + log "Subscription download returned success but produced an empty file: host=$(url_host_for_log "$url"), url=$(redact_url_for_log "$url")" "warn" + fi + rm -f "$tmpfile" log_wget_failure "Subscription download" "$url" "$errfile" "$rc" "$attempt" "$retries" "$timeout" "$http_proxy_address" "$family" @@ -698,10 +762,17 @@ download_subscription() { fi rc=$? if [ "$rc" -eq 0 ] && [ -s "$tmpfile" ]; then - mv "$tmpfile" "$filepath" + if ! mv "$tmpfile" "$filepath"; then + log "Subscription download IPv4 retry succeeded but failed to move temporary file to destination" "error" + rm -f "$tmpfile" "$errfile" + return 1 + fi rm -f "$errfile" return 0 fi + if [ "$rc" -eq 0 ] && [ ! -s "$tmpfile" ]; then + log "Subscription download IPv4 retry returned success but produced an empty file: host=$(url_host_for_log "$url"), url=$(redact_url_for_log "$url")" "warn" + fi log_wget_failure "Subscription download IPv4 retry" "$url" "$errfile" "$rc" "$attempt" "$retries" "$timeout" "$http_proxy_address" "$family" fi @@ -710,6 +781,7 @@ download_subscription() { rm -f "$tmpfile" rm -f "$errfile" + log "Subscription download failed after $retries attempts: host=$(url_host_for_log "$url"), url=$(redact_url_for_log "$url")" "error" return 1 } @@ -844,3 +916,38 @@ validate_subscription_file() { )] | length > 0) ' "$filepath" > /dev/null 2>&1 } + +describe_subscription_validation_failure() { + local filepath="$1" + local total usable + + if [ ! -s "$filepath" ]; then + echo "downloaded file is empty" + return 0 + fi + + if ! jq -e '.' "$filepath" >/dev/null 2>&1; then + echo "downloaded file is not valid JSON" + return 0 + fi + + if ! jq -e 'type == "object"' "$filepath" >/dev/null 2>&1; then + echo "subscription root is not a JSON object" + return 0 + fi + + if ! jq -e '.outbounds | type == "array"' "$filepath" >/dev/null 2>&1; then + echo "subscription has no outbounds array" + return 0 + fi + + total="$(jq -r '.outbounds | length' "$filepath" 2>/dev/null)" + usable="$(jq -r '[.outbounds[] | select( + .type != "selector" and + .type != "urltest" and + .type != "direct" and + .type != "dns" and + .type != "block" + )] | length' "$filepath" 2>/dev/null)" + echo "subscription contains no usable proxy outbounds: total=${total:-unknown}, usable=${usable:-unknown}" +} From b6a0f8ea7113e90e7d5f6b6c86201f98d67e075b Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Wed, 27 May 2026 10:32:31 +0300 Subject: [PATCH 18/75] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D1=83=20=D0=BA=D0=B5=D1=88=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=BF=D0=B8=D1=81=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- podkop/files/usr/bin/podkop | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 05dc894b..a3c74e3b 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -167,12 +167,31 @@ get_subscription_rejected_cache_path() { ensure_subscription_cache_dir() { local state_dir_created=0 cache_dir_created=0 + local mkdir_errfile mkdir_rc [ -d "$PODKOP_STATE_DIR" ] || state_dir_created=1 [ -d "$SUBSCRIPTION_CACHE_FOLDER" ] || cache_dir_created=1 - mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" || return 1 - [ "$state_dir_created" -eq 1 ] && chmod 700 "$PODKOP_STATE_DIR" 2>/dev/null - [ "$cache_dir_created" -eq 1 ] && chmod 700 "$SUBSCRIPTION_CACHE_FOLDER" 2>/dev/null + mkdir_errfile="/tmp/podkop-subscription-cache-mkdir.$$" + if mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" 2>"$mkdir_errfile"; then + rm -f "$mkdir_errfile" + else + mkdir_rc=$? + if [ -d "$SUBSCRIPTION_CACHE_FOLDER" ]; then + log "Subscription cache directory '$SUBSCRIPTION_CACHE_FOLDER' already exists; continuing after mkdir rc=$mkdir_rc" "warn" + rm -f "$mkdir_errfile" + else + local mkdir_err + mkdir_err="$(tr '\n' ' ' < "$mkdir_errfile" 2>/dev/null | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//' | cut -c1-220)" + rm -f "$mkdir_errfile" + log "Failed to prepare subscription cache directory '$SUBSCRIPTION_CACHE_FOLDER': mkdir rc=$mkdir_rc, state_dir_created=$state_dir_created, cache_dir_created=$cache_dir_created, error=\"${mkdir_err:-no stderr}\"" "error" + return 1 + fi + fi + + if [ "$state_dir_created" -eq 1 ] || [ "$cache_dir_created" -eq 1 ]; then + chmod 700 "$PODKOP_STATE_DIR" 2>/dev/null + chmod 700 "$SUBSCRIPTION_CACHE_FOLDER" 2>/dev/null + fi } migrate_subscription_cache_from_tmp() { @@ -1119,7 +1138,11 @@ subscription_update() { fi mkdir -p "$TMP_SUBSCRIPTION_FOLDER" - ensure_subscription_cache_dir + if ! ensure_subscription_cache_dir; then + echolog "вќЊ Subscription cache directory is unavailable for section '$section'" + failed_sections=$((failed_sections + 1)) + return + fi subscription_json_path="$(get_subscription_json_path "$section")" subscription_url_cache_path="$(get_subscription_url_cache_path "$section")" From 3cf321a78bd5762b3383c50a0a09a818ed8372f1 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 16:15:30 +0300 Subject: [PATCH 19/75] feat: sing-box-extended core switching + xhttp client transport Add runtime sing-box core switching (stable <-> extended) and xhttp client outbound support, reimplemented on the fork's jq stack (no ucode). Backend: - helpers.sh: is_sing_box_extended() / get_sing_box_version() - get_system_info exposes sing_box_extended flag - updater.sh (new): download sing-box-extended from shtorm-7/sing-box-extended with arch/musl detection, jq release parsing, backup/extract/validate/rollback; install_stable reverts to packaged sing-box; component_action dispatch - sing_box_config_facade.sh: xhttp transport branch gated on extended (path/host/sni/mode), default ALPN h2,http/1.1; subscription xhttp skipped when core not extended - sing_box_config_manager.sh: sing_box_cm_set_xhttp_transport_for_outbound (jq) Frontend: - types: sing_box_extended + component action method - async componentAction via executeShellCommand (long timeout) - Diagnostics tab: Install extended / Install stable button - RU locales for new strings Default install keeps stock sing-box; extended is opt-in via UI. --- fe-app-podkop/locales/calls.json | 758 ++++++++++-------- fe-app-podkop/locales/podkop.pot | 718 +++++++++-------- fe-app-podkop/locales/podkop.ru.po | 56 +- .../src/podkop/methods/shell/index.ts | 40 + .../src/podkop/services/store.service.ts | 2 + .../tabs/diagnostic/diagnostic.store.ts | 4 + .../podkop/tabs/diagnostic/initController.ts | 56 +- .../partials/renderAvailableActions.ts | 13 + fe-app-podkop/src/podkop/types.ts | 1 + .../luci-static/resources/view/podkop/main.js | 101 ++- luci-app-podkop/po/ru/podkop.po | 56 +- luci-app-podkop/po/templates/podkop.pot | 718 +++++++++-------- podkop/files/usr/bin/podkop | 23 +- podkop/files/usr/lib/helpers.sh | 17 +- .../files/usr/lib/sing_box_config_facade.sh | 31 +- .../files/usr/lib/sing_box_config_manager.sh | 55 ++ podkop/files/usr/lib/updater.sh | 368 +++++++++ 17 files changed, 2025 insertions(+), 992 deletions(-) create mode 100644 podkop/files/usr/lib/updater.sh diff --git a/fe-app-podkop/locales/calls.json b/fe-app-podkop/locales/calls.json index 742d92b9..033db57f 100644 --- a/fe-app-podkop/locales/calls.json +++ b/fe-app-podkop/locales/calls.json @@ -3,1875 +3,1991 @@ "call": "✔ Enabled", "key": "✔ Enabled", "places": [ - "src/podkop/tabs/dashboard/initController.ts:345" + "src\\podkop\\tabs\\dashboard\\initController.ts:345" ] }, { "call": "✔ Running", "key": "✔ Running", "places": [ - "src/podkop/tabs/dashboard/initController.ts:356" + "src\\podkop\\tabs\\dashboard\\initController.ts:356" ] }, { "call": "✘ Disabled", "key": "✘ Disabled", "places": [ - "src/podkop/tabs/dashboard/initController.ts:346" + "src\\podkop\\tabs\\dashboard\\initController.ts:346" ] }, { "call": "✘ Stopped", "key": "✘ Stopped", "places": [ - "src/podkop/tabs/dashboard/initController.ts:357" + "src\\podkop\\tabs\\dashboard\\initController.ts:357" + ] + }, + { + "call": "Группировать по странам", + "key": "Группировать по странам", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:127" + ] + }, + { + "call": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", + "key": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:128" ] }, { "call": "Active Connections", "key": "Active Connections", "places": [ - "src/podkop/tabs/dashboard/initController.ts:307" + "src\\podkop\\tabs\\dashboard\\initController.ts:307" ] }, { "call": "Additional marking rules found", "key": "Additional marking rules found", "places": [ - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:106" + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:106" ] }, { "call": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "key": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:247" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:247" ] }, { "call": "Applicable for SOCKS and Shadowsocks proxy", "key": "Applicable for SOCKS and Shadowsocks proxy", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:199" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:251" ] }, { "call": "At least one valid domain must be specified. Comments-only content is not allowed.", "key": "At least one valid domain must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:444" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:496" ] }, { "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:525" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:577" ] }, { "call": "Available actions", "key": "Available actions", "places": [ - "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:43" + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:47" ] }, { "call": "Bootsrap DNS", "key": "Bootsrap DNS", "places": [ - "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:65" + "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:65" ] }, { "call": "Bootstrap DNS server", "key": "Bootstrap DNS server", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:45" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:45" ] }, { "call": "Browser is not using FakeIP", "key": "Browser is not using FakeIP", "places": [ - "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:58" + "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:58" ] }, { "call": "Browser is using FakeIP correctly", "key": "Browser is using FakeIP correctly", "places": [ - "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:57" + "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:57" ] }, { "call": "Cache File Path", "key": "Cache File Path", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:348" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:348" ] }, { "call": "Cache file path cannot be empty", "key": "Cache file path cannot be empty", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:362" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:362" ] }, { "call": "Cannot receive checks result", "key": "Cannot receive checks result", "places": [ - "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:27", - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:28", - "src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:27", - "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:25" + "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:27", + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:28", + "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:27", + "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:25" ] }, { "call": "Checking, please wait", "key": "Checking, please wait", "places": [ - "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:15", - "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:15", - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:13", - "src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:15", - "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:13" + "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:15", + "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:15", + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:13", + "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:15", + "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:13" ] }, { "call": "checks", "key": "checks", "places": [ - "src/podkop/tabs/diagnostic/helpers/getCheckTitle.ts:2" + "src\\podkop\\tabs\\diagnostic\\helpers\\getCheckTitle.ts:2" ] }, { "call": "Checks failed", "key": "Checks failed", "places": [ - "src/podkop/tabs/diagnostic/helpers/getMeta.ts:26" + "src\\podkop\\tabs\\diagnostic\\helpers\\getMeta.ts:26" ] }, { "call": "Checks passed", "key": "Checks passed", "places": [ - "src/podkop/tabs/diagnostic/helpers/getMeta.ts:13" + "src\\podkop\\tabs\\diagnostic\\helpers\\getMeta.ts:13" ] }, { "call": "CIDR must be between 0 and 32", "key": "CIDR must be between 0 and 32", "places": [ - "src/validators/validateSubnet.ts:33" + "src\\validators\\validateSubnet.ts:33" ] }, { "call": "Close", "key": "Close", "places": [ - "src/partials/modal/renderModal.ts:26" + "src\\partials\\modal\\renderModal.ts:26" ] }, { "call": "Community Lists", "key": "Community Lists", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:299" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:351" ] }, { "call": "Config File Path", "key": "Config File Path", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:335" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:335" ] }, { "call": "Configuration for Podkop service", "key": "Configuration for Podkop service", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:27" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:27" ] }, { "call": "Configuration Type", "key": "Configuration Type", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:23" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:23" ] }, { "call": "Connection Type", "key": "Connection Type", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:12" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:12" ] }, { "call": "Connection URL", "key": "Connection URL", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:26" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:26" ] }, { "call": "Copy", "key": "Copy", "places": [ - "src/partials/modal/renderModal.ts:20" + "src\\partials\\modal\\renderModal.ts:20" ] }, { "call": "Currently unavailable", "key": "Currently unavailable", "places": [ - "src/podkop/tabs/dashboard/partials/renderWidget.ts:22" + "src\\podkop\\tabs\\dashboard\\partials\\renderWidget.ts:22" ] }, { "call": "Dashboard", "key": "Dashboard", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:80" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:80" ] }, { "call": "Dashboard currently unavailable", "key": "Dashboard currently unavailable", "places": [ - "src/podkop/tabs/dashboard/partials/renderSections.ts:19" + "src\\podkop\\tabs\\dashboard\\partials\\renderSections.ts:19" ] }, { "call": "Delay in milliseconds before reloading podkop after interface UP", "key": "Delay in milliseconds before reloading podkop after interface UP", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:222" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:222" ] }, { "call": "Delay value cannot be empty", "key": "Delay value cannot be empty", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:229" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:229" ] }, { "call": "DHCP has DNS server", "key": "DHCP has DNS server", "places": [ - "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:82" + "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:82" ] }, { "call": "Diagnostics", "key": "Diagnostics", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:65" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:65" ] }, { "call": "Disable autostart", "key": "Disable autostart", "places": [ - "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:79" + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:83" ] }, { "call": "Disable QUIC", "key": "Disable QUIC", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:265" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:265" ] }, { "call": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "key": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:266" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:266" ] }, { "call": "Disabled", "key": "Disabled", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:390", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:470" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:442", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:522" ] }, { "call": "DNS on router", "key": "DNS on router", "places": [ - "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:77" + "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:77" ] }, { "call": "DNS over HTTPS (DoH)", "key": "DNS over HTTPS (DoH)", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:267", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:15" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:319", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:15" ] }, { "call": "DNS over TLS (DoT)", "key": "DNS over TLS (DoT)", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:268", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:16" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:320", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:16" ] }, { "call": "DNS Protocol Type", "key": "DNS Protocol Type", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:264", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:12" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:316", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:12" ] }, { "call": "DNS Rewrite TTL", "key": "DNS Rewrite TTL", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:68" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:68" ] }, { "call": "DNS Server", "key": "DNS Server", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:277", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:24" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:329", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:24" ] }, { "call": "DNS server address cannot be empty", "key": "DNS server address cannot be empty", "places": [ - "src/validators/validateDns.ts:7" + "src\\validators\\validateDns.ts:7" ] }, { "call": "Do not panic, everything can be fixed, just...", "key": "Do not panic, everything can be fixed, just...", "places": [ - "src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26" + "src\\podkop\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:26" ] }, { "call": "Domain Resolver", "key": "Domain Resolver", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:254" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:306" ] }, { "call": "Dont Touch My DHCP!", "key": "Dont Touch My DHCP!", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:326" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:326" ] }, { "call": "Downlink", "key": "Downlink", "places": [ - "src/podkop/tabs/dashboard/initController.ts:241", - "src/podkop/tabs/dashboard/initController.ts:275" + "src\\podkop\\tabs\\dashboard\\initController.ts:241", + "src\\podkop\\tabs\\dashboard\\initController.ts:275" ] }, { "call": "Download", "key": "Download", "places": [ - "src/partials/modal/renderModal.ts:15" + "src\\partials\\modal\\renderModal.ts:15" ] }, { "call": "Download Lists via Proxy/VPN", "key": "Download Lists via Proxy/VPN", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:288" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:288" ] }, { "call": "Download Lists via specific proxy section", "key": "Download Lists via specific proxy section", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:297" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:297" ] }, { "call": "Downloading all lists via specific Proxy/VPN", "key": "Downloading all lists via specific Proxy/VPN", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:289", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:298" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:289", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:298" ] }, { "call": "Dynamic List", "key": "Dynamic List", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:391", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:471" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:443", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:523" ] }, { "call": "Enable autostart", "key": "Enable autostart", "places": [ - "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:89" + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:93" ] }, { "call": "Enable built-in DNS resolver for domains handled by this section", "key": "Enable built-in DNS resolver for domains handled by this section", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:255" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:307" ] }, { "call": "Enable DNS resolve to get real IP when routing", "key": "Enable DNS resolve to get real IP when routing", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:691" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:746" ] }, { "call": "Enable Mixed Proxy", "key": "Enable Mixed Proxy", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:665" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:717" ] }, { "call": "Enable Output Network Interface", "key": "Enable Output Network Interface", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:126" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:126" ] }, { "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:666" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:718" ] }, { "call": "Enable YACD", "key": "Enable YACD", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:237" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:237" ] }, { "call": "Enable YACD WAN Access", "key": "Enable YACD WAN Access", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:246" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:246" ] }, { "call": "Enter complete outbound configuration in JSON format", "key": "Enter complete outbound configuration in JSON format", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:66" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:67" ] }, { "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:426" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:478" ] }, { "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:400" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:452" ] }, { "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:480" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:532" + ] + }, + { + "call": "Enter the subscription URL to fetch proxy configurations from your provider", + "key": "Enter the subscription URL to fetch proxy configurations from your provider", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:90" ] }, { "call": "Every 1 minute", "key": "Every 1 minute", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:138" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:187" + ] + }, + { + "call": "Every 12 hours", + "key": "Every 12 hours", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:119" + ] + }, + { + "call": "Every 3 hours", + "key": "Every 3 hours", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:117" ] }, { "call": "Every 3 minutes", "key": "Every 3 minutes", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:139" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:188" + ] + }, + { + "call": "Every 30 minutes", + "key": "Every 30 minutes", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:115" ] }, { "call": "Every 30 seconds", "key": "Every 30 seconds", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:137" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:186" ] }, { "call": "Every 5 minutes", "key": "Every 5 minutes", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:140" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:189" + ] + }, + { + "call": "Every 6 hours", + "key": "Every 6 hours", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:118" + ] + }, + { + "call": "Every day", + "key": "Every day", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:120" + ] + }, + { + "call": "Every hour", + "key": "Every hour", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:116" ] }, { "call": "Exclude NTP", "key": "Exclude NTP", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:402" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:402" ] }, { "call": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "key": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:403" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:403" ] }, { "call": "Failed to copy!", "key": "Failed to copy!", "places": [ - "src/helpers/copyToClipboard.ts:12" + "src\\helpers\\copyToClipboard.ts:12" ] }, { "call": "Failed to execute!", "key": "Failed to execute!", "places": [ - "src/podkop/tabs/diagnostic/initController.ts:227", - "src/podkop/tabs/diagnostic/initController.ts:231", - "src/podkop/tabs/diagnostic/initController.ts:261", - "src/podkop/tabs/diagnostic/initController.ts:265", - "src/podkop/tabs/diagnostic/initController.ts:302", - "src/podkop/tabs/diagnostic/initController.ts:306" + "src\\podkop\\tabs\\diagnostic\\initController.ts:229", + "src\\podkop\\tabs\\diagnostic\\initController.ts:233", + "src\\podkop\\tabs\\diagnostic\\initController.ts:263", + "src\\podkop\\tabs\\diagnostic\\initController.ts:267", + "src\\podkop\\tabs\\diagnostic\\initController.ts:304", + "src\\podkop\\tabs\\diagnostic\\initController.ts:308", + "src\\podkop\\tabs\\diagnostic\\initController.ts:342", + "src\\podkop\\tabs\\diagnostic\\initController.ts:346" ] }, { "call": "Fastest", "key": "Fastest", "places": [ - "src/podkop/methods/custom/getDashboardSections.ts:148", - "src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:59" + "src\\podkop\\methods\\custom\\getDashboardSections.ts:150", + "src\\podkop\\methods\\custom\\getDashboardSections.ts:181", + "src\\podkop\\methods\\custom\\getDashboardSections.ts:218", + "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:59" ] }, { "call": "Fully Routed IPs", "key": "Fully Routed IPs", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:638" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:690" ] }, { "call": "Get global check", "key": "Get global check", "places": [ - "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:98" + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:102" ] }, { "call": "Global check", "key": "Global check", "places": [ - "src/podkop/tabs/diagnostic/initController.ts:222" + "src\\podkop\\tabs\\diagnostic\\initController.ts:224" + ] + }, + { + "call": "How often to automatically update the subscription", + "key": "How often to automatically update the subscription", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:113" ] }, { "call": "HTTP error", "key": "HTTP error", "places": [ - "src/podkop/api.ts:27" + "src\\podkop\\api.ts:27" + ] + }, + { + "call": "Install extended", + "key": "Install extended", + "places": [ + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:129" + ] + }, + { + "call": "Install stable", + "key": "Install stable", + "places": [ + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:129" ] }, { "call": "Interface Monitoring", "key": "Interface Monitoring", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:189" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:189" ] }, { "call": "Interface Monitoring Delay", "key": "Interface Monitoring Delay", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:221" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:221" ] }, { "call": "Interface monitoring for Bad WAN", "key": "Interface monitoring for Bad WAN", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:190" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:190" ] }, { "call": "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH", "key": "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH", "places": [ - "src/validators/validateDns.ts:23" + "src\\validators\\validateDns.ts:23" ] }, { "call": "Invalid domain address", "key": "Invalid domain address", "places": [ - "src/validators/validateDomain.ts:18", - "src/validators/validateDomain.ts:27" + "src\\validators\\validateDomain.ts:18", + "src\\validators\\validateDomain.ts:27" ] }, { "call": "Invalid format. Use X.X.X.X or X.X.X.X/Y", "key": "Invalid format. Use X.X.X.X or X.X.X.X/Y", "places": [ - "src/validators/validateSubnet.ts:11" + "src\\validators\\validateSubnet.ts:11" ] }, { "call": "Invalid HY2 URL: insecure must be 0 or 1", "key": "Invalid HY2 URL: insecure must be 0 or 1", "places": [ - "src/validators/validateHysteriaUrl.ts:90" + "src\\validators\\validateHysteriaUrl.ts:90" ] }, { "call": "Invalid HY2 URL: invalid port number", "key": "Invalid HY2 URL: invalid port number", "places": [ - "src/validators/validateHysteriaUrl.ts:77" + "src\\validators\\validateHysteriaUrl.ts:77" ] }, { "call": "Invalid HY2 URL: missing credentials/server", "key": "Invalid HY2 URL: missing credentials/server", "places": [ - "src/validators/validateHysteriaUrl.ts:30" + "src\\validators\\validateHysteriaUrl.ts:30" ] }, { "call": "Invalid HY2 URL: missing host", "key": "Invalid HY2 URL: missing host", "places": [ - "src/validators/validateHysteriaUrl.ts:47" + "src\\validators\\validateHysteriaUrl.ts:47" ] }, { "call": "Invalid HY2 URL: missing host & port", "key": "Invalid HY2 URL: missing host & port", "places": [ - "src/validators/validateHysteriaUrl.ts:41" + "src\\validators\\validateHysteriaUrl.ts:41" ] }, { "call": "Invalid HY2 URL: missing password", "key": "Invalid HY2 URL: missing password", "places": [ - "src/validators/validateHysteriaUrl.ts:36" + "src\\validators\\validateHysteriaUrl.ts:36" ] }, { "call": "Invalid HY2 URL: missing port", "key": "Invalid HY2 URL: missing port", "places": [ - "src/validators/validateHysteriaUrl.ts:50" + "src\\validators\\validateHysteriaUrl.ts:50" ] }, { "call": "Invalid HY2 URL: must not contain spaces", "key": "Invalid HY2 URL: must not contain spaces", "places": [ - "src/validators/validateHysteriaUrl.ts:18" + "src\\validators\\validateHysteriaUrl.ts:18" ] }, { "call": "Invalid HY2 URL: must start with hysteria2:// or hy2://", "key": "Invalid HY2 URL: must start with hysteria2:// or hy2://", "places": [ - "src/validators/validateHysteriaUrl.ts:12" + "src\\validators\\validateHysteriaUrl.ts:12" ] }, { "call": "Invalid HY2 URL: obfs-password required when obfs is set", "key": "Invalid HY2 URL: obfs-password required when obfs is set", "places": [ - "src/validators/validateHysteriaUrl.ts:108" + "src\\validators\\validateHysteriaUrl.ts:108" ] }, { "call": "Invalid HY2 URL: parsing failed", "key": "Invalid HY2 URL: parsing failed", "places": [ - "src/validators/validateHysteriaUrl.ts:122" + "src\\validators\\validateHysteriaUrl.ts:122" ] }, { "call": "Invalid HY2 URL: sni cannot be empty", "key": "Invalid HY2 URL: sni cannot be empty", "places": [ - "src/validators/validateHysteriaUrl.ts:116" + "src\\validators\\validateHysteriaUrl.ts:116" ] }, { "call": "Invalid HY2 URL: unsupported obfs type", "key": "Invalid HY2 URL: unsupported obfs type", "places": [ - "src/validators/validateHysteriaUrl.ts:98" + "src\\validators\\validateHysteriaUrl.ts:98" ] }, { "call": "Invalid IP address", "key": "Invalid IP address", "places": [ - "src/validators/validateIp.ts:11" + "src\\validators\\validateIp.ts:11" ] }, { "call": "Invalid JSON format", "key": "Invalid JSON format", "places": [ - "src/validators/validateOutboundJson.ts:9" + "src\\validators\\validateOutboundJson.ts:9" ] }, { "call": "Invalid path format. Path must start with \"/\" and contain valid characters", "key": "Invalid path format. Path must start with \"/\" and contain valid characters", "places": [ - "src/validators/validatePath.ts:22" + "src\\validators\\validatePath.ts:22" ] }, { "call": "Invalid port number. Must be between 1 and 65535", "key": "Invalid port number. Must be between 1 and 65535", "places": [ - "src/validators/validateShadowsocksUrl.ts:85" + "src\\validators\\validateShadowsocksUrl.ts:85" ] }, { "call": "Invalid Shadowsocks URL: decoded credentials must contain method:password", "key": "Invalid Shadowsocks URL: decoded credentials must contain method:password", "places": [ - "src/validators/validateShadowsocksUrl.ts:37" + "src\\validators\\validateShadowsocksUrl.ts:37" ] }, { "call": "Invalid Shadowsocks URL: missing credentials", "key": "Invalid Shadowsocks URL: missing credentials", "places": [ - "src/validators/validateShadowsocksUrl.ts:27" + "src\\validators\\validateShadowsocksUrl.ts:27" ] }, { "call": "Invalid Shadowsocks URL: missing method and password separator \":\"", "key": "Invalid Shadowsocks URL: missing method and password separator \":\"", "places": [ - "src/validators/validateShadowsocksUrl.ts:46" + "src\\validators\\validateShadowsocksUrl.ts:46" ] }, { "call": "Invalid Shadowsocks URL: missing port", "key": "Invalid Shadowsocks URL: missing port", "places": [ - "src/validators/validateShadowsocksUrl.ts:76" + "src\\validators\\validateShadowsocksUrl.ts:76" ] }, { "call": "Invalid Shadowsocks URL: missing server", "key": "Invalid Shadowsocks URL: missing server", "places": [ - "src/validators/validateShadowsocksUrl.ts:67" + "src\\validators\\validateShadowsocksUrl.ts:67" ] }, { "call": "Invalid Shadowsocks URL: missing server address", "key": "Invalid Shadowsocks URL: missing server address", "places": [ - "src/validators/validateShadowsocksUrl.ts:58" + "src\\validators\\validateShadowsocksUrl.ts:58" ] }, { "call": "Invalid Shadowsocks URL: must not contain spaces", "key": "Invalid Shadowsocks URL: must not contain spaces", "places": [ - "src/validators/validateShadowsocksUrl.ts:16" + "src\\validators\\validateShadowsocksUrl.ts:16" ] }, { "call": "Invalid Shadowsocks URL: must start with ss://", "key": "Invalid Shadowsocks URL: must start with ss://", "places": [ - "src/validators/validateShadowsocksUrl.ts:8" + "src\\validators\\validateShadowsocksUrl.ts:8" ] }, { "call": "Invalid Shadowsocks URL: parsing failed", "key": "Invalid Shadowsocks URL: parsing failed", "places": [ - "src/validators/validateShadowsocksUrl.ts:91" + "src\\validators\\validateShadowsocksUrl.ts:91" ] }, { "call": "Invalid SOCKS URL: invalid host format", "key": "Invalid SOCKS URL: invalid host format", "places": [ - "src/validators/validateSocksUrl.ts:73" + "src\\validators\\validateSocksUrl.ts:73" ] }, { "call": "Invalid SOCKS URL: invalid port number", "key": "Invalid SOCKS URL: invalid port number", "places": [ - "src/validators/validateSocksUrl.ts:63" + "src\\validators\\validateSocksUrl.ts:63" ] }, { "call": "Invalid SOCKS URL: missing host and port", "key": "Invalid SOCKS URL: missing host and port", "places": [ - "src/validators/validateSocksUrl.ts:42" + "src\\validators\\validateSocksUrl.ts:42" ] }, { "call": "Invalid SOCKS URL: missing hostname or IP", "key": "Invalid SOCKS URL: missing hostname or IP", "places": [ - "src/validators/validateSocksUrl.ts:51" + "src\\validators\\validateSocksUrl.ts:51" ] }, { "call": "Invalid SOCKS URL: missing port", "key": "Invalid SOCKS URL: missing port", "places": [ - "src/validators/validateSocksUrl.ts:56" + "src\\validators\\validateSocksUrl.ts:56" ] }, { "call": "Invalid SOCKS URL: missing username", "key": "Invalid SOCKS URL: missing username", "places": [ - "src/validators/validateSocksUrl.ts:34" + "src\\validators\\validateSocksUrl.ts:34" ] }, { "call": "Invalid SOCKS URL: must not contain spaces", "key": "Invalid SOCKS URL: must not contain spaces", "places": [ - "src/validators/validateSocksUrl.ts:19" + "src\\validators\\validateSocksUrl.ts:19" ] }, { "call": "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://", "key": "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://", "places": [ - "src/validators/validateSocksUrl.ts:10" + "src\\validators\\validateSocksUrl.ts:10" ] }, { "call": "Invalid SOCKS URL: parsing failed", "key": "Invalid SOCKS URL: parsing failed", "places": [ - "src/validators/validateSocksUrl.ts:77" + "src\\validators\\validateSocksUrl.ts:77" ] }, { "call": "Invalid Trojan URL: must not contain spaces", "key": "Invalid Trojan URL: must not contain spaces", "places": [ - "src/validators/validateTrojanUrl.ts:15" + "src\\validators\\validateTrojanUrl.ts:15" ] }, { "call": "Invalid Trojan URL: must start with trojan://", "key": "Invalid Trojan URL: must start with trojan://", "places": [ - "src/validators/validateTrojanUrl.ts:8" + "src\\validators\\validateTrojanUrl.ts:8" ] }, { "call": "Invalid Trojan URL: parsing failed", "key": "Invalid Trojan URL: parsing failed", "places": [ - "src/validators/validateTrojanUrl.ts:56" + "src\\validators\\validateTrojanUrl.ts:56" ] }, { "call": "Invalid URL format", "key": "Invalid URL format", "places": [ - "src/validators/validateUrl.ts:8", - "src/validators/validateUrl.ts:31" + "src\\validators\\validateUrl.ts:8", + "src\\validators\\validateUrl.ts:31" ] }, { "call": "Invalid VLESS URL: parsing failed", "key": "Invalid VLESS URL: parsing failed", "places": [ - "src/validators/validateVlessUrl.ts:110" + "src\\validators\\validateVlessUrl.ts:110" ] }, { "call": "IP address 0.0.0.0 is not allowed", "key": "IP address 0.0.0.0 is not allowed", "places": [ - "src/validators/validateSubnet.ts:18" + "src\\validators\\validateSubnet.ts:18" ] }, { "call": "Issues detected", "key": "Issues detected", "places": [ - "src/podkop/tabs/diagnostic/helpers/getMeta.ts:20" + "src\\podkop\\tabs\\diagnostic\\helpers\\getMeta.ts:20" ] }, { "call": "Latest", "key": "Latest", "places": [ - "src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:48" + "src\\podkop\\tabs\\diagnostic\\helpers\\getPodkopVersionRow.ts:48" ] }, { "call": "List Update Frequency", "key": "List Update Frequency", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:276" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:276" ] }, { "call": "Local Domain Lists", "key": "Local Domain Lists", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:546" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:598" ] }, { "call": "Local Subnet Lists", "key": "Local Subnet Lists", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:569" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:621" ] }, { "call": "Log Level", "key": "Log Level", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:384" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:384" ] }, { "call": "Main DNS", "key": "Main DNS", "places": [ - "src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:72" + "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:72" ] }, { "call": "Memory Usage", "key": "Memory Usage", "places": [ - "src/podkop/tabs/dashboard/initController.ts:311" + "src\\podkop\\tabs\\dashboard\\initController.ts:311" ] }, { "call": "Mixed Proxy Port", "key": "Mixed Proxy Port", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:678" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:730" ] }, { "call": "Monitored Interfaces", "key": "Monitored Interfaces", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:198" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:198" ] }, { "call": "Must be a number in the range of 50 - 1000", "key": "Must be a number in the range of 50 - 1000", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:164" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:215" ] }, { "call": "Network Interface", "key": "Network Interface", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:208" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:260" ] }, { "call": "No other marking rules found", "key": "No other marking rules found", "places": [ - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:105" + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:105" ] }, { "call": "Not implement yet", "key": "Not implement yet", "places": [ - "src/podkop/tabs/diagnostic/partials/renderCheckSection.ts:189" + "src\\podkop\\tabs\\diagnostic\\partials\\renderCheckSection.ts:189" ] }, { "call": "Not responding", "key": "Not responding", "places": [ - "src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:75", - "src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:81", - "src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:100" + "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:75", + "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:81", + "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:100" ] }, { "call": "Not running", "key": "Not running", "places": [ - "src/podkop/tabs/diagnostic/diagnostic.store.ts:55", - "src/podkop/tabs/diagnostic/diagnostic.store.ts:63", - "src/podkop/tabs/diagnostic/diagnostic.store.ts:71", - "src/podkop/tabs/diagnostic/diagnostic.store.ts:79", - "src/podkop/tabs/diagnostic/diagnostic.store.ts:87" + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:59", + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:67", + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:75", + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:83", + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:91" ] }, { "call": "Operation timed out", "key": "Operation timed out", "places": [ - "src/helpers/withTimeout.ts:7" + "src\\helpers\\withTimeout.ts:7" ] }, { "call": "Outbound Config", "key": "Outbound Config", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:29" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:30" ] }, { "call": "Outbound Configuration", "key": "Outbound Configuration", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:65" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:66" ] }, { "call": "Outdated", "key": "Outdated", "places": [ - "src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:38" + "src\\podkop\\tabs\\diagnostic\\helpers\\getPodkopVersionRow.ts:38" ] }, { "call": "Output Network Interface", "key": "Output Network Interface", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:135" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:135" ] }, { "call": "Path cannot be empty", "key": "Path cannot be empty", "places": [ - "src/validators/validatePath.ts:7" + "src\\validators\\validatePath.ts:7" ] }, { "call": "Path must be absolute (start with /)", "key": "Path must be absolute (start with /)", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:366" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:366" ] }, { "call": "Path must contain at least one directory (like /tmp/cache.db)", "key": "Path must contain at least one directory (like /tmp/cache.db)", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:375" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:375" ] }, { "call": "Path must end with cache.db", "key": "Path must end with cache.db", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:370" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:370" ] }, { "call": "Pending", "key": "Pending", "places": [ - "src/podkop/tabs/diagnostic/diagnostic.store.ts:103", - "src/podkop/tabs/diagnostic/diagnostic.store.ts:111", - "src/podkop/tabs/diagnostic/diagnostic.store.ts:119", - "src/podkop/tabs/diagnostic/diagnostic.store.ts:127", - "src/podkop/tabs/diagnostic/diagnostic.store.ts:135" + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:107", + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:115", + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:123", + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:131", + "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:139" ] }, { "call": "Podkop", "key": "Podkop", "places": [ - "src/podkop/tabs/dashboard/initController.ts:343" + "src\\podkop\\tabs\\dashboard\\initController.ts:343" ] }, { "call": "Podkop Settings", "key": "Podkop Settings", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:26" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:26" ] }, { "call": "Podkop will not modify your DHCP configuration", "key": "Podkop will not modify your DHCP configuration", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:327" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:327" ] }, { "call": "Proxy Configuration URL", "key": "Proxy Configuration URL", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:36" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:37" ] }, { "call": "Proxy traffic is not routed via FakeIP", "key": "Proxy traffic is not routed via FakeIP", "places": [ - "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:66" + "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:66" ] }, { "call": "Proxy traffic is routed via FakeIP", "key": "Proxy traffic is routed via FakeIP", "places": [ - "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:65" + "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:65" ] }, { "call": "Regional options cannot be used together", "key": "Regional options cannot be used together", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:333" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:385" ] }, { "call": "Remote Domain Lists", "key": "Remote Domain Lists", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:592" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:644" ] }, { "call": "Remote Subnet Lists", "key": "Remote Subnet Lists", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:615" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:667" ] }, { "call": "Resolve real IP for routing", "key": "Resolve real IP for routing", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:690" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:745" ] }, { "call": "Restart podkop", "key": "Restart podkop", "places": [ - "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:49" + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:53" ] }, { "call": "Router DNS is not routed through sing-box", "key": "Router DNS is not routed through sing-box", "places": [ - "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:51" + "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:51" ] }, { "call": "Router DNS is routed through sing-box", "key": "Router DNS is routed through sing-box", "places": [ - "src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:50" + "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:50" ] }, { "call": "Routing Excluded IPs", "key": "Routing Excluded IPs", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:413" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:413" ] }, { "call": "Rules mangle counters", "key": "Rules mangle counters", "places": [ - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:79" + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:79" ] }, { "call": "Rules mangle exist", "key": "Rules mangle exist", "places": [ - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:74" + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:74" ] }, { "call": "Rules mangle output counters", "key": "Rules mangle output counters", "places": [ - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:89" + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:89" ] }, { "call": "Rules mangle output exist", "key": "Rules mangle output exist", "places": [ - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:84" + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:84" ] }, { "call": "Rules proxy counters", "key": "Rules proxy counters", "places": [ - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:99" + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:99" ] }, { "call": "Rules proxy exist", "key": "Rules proxy exist", "places": [ - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:94" + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:94" ] }, { "call": "Run Diagnostic", "key": "Run Diagnostic", "places": [ - "src/podkop/tabs/diagnostic/partials/renderRunAction.ts:15" + "src\\podkop\\tabs\\diagnostic\\partials\\renderRunAction.ts:15" ] }, { "call": "Russia inside restrictions", "key": "Russia inside restrictions", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:352" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:404" ] }, { "call": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "key": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:257" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:257" ] }, { "call": "Sections", "key": "Sections", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:36" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:36" ] }, { "call": "Select a predefined list for routing", "key": "Select a predefined list for routing", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:300" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:352" ] }, { "call": "Select between VPN and Proxy connection methods for traffic routing", "key": "Select between VPN and Proxy connection methods for traffic routing", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:13" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:13" ] }, { "call": "Select DNS protocol to use", "key": "Select DNS protocol to use", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:13" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:13" ] }, { "call": "Select how often the domain or subnet lists are updated automatically", "key": "Select how often the domain or subnet lists are updated automatically", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:277" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:277" ] }, { "call": "Select how to configure the proxy", "key": "Select how to configure the proxy", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:24" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:24" ] }, { "call": "Select network interface for VPN connection", "key": "Select network interface for VPN connection", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:209" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:261" ] }, { "call": "Select or enter DNS server address", "key": "Select or enter DNS server address", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:278", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:25" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:330", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:25" ] }, { "call": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "key": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:349" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:349" ] }, { "call": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "key": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:336" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:336" ] }, { "call": "Select the DNS protocol type for the domain resolver", "key": "Select the DNS protocol type for the domain resolver", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:265" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:317" ] }, { "call": "Select the list type for adding custom domains", "key": "Select the list type for adding custom domains", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:388" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:440" ] }, { "call": "Select the list type for adding custom subnets", "key": "Select the list type for adding custom subnets", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:468" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:520" ] }, { "call": "Select the log level for sing-box", "key": "Select the log level for sing-box", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:385" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:385" ] }, { "call": "Select the network interface from which the traffic will originate", "key": "Select the network interface from which the traffic will originate", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:90" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:90" ] }, { "call": "Select the network interface to which the traffic will originate", "key": "Select the network interface to which the traffic will originate", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:136" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:136" ] }, { "call": "Select the WAN interfaces to be monitored", "key": "Select the WAN interfaces to be monitored", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:199" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:199" ] }, { "call": "Selector", "key": "Selector", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:27" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:27" ] }, { "call": "Selector Proxy Links", "key": "Selector Proxy Links", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:88" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:137" ] }, { "call": "Services info", "key": "Services info", "places": [ - "src/podkop/tabs/dashboard/initController.ts:340" + "src\\podkop\\tabs\\dashboard\\initController.ts:340" ] }, { "call": "Settings", "key": "Settings", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:49" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:49" ] }, { "call": "Show sing-box config", "key": "Show sing-box config", "places": [ - "src/podkop/tabs/diagnostic/initController.ts:290", - "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:116" + "src\\podkop\\tabs\\diagnostic\\initController.ts:292", + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:120" ] }, { "call": "Sing-box", "key": "Sing-box", "places": [ - "src/podkop/tabs/dashboard/initController.ts:354" + "src\\podkop\\tabs\\dashboard\\initController.ts:354" ] }, { "call": "Sing-box autostart disabled", "key": "Sing-box autostart disabled", "places": [ - "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:77" + "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:77" + ] + }, + { + "call": "Sing-box core changed, version:", + "key": "Sing-box core changed, version:", + "places": [ + "src\\podkop\\tabs\\diagnostic\\initController.ts:337" ] }, { "call": "Sing-box installed", "key": "Sing-box installed", "places": [ - "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:62" + "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:62" ] }, { "call": "Sing-box listening ports", "key": "Sing-box listening ports", "places": [ - "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:87" + "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:87" ] }, { "call": "Sing-box process running", "key": "Sing-box process running", "places": [ - "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:82" + "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:82" ] }, { "call": "Sing-box service exist", "key": "Sing-box service exist", "places": [ - "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:72" + "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:72" ] }, { "call": "Sing-box version is compatible (newer than 1.12.4)", "key": "Sing-box version is compatible (newer than 1.12.4)", "places": [ - "src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:67" + "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:67" ] }, { "call": "Source Network Interface", "key": "Source Network Interface", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:89" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:89" ] }, { "call": "Specify a local IP address to be excluded from routing", "key": "Specify a local IP address to be excluded from routing", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:414" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:414" ] }, { "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:639" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:691" ] }, { "call": "Specify remote URLs to download and use domain lists", "key": "Specify remote URLs to download and use domain lists", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:593" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:645" ] }, { "call": "Specify remote URLs to download and use subnet lists", "key": "Specify remote URLs to download and use subnet lists", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:616" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:668" ] }, { "call": "Specify the path to the list file located on the router filesystem", "key": "Specify the path to the list file located on the router filesystem", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:547", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:570" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:599", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:622" ] }, { "call": "Start podkop", "key": "Start podkop", "places": [ - "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:69" + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:73" ] }, { "call": "Stop podkop", "key": "Stop podkop", "places": [ - "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:59" + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:63" + ] + }, + { + "call": "Subscription", + "key": "Subscription", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:29" + ] + }, + { + "call": "Subscription Update Interval", + "key": "Subscription Update Interval", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:112" + ] + }, + { + "call": "Subscription URL", + "key": "Subscription URL", + "places": [ + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:89" ] }, { "call": "Successfully copied!", "key": "Successfully copied!", "places": [ - "src/helpers/copyToClipboard.ts:10" + "src\\helpers\\copyToClipboard.ts:10" ] }, { "call": "System info", "key": "System info", "places": [ - "src/podkop/tabs/dashboard/initController.ts:304" + "src\\podkop\\tabs\\dashboard\\initController.ts:304" ] }, { "call": "System information", "key": "System information", "places": [ - "src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts:21" + "src\\podkop\\tabs\\diagnostic\\partials\\renderSystemInfo.ts:21" ] }, { "call": "Table exist", "key": "Table exist", "places": [ - "src/podkop/tabs/diagnostic/checks/runNftCheck.ts:69" + "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:69" ] }, { "call": "Test latency", "key": "Test latency", "places": [ - "src/podkop/tabs/dashboard/partials/renderSections.ts:108" + "src\\podkop\\tabs\\dashboard\\partials\\renderSections.ts:108" ] }, { "call": "Text List", "key": "Text List", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:392", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:472" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:444", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:524" ] }, { "call": "The DNS server used to look up the IP address of an upstream DNS server", "key": "The DNS server used to look up the IP address of an upstream DNS server", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:46" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:46" ] }, { "call": "The interval between connectivity tests", "key": "The interval between connectivity tests", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:135" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:184" ] }, { "call": "The maximum difference in response times (ms) allowed when comparing servers", "key": "The maximum difference in response times (ms) allowed when comparing servers", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:148" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:198" ] }, { "call": "The URL used to test server connectivity", "key": "The URL used to test server connectivity", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:171" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:222" ] }, { "call": "Time in seconds for DNS record caching (default: 60)", "key": "Time in seconds for DNS record caching (default: 60)", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:69" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:69" ] }, { "call": "Traffic", "key": "Traffic", "places": [ - "src/podkop/tabs/dashboard/initController.ts:238" + "src\\podkop\\tabs\\dashboard\\initController.ts:238" ] }, { "call": "Traffic Total", "key": "Traffic Total", "places": [ - "src/podkop/tabs/dashboard/initController.ts:268" + "src\\podkop\\tabs\\dashboard\\initController.ts:268" ] }, { "call": "Troubleshooting", "key": "Troubleshooting", "places": [ - "src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts:25" + "src\\podkop\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:25" ] }, { "call": "TTL must be a positive number", "key": "TTL must be a positive number", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:80" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:80" ] }, { "call": "TTL value cannot be empty", "key": "TTL value cannot be empty", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:75" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:75" ] }, { "call": "UDP (Unprotected DNS)", "key": "UDP (Unprotected DNS)", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:269", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:17" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:321", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:17" ] }, { "call": "UDP over TCP", "key": "UDP over TCP", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:198" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:250" ] }, { "call": "unknown", "key": "unknown", "places": [ - "src/podkop/tabs/diagnostic/initController.ts:38", - "src/podkop/tabs/diagnostic/initController.ts:39", - "src/podkop/tabs/diagnostic/initController.ts:40", - "src/podkop/tabs/diagnostic/initController.ts:41", - "src/podkop/tabs/diagnostic/initController.ts:42", - "src/podkop/tabs/diagnostic/initController.ts:43", - "src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:7" + "src\\podkop\\tabs\\diagnostic\\initController.ts:39", + "src\\podkop\\tabs\\diagnostic\\initController.ts:40", + "src\\podkop\\tabs\\diagnostic\\initController.ts:41", + "src\\podkop\\tabs\\diagnostic\\initController.ts:42", + "src\\podkop\\tabs\\diagnostic\\initController.ts:43", + "src\\podkop\\tabs\\diagnostic\\initController.ts:44", + "src\\podkop\\tabs\\diagnostic\\helpers\\getPodkopVersionRow.ts:7" ] }, { "call": "Unknown error", "key": "Unknown error", "places": [ - "src/podkop/api.ts:40" + "src\\podkop\\api.ts:40" ] }, { "call": "Uplink", "key": "Uplink", "places": [ - "src/podkop/tabs/dashboard/initController.ts:240", - "src/podkop/tabs/dashboard/initController.ts:271" + "src\\podkop\\tabs\\dashboard\\initController.ts:240", + "src\\podkop\\tabs\\dashboard\\initController.ts:271" ] }, { "call": "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", "key": "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", "places": [ - "src/validators/validateProxyUrl.ts:37" + "src\\validators\\validateProxyUrl.ts:37" ] }, { "call": "URL must use one of the following protocols:", "key": "URL must use one of the following protocols:", "places": [ - "src/validators/validateUrl.ts:17" + "src\\validators\\validateUrl.ts:17" ] }, { "call": "URLTest", "key": "URLTest", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:28" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:28" ] }, { "call": "URLTest Check Interval", "key": "URLTest Check Interval", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:134" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:183" ] }, { "call": "URLTest Proxy Links", "key": "URLTest Proxy Links", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:111" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:160" ] }, { "call": "URLTest Testing URL", "key": "URLTest Testing URL", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:170" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:221" ] }, { "call": "URLTest Tolerance", "key": "URLTest Tolerance", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:147" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:197" ] }, { "call": "User Domain List Type", "key": "User Domain List Type", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:387" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:439" ] }, { "call": "User Domains", "key": "User Domains", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:399" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:451" ] }, { "call": "User Domains List", "key": "User Domains List", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:425" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:477" ] }, { "call": "User Subnet List Type", "key": "User Subnet List Type", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:467" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:519" ] }, { "call": "User Subnets", "key": "User Subnets", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:479" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:531" ] }, { "call": "User Subnets List", "key": "User Subnets List", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:505" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:557" ] }, { "call": "Valid", "key": "Valid", "places": [ - "src/validators/validateDns.ts:14", - "src/validators/validateDns.ts:18", - "src/validators/validateDomain.ts:13", - "src/validators/validateDomain.ts:30", - "src/validators/validateHysteriaUrl.ts:120", - "src/validators/validateIp.ts:8", - "src/validators/validateOutboundJson.ts:7", - "src/validators/validatePath.ts:16", - "src/validators/validateShadowsocksUrl.ts:95", - "src/validators/validateSocksUrl.ts:80", - "src/validators/validateSubnet.ts:38", - "src/validators/validateTrojanUrl.ts:59", - "src/validators/validateUrl.ts:28", - "src/validators/validateVlessUrl.ts:108" + "src\\validators\\validateDns.ts:14", + "src\\validators\\validateDns.ts:18", + "src\\validators\\validateDomain.ts:13", + "src\\validators\\validateDomain.ts:30", + "src\\validators\\validateHysteriaUrl.ts:120", + "src\\validators\\validateIp.ts:8", + "src\\validators\\validateOutboundJson.ts:7", + "src\\validators\\validatePath.ts:16", + "src\\validators\\validateShadowsocksUrl.ts:95", + "src\\validators\\validateSocksUrl.ts:80", + "src\\validators\\validateSubnet.ts:38", + "src\\validators\\validateTrojanUrl.ts:59", + "src\\validators\\validateUrl.ts:28", + "src\\validators\\validateVlessUrl.ts:108" ] }, { "call": "Validation errors:", "key": "Validation errors:", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:458", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:537" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:510", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:589" ] }, { "call": "View logs", "key": "View logs", "places": [ - "src/podkop/tabs/diagnostic/initController.ts:256", - "src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:107" + "src\\podkop\\tabs\\diagnostic\\initController.ts:258", + "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:111" ] }, { "call": "Visit Wiki", "key": "Visit Wiki", "places": [ - "src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts:31" + "src\\podkop\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:31" ] }, { "call": "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "key": "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:37", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:89", - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:112" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:38", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:138", + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:161" ] }, { "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:335" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:387" ] }, { "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:354" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:406" ] }, { "call": "YACD Secret Key", "key": "YACD Secret Key", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:256" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:256" ] }, { "call": "You can select Output Network Interface, by default autodetect", "key": "You can select Output Network Interface, by default autodetect", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:127" + "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:127" ] } ] \ No newline at end of file diff --git a/fe-app-podkop/locales/podkop.pot b/fe-app-podkop/locales/podkop.pot index d96b948e..a722424c 100644 --- a/fe-app-podkop/locales/podkop.pot +++ b/fe-app-podkop/locales/podkop.pot @@ -1,1115 +1,1183 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PODKOP package. -# divocatt <210179590+divocatt@users.noreply.github.com>, 2026. +# yandexru45 , 2026. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 13:40+0300\n" -"PO-Revision-Date: 2026-05-29 13:40+0300\n" -"Last-Translator: divocatt <210179590+divocatt@users.noreply.github.com>\n" +"POT-Creation-Date: 2026-06-02 11:25+0300\n" +"PO-Revision-Date: 2026-06-02 11:25+0300\n" +"Last-Translator: yandexru45 \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/podkop/tabs/dashboard/initController.ts:345 +#: src\podkop\tabs\dashboard\initController.ts:345 msgid "✔ Enabled" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:356 +#: src\podkop\tabs\dashboard\initController.ts:356 msgid "✔ Running" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:346 +#: src\podkop\tabs\dashboard\initController.ts:346 msgid "✘ Disabled" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:357 +#: src\podkop\tabs\dashboard\initController.ts:357 msgid "✘ Stopped" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:307 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:127 +msgid "Группировать по странам" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:128 +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "" + +#: src\podkop\tabs\dashboard\initController.ts:307 msgid "Active Connections" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:106 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:106 msgid "Additional marking rules found" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:247 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:247 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:199 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:251 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:444 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:496 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:525 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:577 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:43 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:47 msgid "Available actions" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:65 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:65 msgid "Bootsrap DNS" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:45 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:45 msgid "Bootstrap DNS server" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:58 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:58 msgid "Browser is not using FakeIP" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:57 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:57 msgid "Browser is using FakeIP correctly" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:348 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:348 msgid "Cache File Path" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:362 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:362 msgid "Cache file path cannot be empty" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:27 -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:28 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:27 -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:25 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:27 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:28 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:27 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:25 msgid "Cannot receive checks result" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:15 -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:15 -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:13 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:15 -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:13 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:15 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:15 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:13 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:15 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:13 msgid "Checking, please wait" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getCheckTitle.ts:2 +#: src\podkop\tabs\diagnostic\helpers\getCheckTitle.ts:2 msgid "checks" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getMeta.ts:26 +#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:26 msgid "Checks failed" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getMeta.ts:13 +#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:13 msgid "Checks passed" msgstr "" -#: src/validators/validateSubnet.ts:33 +#: src\validators\validateSubnet.ts:33 msgid "CIDR must be between 0 and 32" msgstr "" -#: src/partials/modal/renderModal.ts:26 +#: src\partials\modal\renderModal.ts:26 msgid "Close" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:299 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:351 msgid "Community Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:335 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:335 msgid "Config File Path" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:27 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:27 msgid "Configuration for Podkop service" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:23 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:23 msgid "Configuration Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:12 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:12 msgid "Connection Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:26 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:26 msgid "Connection URL" msgstr "" -#: src/partials/modal/renderModal.ts:20 +#: src\partials\modal\renderModal.ts:20 msgid "Copy" msgstr "" -#: src/podkop/tabs/dashboard/partials/renderWidget.ts:22 +#: src\podkop\tabs\dashboard\partials\renderWidget.ts:22 msgid "Currently unavailable" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:80 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:80 msgid "Dashboard" msgstr "" -#: src/podkop/tabs/dashboard/partials/renderSections.ts:19 +#: src\podkop\tabs\dashboard\partials\renderSections.ts:19 msgid "Dashboard currently unavailable" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:222 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:222 msgid "Delay in milliseconds before reloading podkop after interface UP" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:229 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:229 msgid "Delay value cannot be empty" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:82 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:82 msgid "DHCP has DNS server" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:65 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:65 msgid "Diagnostics" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:79 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:83 msgid "Disable autostart" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:265 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:265 msgid "Disable QUIC" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:266 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:266 msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:390 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:470 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:442 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:522 msgid "Disabled" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:77 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:77 msgid "DNS on router" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:267 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:15 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:319 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:268 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:16 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:320 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:264 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:12 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:316 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:12 msgid "DNS Protocol Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:68 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:68 msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:277 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:24 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:329 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:24 msgid "DNS Server" msgstr "" -#: src/validators/validateDns.ts:7 +#: src\validators\validateDns.ts:7 msgid "DNS server address cannot be empty" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26 +#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:26 msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:254 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:306 msgid "Domain Resolver" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:326 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:326 msgid "Dont Touch My DHCP!" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:241 -#: src/podkop/tabs/dashboard/initController.ts:275 +#: src\podkop\tabs\dashboard\initController.ts:241 +#: src\podkop\tabs\dashboard\initController.ts:275 msgid "Downlink" msgstr "" -#: src/partials/modal/renderModal.ts:15 +#: src\partials\modal\renderModal.ts:15 msgid "Download" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:288 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:288 msgid "Download Lists via Proxy/VPN" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:297 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:297 msgid "Download Lists via specific proxy section" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:289 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:298 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:289 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:298 msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:391 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:471 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:443 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:523 msgid "Dynamic List" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:89 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:93 msgid "Enable autostart" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:255 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:307 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:691 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:746 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:665 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:717 msgid "Enable Mixed Proxy" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:126 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:126 msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:666 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:718 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:237 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:237 msgid "Enable YACD" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:246 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:246 msgid "Enable YACD WAN Access" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:66 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:67 msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:426 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:478 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:400 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:452 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:480 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:532 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:138 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:90 +msgid "Enter the subscription URL to fetch proxy configurations from your provider" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:187 msgid "Every 1 minute" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:139 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:119 +msgid "Every 12 hours" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:117 +msgid "Every 3 hours" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:188 msgid "Every 3 minutes" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:137 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:115 +msgid "Every 30 minutes" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:186 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:140 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:189 msgid "Every 5 minutes" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:402 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:118 +msgid "Every 6 hours" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:120 +msgid "Every day" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:116 +msgid "Every hour" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:402 msgid "Exclude NTP" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:403 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:403 msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: src/helpers/copyToClipboard.ts:12 +#: src\helpers\copyToClipboard.ts:12 msgid "Failed to copy!" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:227 -#: src/podkop/tabs/diagnostic/initController.ts:231 -#: src/podkop/tabs/diagnostic/initController.ts:261 -#: src/podkop/tabs/diagnostic/initController.ts:265 -#: src/podkop/tabs/diagnostic/initController.ts:302 -#: src/podkop/tabs/diagnostic/initController.ts:306 +#: src\podkop\tabs\diagnostic\initController.ts:229 +#: src\podkop\tabs\diagnostic\initController.ts:233 +#: src\podkop\tabs\diagnostic\initController.ts:263 +#: src\podkop\tabs\diagnostic\initController.ts:267 +#: src\podkop\tabs\diagnostic\initController.ts:304 +#: src\podkop\tabs\diagnostic\initController.ts:308 +#: src\podkop\tabs\diagnostic\initController.ts:342 +#: src\podkop\tabs\diagnostic\initController.ts:346 msgid "Failed to execute!" msgstr "" -#: src/podkop/methods/custom/getDashboardSections.ts:148 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:59 +#: src\podkop\methods\custom\getDashboardSections.ts:150 +#: src\podkop\methods\custom\getDashboardSections.ts:181 +#: src\podkop\methods\custom\getDashboardSections.ts:218 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:59 msgid "Fastest" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:638 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:690 msgid "Fully Routed IPs" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:98 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:102 msgid "Get global check" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:222 +#: src\podkop\tabs\diagnostic\initController.ts:224 msgid "Global check" msgstr "" -#: src/podkop/api.ts:27 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:113 +msgid "How often to automatically update the subscription" +msgstr "" + +#: src\podkop\api.ts:27 msgid "HTTP error" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:189 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:129 +msgid "Install extended" +msgstr "" + +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:129 +msgid "Install stable" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:189 msgid "Interface Monitoring" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:221 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:221 msgid "Interface Monitoring Delay" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:190 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:190 msgid "Interface monitoring for Bad WAN" msgstr "" -#: src/validators/validateDns.ts:23 +#: src\validators\validateDns.ts:23 msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" msgstr "" -#: src/validators/validateDomain.ts:18 -#: src/validators/validateDomain.ts:27 +#: src\validators\validateDomain.ts:18 +#: src\validators\validateDomain.ts:27 msgid "Invalid domain address" msgstr "" -#: src/validators/validateSubnet.ts:11 +#: src\validators\validateSubnet.ts:11 msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" msgstr "" -#: src/validators/validateHysteriaUrl.ts:90 +#: src\validators\validateHysteriaUrl.ts:90 msgid "Invalid HY2 URL: insecure must be 0 or 1" msgstr "" -#: src/validators/validateHysteriaUrl.ts:77 +#: src\validators\validateHysteriaUrl.ts:77 msgid "Invalid HY2 URL: invalid port number" msgstr "" -#: src/validators/validateHysteriaUrl.ts:30 +#: src\validators\validateHysteriaUrl.ts:30 msgid "Invalid HY2 URL: missing credentials/server" msgstr "" -#: src/validators/validateHysteriaUrl.ts:47 +#: src\validators\validateHysteriaUrl.ts:47 msgid "Invalid HY2 URL: missing host" msgstr "" -#: src/validators/validateHysteriaUrl.ts:41 +#: src\validators\validateHysteriaUrl.ts:41 msgid "Invalid HY2 URL: missing host & port" msgstr "" -#: src/validators/validateHysteriaUrl.ts:36 +#: src\validators\validateHysteriaUrl.ts:36 msgid "Invalid HY2 URL: missing password" msgstr "" -#: src/validators/validateHysteriaUrl.ts:50 +#: src\validators\validateHysteriaUrl.ts:50 msgid "Invalid HY2 URL: missing port" msgstr "" -#: src/validators/validateHysteriaUrl.ts:18 +#: src\validators\validateHysteriaUrl.ts:18 msgid "Invalid HY2 URL: must not contain spaces" msgstr "" -#: src/validators/validateHysteriaUrl.ts:12 +#: src\validators\validateHysteriaUrl.ts:12 msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" msgstr "" -#: src/validators/validateHysteriaUrl.ts:108 +#: src\validators\validateHysteriaUrl.ts:108 msgid "Invalid HY2 URL: obfs-password required when obfs is set" msgstr "" -#: src/validators/validateHysteriaUrl.ts:122 +#: src\validators\validateHysteriaUrl.ts:122 msgid "Invalid HY2 URL: parsing failed" msgstr "" -#: src/validators/validateHysteriaUrl.ts:116 +#: src\validators\validateHysteriaUrl.ts:116 msgid "Invalid HY2 URL: sni cannot be empty" msgstr "" -#: src/validators/validateHysteriaUrl.ts:98 +#: src\validators\validateHysteriaUrl.ts:98 msgid "Invalid HY2 URL: unsupported obfs type" msgstr "" -#: src/validators/validateIp.ts:11 +#: src\validators\validateIp.ts:11 msgid "Invalid IP address" msgstr "" -#: src/validators/validateOutboundJson.ts:9 +#: src\validators\validateOutboundJson.ts:9 msgid "Invalid JSON format" msgstr "" -#: src/validators/validatePath.ts:22 +#: src\validators\validatePath.ts:22 msgid "Invalid path format. Path must start with \"/\" and contain valid characters" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:85 +#: src\validators\validateShadowsocksUrl.ts:85 msgid "Invalid port number. Must be between 1 and 65535" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:37 +#: src\validators\validateShadowsocksUrl.ts:37 msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:27 +#: src\validators\validateShadowsocksUrl.ts:27 msgid "Invalid Shadowsocks URL: missing credentials" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:46 +#: src\validators\validateShadowsocksUrl.ts:46 msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:76 +#: src\validators\validateShadowsocksUrl.ts:76 msgid "Invalid Shadowsocks URL: missing port" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:67 +#: src\validators\validateShadowsocksUrl.ts:67 msgid "Invalid Shadowsocks URL: missing server" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:58 +#: src\validators\validateShadowsocksUrl.ts:58 msgid "Invalid Shadowsocks URL: missing server address" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:16 +#: src\validators\validateShadowsocksUrl.ts:16 msgid "Invalid Shadowsocks URL: must not contain spaces" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:8 +#: src\validators\validateShadowsocksUrl.ts:8 msgid "Invalid Shadowsocks URL: must start with ss://" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:91 +#: src\validators\validateShadowsocksUrl.ts:91 msgid "Invalid Shadowsocks URL: parsing failed" msgstr "" -#: src/validators/validateSocksUrl.ts:73 +#: src\validators\validateSocksUrl.ts:73 msgid "Invalid SOCKS URL: invalid host format" msgstr "" -#: src/validators/validateSocksUrl.ts:63 +#: src\validators\validateSocksUrl.ts:63 msgid "Invalid SOCKS URL: invalid port number" msgstr "" -#: src/validators/validateSocksUrl.ts:42 +#: src\validators\validateSocksUrl.ts:42 msgid "Invalid SOCKS URL: missing host and port" msgstr "" -#: src/validators/validateSocksUrl.ts:51 +#: src\validators\validateSocksUrl.ts:51 msgid "Invalid SOCKS URL: missing hostname or IP" msgstr "" -#: src/validators/validateSocksUrl.ts:56 +#: src\validators\validateSocksUrl.ts:56 msgid "Invalid SOCKS URL: missing port" msgstr "" -#: src/validators/validateSocksUrl.ts:34 +#: src\validators\validateSocksUrl.ts:34 msgid "Invalid SOCKS URL: missing username" msgstr "" -#: src/validators/validateSocksUrl.ts:19 +#: src\validators\validateSocksUrl.ts:19 msgid "Invalid SOCKS URL: must not contain spaces" msgstr "" -#: src/validators/validateSocksUrl.ts:10 +#: src\validators\validateSocksUrl.ts:10 msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" msgstr "" -#: src/validators/validateSocksUrl.ts:77 +#: src\validators\validateSocksUrl.ts:77 msgid "Invalid SOCKS URL: parsing failed" msgstr "" -#: src/validators/validateTrojanUrl.ts:15 +#: src\validators\validateTrojanUrl.ts:15 msgid "Invalid Trojan URL: must not contain spaces" msgstr "" -#: src/validators/validateTrojanUrl.ts:8 +#: src\validators\validateTrojanUrl.ts:8 msgid "Invalid Trojan URL: must start with trojan://" msgstr "" -#: src/validators/validateTrojanUrl.ts:56 +#: src\validators\validateTrojanUrl.ts:56 msgid "Invalid Trojan URL: parsing failed" msgstr "" -#: src/validators/validateUrl.ts:8 -#: src/validators/validateUrl.ts:31 +#: src\validators\validateUrl.ts:8 +#: src\validators\validateUrl.ts:31 msgid "Invalid URL format" msgstr "" -#: src/validators/validateVlessUrl.ts:110 +#: src\validators\validateVlessUrl.ts:110 msgid "Invalid VLESS URL: parsing failed" msgstr "" -#: src/validators/validateSubnet.ts:18 +#: src\validators\validateSubnet.ts:18 msgid "IP address 0.0.0.0 is not allowed" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getMeta.ts:20 +#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:20 msgid "Issues detected" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:48 +#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:48 msgid "Latest" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:276 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:276 msgid "List Update Frequency" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:546 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:598 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:569 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:621 msgid "Local Subnet Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:384 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:384 msgid "Log Level" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:72 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:72 msgid "Main DNS" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:311 +#: src\podkop\tabs\dashboard\initController.ts:311 msgid "Memory Usage" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:678 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:730 msgid "Mixed Proxy Port" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:198 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:198 msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:164 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:215 msgid "Must be a number in the range of 50 - 1000" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:208 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:260 msgid "Network Interface" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:105 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:105 msgid "No other marking rules found" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderCheckSection.ts:189 +#: src\podkop\tabs\diagnostic\partials\renderCheckSection.ts:189 msgid "Not implement yet" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:75 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:81 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:100 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:75 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:81 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:100 msgid "Not responding" msgstr "" -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:55 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:63 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:71 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:79 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:87 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:59 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:67 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:75 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:83 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:91 msgid "Not running" msgstr "" -#: src/helpers/withTimeout.ts:7 +#: src\helpers\withTimeout.ts:7 msgid "Operation timed out" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:29 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:30 msgid "Outbound Config" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:65 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:66 msgid "Outbound Configuration" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:38 +#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:38 msgid "Outdated" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:135 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:135 msgid "Output Network Interface" msgstr "" -#: src/validators/validatePath.ts:7 +#: src\validators\validatePath.ts:7 msgid "Path cannot be empty" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:366 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:366 msgid "Path must be absolute (start with /)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:375 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:375 msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:370 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:370 msgid "Path must end with cache.db" msgstr "" -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:103 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:111 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:119 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:127 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:135 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:107 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:115 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:123 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:131 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:139 msgid "Pending" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:343 +#: src\podkop\tabs\dashboard\initController.ts:343 msgid "Podkop" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:26 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:26 msgid "Podkop Settings" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:327 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:327 msgid "Podkop will not modify your DHCP configuration" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:36 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:37 msgid "Proxy Configuration URL" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:66 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:66 msgid "Proxy traffic is not routed via FakeIP" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:65 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:65 msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:333 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:385 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:592 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:644 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:615 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:667 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:690 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:745 msgid "Resolve real IP for routing" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:49 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:53 msgid "Restart podkop" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:51 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:51 msgid "Router DNS is not routed through sing-box" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:50 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:50 msgid "Router DNS is routed through sing-box" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:413 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:413 msgid "Routing Excluded IPs" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:79 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:79 msgid "Rules mangle counters" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:74 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:74 msgid "Rules mangle exist" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:89 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:89 msgid "Rules mangle output counters" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:84 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:84 msgid "Rules mangle output exist" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:99 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:99 msgid "Rules proxy counters" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:94 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:94 msgid "Rules proxy exist" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderRunAction.ts:15 +#: src\podkop\tabs\diagnostic\partials\renderRunAction.ts:15 msgid "Run Diagnostic" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:352 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:404 msgid "Russia inside restrictions" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:257 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:257 msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:36 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:36 msgid "Sections" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:300 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:352 msgid "Select a predefined list for routing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:13 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:13 msgid "Select between VPN and Proxy connection methods for traffic routing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:13 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:13 msgid "Select DNS protocol to use" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:277 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:277 msgid "Select how often the domain or subnet lists are updated automatically" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:24 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:24 msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:209 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:261 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:278 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:25 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:330 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:25 msgid "Select or enter DNS server address" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:349 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:349 msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:336 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:336 msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:265 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:317 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:388 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:440 msgid "Select the list type for adding custom domains" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:468 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:520 msgid "Select the list type for adding custom subnets" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:385 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:385 msgid "Select the log level for sing-box" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:90 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:90 msgid "Select the network interface from which the traffic will originate" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:136 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:136 msgid "Select the network interface to which the traffic will originate" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:199 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:199 msgid "Select the WAN interfaces to be monitored" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:27 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:27 msgid "Selector" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:88 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:137 msgid "Selector Proxy Links" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:340 +#: src\podkop\tabs\dashboard\initController.ts:340 msgid "Services info" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:49 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:49 msgid "Settings" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:290 -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:116 +#: src\podkop\tabs\diagnostic\initController.ts:292 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:120 msgid "Show sing-box config" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:354 +#: src\podkop\tabs\dashboard\initController.ts:354 msgid "Sing-box" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:77 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:77 msgid "Sing-box autostart disabled" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:62 +#: src\podkop\tabs\diagnostic\initController.ts:337 +msgid "Sing-box core changed, version:" +msgstr "" + +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:62 msgid "Sing-box installed" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:87 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:87 msgid "Sing-box listening ports" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:82 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:82 msgid "Sing-box process running" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:72 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:72 msgid "Sing-box service exist" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:67 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:67 msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:89 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:89 msgid "Source Network Interface" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:414 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:414 msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:639 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:691 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:593 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:645 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:616 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:668 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:547 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:570 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:599 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:622 msgid "Specify the path to the list file located on the router filesystem" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:69 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:73 msgid "Start podkop" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:59 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:63 msgid "Stop podkop" msgstr "" -#: src/helpers/copyToClipboard.ts:10 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:29 +msgid "Subscription" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:112 +msgid "Subscription Update Interval" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:89 +msgid "Subscription URL" +msgstr "" + +#: src\helpers\copyToClipboard.ts:10 msgid "Successfully copied!" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:304 +#: src\podkop\tabs\dashboard\initController.ts:304 msgid "System info" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts:21 +#: src\podkop\tabs\diagnostic\partials\renderSystemInfo.ts:21 msgid "System information" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:69 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:69 msgid "Table exist" msgstr "" -#: src/podkop/tabs/dashboard/partials/renderSections.ts:108 +#: src\podkop\tabs\dashboard\partials\renderSections.ts:108 msgid "Test latency" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:392 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:472 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:444 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:524 msgid "Text List" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:46 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:46 msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:135 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:184 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:148 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:198 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:171 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:222 msgid "The URL used to test server connectivity" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:69 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:69 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:238 +#: src\podkop\tabs\dashboard\initController.ts:238 msgid "Traffic" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:268 +#: src\podkop\tabs\dashboard\initController.ts:268 msgid "Traffic Total" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts:25 +#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:25 msgid "Troubleshooting" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:80 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:80 msgid "TTL must be a positive number" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:75 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:75 msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:269 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:17 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:321 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:198 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:250 msgid "UDP over TCP" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:38 -#: src/podkop/tabs/diagnostic/initController.ts:39 -#: src/podkop/tabs/diagnostic/initController.ts:40 -#: src/podkop/tabs/diagnostic/initController.ts:41 -#: src/podkop/tabs/diagnostic/initController.ts:42 -#: src/podkop/tabs/diagnostic/initController.ts:43 -#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:7 +#: src\podkop\tabs\diagnostic\initController.ts:39 +#: src\podkop\tabs\diagnostic\initController.ts:40 +#: src\podkop\tabs\diagnostic\initController.ts:41 +#: src\podkop\tabs\diagnostic\initController.ts:42 +#: src\podkop\tabs\diagnostic\initController.ts:43 +#: src\podkop\tabs\diagnostic\initController.ts:44 +#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:7 msgid "unknown" msgstr "" -#: src/podkop/api.ts:40 +#: src\podkop\api.ts:40 msgid "Unknown error" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:240 -#: src/podkop/tabs/dashboard/initController.ts:271 +#: src\podkop\tabs\dashboard\initController.ts:240 +#: src\podkop\tabs\dashboard\initController.ts:271 msgid "Uplink" msgstr "" -#: src/validators/validateProxyUrl.ts:37 +#: src\validators\validateProxyUrl.ts:37 msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" -#: src/validators/validateUrl.ts:17 +#: src\validators\validateUrl.ts:17 msgid "URL must use one of the following protocols:" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:28 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:28 msgid "URLTest" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:134 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:183 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:111 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:160 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:170 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:221 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:147 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:197 msgid "URLTest Tolerance" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:387 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:439 msgid "User Domain List Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:399 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:451 msgid "User Domains" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:425 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:477 msgid "User Domains List" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:467 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:519 msgid "User Subnet List Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:479 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:531 msgid "User Subnets" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:505 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:557 msgid "User Subnets List" msgstr "" -#: src/validators/validateDns.ts:14 -#: src/validators/validateDns.ts:18 -#: src/validators/validateDomain.ts:13 -#: src/validators/validateDomain.ts:30 -#: src/validators/validateHysteriaUrl.ts:120 -#: src/validators/validateIp.ts:8 -#: src/validators/validateOutboundJson.ts:7 -#: src/validators/validatePath.ts:16 -#: src/validators/validateShadowsocksUrl.ts:95 -#: src/validators/validateSocksUrl.ts:80 -#: src/validators/validateSubnet.ts:38 -#: src/validators/validateTrojanUrl.ts:59 -#: src/validators/validateUrl.ts:28 -#: src/validators/validateVlessUrl.ts:108 +#: src\validators\validateDns.ts:14 +#: src\validators\validateDns.ts:18 +#: src\validators\validateDomain.ts:13 +#: src\validators\validateDomain.ts:30 +#: src\validators\validateHysteriaUrl.ts:120 +#: src\validators\validateIp.ts:8 +#: src\validators\validateOutboundJson.ts:7 +#: src\validators\validatePath.ts:16 +#: src\validators\validateShadowsocksUrl.ts:95 +#: src\validators\validateSocksUrl.ts:80 +#: src\validators\validateSubnet.ts:38 +#: src\validators\validateTrojanUrl.ts:59 +#: src\validators\validateUrl.ts:28 +#: src\validators\validateVlessUrl.ts:108 msgid "Valid" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:458 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:537 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:510 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:589 msgid "Validation errors:" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:256 -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:107 +#: src\podkop\tabs\diagnostic\initController.ts:258 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:111 msgid "View logs" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts:31 +#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:31 msgid "Visit Wiki" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:37 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:89 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:112 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:38 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:138 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:161 msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:335 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:387 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:354 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:406 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:256 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:256 msgid "YACD Secret Key" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:127 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:127 msgid "You can select Output Network Interface, by default autodetect" msgstr "" diff --git a/fe-app-podkop/locales/podkop.ru.po b/fe-app-podkop/locales/podkop.ru.po index 77a33a4f..8bd2c236 100644 --- a/fe-app-podkop/locales/podkop.ru.po +++ b/fe-app-podkop/locales/podkop.ru.po @@ -1,15 +1,15 @@ # RU translations for PODKOP package. # Copyright (C) 2026 THE PODKOP'S COPYRIGHT HOLDER # This file is distributed under the same license as the PODKOP package. -# divocatt, 2026. +# yandexru45, 2026. # msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 16:40+0300\n" -"PO-Revision-Date: 2026-05-29 16:40+0300\n" -"Last-Translator: divocatt\n" +"POT-Creation-Date: 2026-06-02 14:25+0300\n" +"PO-Revision-Date: 2026-06-02 14:25+0300\n" +"Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -29,6 +29,12 @@ msgstr "✘ Отключено" msgid "✘ Stopped" msgstr "✘ Остановлен" +msgid "Группировать по странам" +msgstr "" + +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "" + msgid "Active Connections" msgstr "Активные соединения" @@ -227,18 +233,39 @@ msgstr "Введите доменные имена без протоколов, msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса" +msgid "Enter the subscription URL to fetch proxy configurations from your provider" +msgstr "" + msgid "Every 1 minute" msgstr "Каждую минуту" +msgid "Every 12 hours" +msgstr "" + +msgid "Every 3 hours" +msgstr "" + msgid "Every 3 minutes" msgstr "Каждые 3 минуты" +msgid "Every 30 minutes" +msgstr "" + msgid "Every 30 seconds" msgstr "Каждые 30 секунд" msgid "Every 5 minutes" msgstr "Каждые 5 минут" +msgid "Every 6 hours" +msgstr "" + +msgid "Every day" +msgstr "" + +msgid "Every hour" +msgstr "" + msgid "Exclude NTP" msgstr "Исключить NTP" @@ -263,9 +290,18 @@ msgstr "Получить глобальную проверку" msgid "Global check" msgstr "Глобальная проверка" +msgid "How often to automatically update the subscription" +msgstr "" + msgid "HTTP error" msgstr "Ошибка HTTP" +msgid "Install extended" +msgstr "Установить extended" + +msgid "Install stable" +msgstr "Установить stable" + msgid "Interface Monitoring" msgstr "Мониторинг интерфейса" @@ -626,6 +662,9 @@ msgstr "Sing-box" msgid "Sing-box autostart disabled" msgstr "Автостарт sing-box отключен" +msgid "Sing-box core changed, version:" +msgstr "Ядро sing-box изменено, версия:" + msgid "Sing-box installed" msgstr "Sing-box установлен" @@ -665,6 +704,15 @@ msgstr "Запустить podkop" msgid "Stop podkop" msgstr "Остановить podkop" +msgid "Subscription" +msgstr "" + +msgid "Subscription Update Interval" +msgstr "" + +msgid "Subscription URL" +msgstr "" + msgid "Successfully copied!" msgstr "Успешно скопировано!" diff --git a/fe-app-podkop/src/podkop/methods/shell/index.ts b/fe-app-podkop/src/podkop/methods/shell/index.ts index 6b0337a4..24038743 100644 --- a/fe-app-podkop/src/podkop/methods/shell/index.ts +++ b/fe-app-podkop/src/podkop/methods/shell/index.ts @@ -1,5 +1,12 @@ import { callBaseMethod } from './callBaseMethod'; import { ClashAPI, Podkop } from '../../types'; +import { executeShellCommand } from '../../../helpers'; + +interface SingBoxComponentActionResult { + success: boolean; + version?: string; + message?: string; +} export const PodkopShellMethods = { checkDNSAvailable: async () => @@ -86,4 +93,37 @@ export const PodkopShellMethods = { ), subscriptionUpdate: async () => callBaseMethod(Podkop.AvailableMethods.SUBSCRIPTION_UPDATE), + singBoxComponentAction: async ( + action: 'install_extended' | 'install_stable' | 'check_update', + ): Promise => { + const response = await executeShellCommand({ + command: '/usr/bin/podkop', + args: ['component_action', 'sing_box', action], + timeout: 600000, + }); + + if (response.stdout) { + try { + const parsed = JSON.parse( + response.stdout, + ) as SingBoxComponentActionResult; + + return { + success: Boolean(parsed.success), + version: parsed.version, + message: parsed.message, + }; + } catch (_e) { + return { + success: false, + message: response.stdout, + }; + } + } + + return { + success: false, + message: response.stderr || '', + }; + }, }; diff --git a/fe-app-podkop/src/podkop/services/store.service.ts b/fe-app-podkop/src/podkop/services/store.service.ts index 4069240c..ca9db829 100644 --- a/fe-app-podkop/src/podkop/services/store.service.ts +++ b/fe-app-podkop/src/podkop/services/store.service.ts @@ -180,6 +180,7 @@ export interface StoreType { globalCheck: { loading: boolean }; viewLogs: { loading: boolean }; showSingBoxConfig: { loading: boolean }; + singBoxInstall: { loading: boolean }; }; diagnosticsSystemInfo: { loading: boolean; @@ -189,6 +190,7 @@ export interface StoreType { sing_box_version: string; openwrt_version: string; device_model: string; + sing_box_extended: 0 | 1; }; } diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts index c6ea5626..261f1cdf 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts @@ -19,6 +19,7 @@ export const initialDiagnosticStore: Pick< sing_box_version: 'loading', openwrt_version: 'loading', device_model: 'loading', + sing_box_extended: 0, }, diagnosticsActions: { restart: { @@ -45,6 +46,9 @@ export const initialDiagnosticStore: Pick< showSingBoxConfig: { loading: false, }, + singBoxInstall: { + loading: false, + }, }, diagnosticsRunAction: { loading: false }, diagnosticsChecks: [ diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts index 7ef08cdd..3e90e9f9 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts @@ -29,6 +29,7 @@ async function fetchSystemInfo() { diagnosticsSystemInfo: { loading: false, ...systemInfo.data, + sing_box_extended: systemInfo.data.sing_box_extended === 1 ? 1 : 0, }, }); } else { @@ -41,6 +42,7 @@ async function fetchSystemInfo() { sing_box_version: _('unknown'), openwrt_version: _('unknown'), device_model: _('unknown'), + sing_box_extended: 0, }, }); } @@ -314,6 +316,45 @@ async function handleShowSingBoxConfig() { } } +async function handleInstallSingBox() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + singBoxInstall: { loading: true }, + }, + }); + + const isExtended = store.get().diagnosticsSystemInfo.sing_box_extended === 1; + + try { + const result = await PodkopShellMethods.singBoxComponentAction( + isExtended ? 'install_stable' : 'install_extended', + ); + + if (result.success) { + showToast( + _('Sing-box core changed, version: ') + (result.version || ''), + 'success', + ); + } else { + logger.error('[DIAGNOSTIC]', 'handleInstallSingBox - e', result); + showToast(result.message || _('Failed to execute!'), 'error'); + } + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleInstallSingBox - e', e); + showToast(_('Failed to execute!'), 'error'); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + singBoxInstall: { loading: false }, + }, + }); + await fetchSystemInfo(); + } +} + function renderWikiDisclaimerWidget() { const diagnosticsChecks = store.get().diagnosticsChecks; @@ -402,6 +443,15 @@ function renderDiagnosticAvailableActionsWidget() { onClick: handleShowSingBoxConfig, disabled: atLeastOneServiceCommandLoading, }, + singBoxInstall: { + loading: diagnosticsActions.singBoxInstall.loading, + visible: true, + onClick: handleInstallSingBox, + disabled: + atLeastOneServiceCommandLoading || + diagnosticsActions.singBoxInstall.loading, + }, + singBoxExtended: store.get().diagnosticsSystemInfo.sing_box_extended, }); return preserveScrollForPage(() => { @@ -456,7 +506,11 @@ async function onStoreUpdate( renderDiagnosticRunActionWidget(); } - if (diff.diagnosticsActions || diff.servicesInfoWidget) { + if ( + diff.diagnosticsActions || + diff.servicesInfoWidget || + diff.diagnosticsSystemInfo + ) { renderDiagnosticAvailableActionsWidget(); } diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts index f54fd7f3..65ac61c8 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts +++ b/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts @@ -27,6 +27,8 @@ interface IRenderAvailableActionsProps { globalCheck: ActionProps; viewLogs: ActionProps; showSingBoxConfig: ActionProps; + singBoxInstall: ActionProps; + singBoxExtended: 0 | 1; } export function renderAvailableActions({ @@ -38,6 +40,8 @@ export function renderAvailableActions({ globalCheck, viewLogs, showSingBoxConfig, + singBoxInstall, + singBoxExtended, }: IRenderAvailableActionsProps) { return E('div', { class: 'pdk_diagnostic-page__right-bar__actions' }, [ E('b', {}, _('Available actions')), @@ -118,5 +122,14 @@ export function renderAvailableActions({ disabled: showSingBoxConfig.disabled, }), ]), + ...insertIf(singBoxInstall.visible, [ + renderButton({ + onClick: singBoxInstall.onClick, + icon: renderRotateCcwIcon24, + text: singBoxExtended ? _('Install stable') : _('Install extended'), + loading: singBoxInstall.loading, + disabled: singBoxInstall.disabled, + }), + ]), ]); } diff --git a/fe-app-podkop/src/podkop/types.ts b/fe-app-podkop/src/podkop/types.ts index c3f8afc7..f336ab6f 100644 --- a/fe-app-podkop/src/podkop/types.ts +++ b/fe-app-podkop/src/podkop/types.ts @@ -218,6 +218,7 @@ export namespace Podkop { sing_box_version: string; openwrt_version: string; device_model: string; + sing_box_extended: 0 | 1; } export interface GetClashApiProxyLatency { diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index 83fdcb65..11453b9d 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -698,7 +698,35 @@ var PodkopShellMethods = { getSystemInfo: async () => callBaseMethod( Podkop.AvailableMethods.GET_SYSTEM_INFO ), - subscriptionUpdate: async () => callBaseMethod(Podkop.AvailableMethods.SUBSCRIPTION_UPDATE) + subscriptionUpdate: async () => callBaseMethod(Podkop.AvailableMethods.SUBSCRIPTION_UPDATE), + singBoxComponentAction: async (action) => { + const response = await executeShellCommand({ + command: "/usr/bin/podkop", + args: ["component_action", "sing_box", action], + timeout: 6e5 + }); + if (response.stdout) { + try { + const parsed = JSON.parse( + response.stdout + ); + return { + success: Boolean(parsed.success), + version: parsed.version, + message: parsed.message + }; + } catch (_e) { + return { + success: false, + message: response.stdout + }; + } + } + return { + success: false, + message: response.stderr || "" + }; + } }; // src/podkop/methods/custom/getDashboardSections.ts @@ -1223,7 +1251,8 @@ var initialDiagnosticStore = { luci_app_version: "loading", sing_box_version: "loading", openwrt_version: "loading", - device_model: "loading" + device_model: "loading", + sing_box_extended: 0 }, diagnosticsActions: { restart: { @@ -1249,6 +1278,9 @@ var initialDiagnosticStore = { }, showSingBoxConfig: { loading: false + }, + singBoxInstall: { + loading: false } }, diagnosticsRunAction: { loading: false }, @@ -3574,7 +3606,9 @@ function renderAvailableActions({ disable, globalCheck, viewLogs, - showSingBoxConfig + showSingBoxConfig, + singBoxInstall, + singBoxExtended }) { return E("div", { class: "pdk_diagnostic-page__right-bar__actions" }, [ E("b", {}, _("Available actions")), @@ -3654,6 +3688,15 @@ function renderAvailableActions({ loading: showSingBoxConfig.loading, disabled: showSingBoxConfig.disabled }) + ]), + ...insertIf(singBoxInstall.visible, [ + renderButton({ + onClick: singBoxInstall.onClick, + icon: renderRotateCcwIcon24, + text: singBoxExtended ? _("Install stable") : _("Install extended"), + loading: singBoxInstall.loading, + disabled: singBoxInstall.disabled + }) ]) ]); } @@ -4054,7 +4097,8 @@ async function fetchSystemInfo() { store.set({ diagnosticsSystemInfo: { loading: false, - ...systemInfo.data + ...systemInfo.data, + sing_box_extended: systemInfo.data.sing_box_extended === 1 ? 1 : 0 } }); } else { @@ -4066,7 +4110,8 @@ async function fetchSystemInfo() { luci_app_version: _("unknown"), sing_box_version: _("unknown"), openwrt_version: _("unknown"), - device_model: _("unknown") + device_model: _("unknown"), + sing_box_extended: 0 } }); } @@ -4311,6 +4356,41 @@ async function handleShowSingBoxConfig() { }); } } +async function handleInstallSingBox() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + singBoxInstall: { loading: true } + } + }); + const isExtended = store.get().diagnosticsSystemInfo.sing_box_extended === 1; + try { + const result = await PodkopShellMethods.singBoxComponentAction( + isExtended ? "install_stable" : "install_extended" + ); + if (result.success) { + showToast( + _("Sing-box core changed, version: ") + (result.version || ""), + "success" + ); + } else { + logger.error("[DIAGNOSTIC]", "handleInstallSingBox - e", result); + showToast(result.message || _("Failed to execute!"), "error"); + } + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleInstallSingBox - e", e); + showToast(_("Failed to execute!"), "error"); + } finally { + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + singBoxInstall: { loading: false } + } + }); + await fetchSystemInfo(); + } +} function renderWikiDisclaimerWidget() { const diagnosticsChecks = store.get().diagnosticsChecks; function getWikiKind() { @@ -4384,7 +4464,14 @@ function renderDiagnosticAvailableActionsWidget() { visible: true, onClick: handleShowSingBoxConfig, disabled: atLeastOneServiceCommandLoading - } + }, + singBoxInstall: { + loading: diagnosticsActions.singBoxInstall.loading, + visible: true, + onClick: handleInstallSingBox, + disabled: atLeastOneServiceCommandLoading || diagnosticsActions.singBoxInstall.loading + }, + singBoxExtended: store.get().diagnosticsSystemInfo.sing_box_extended }); return preserveScrollForPage(() => { container.replaceChildren(renderedActions); @@ -4427,7 +4514,7 @@ async function onStoreUpdate2(next, prev, diff) { if (diff.diagnosticsRunAction) { renderDiagnosticRunActionWidget(); } - if (diff.diagnosticsActions || diff.servicesInfoWidget) { + if (diff.diagnosticsActions || diff.servicesInfoWidget || diff.diagnosticsSystemInfo) { renderDiagnosticAvailableActionsWidget(); } if (diff.diagnosticsSystemInfo) { diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index 77a33a4f..8bd2c236 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -1,15 +1,15 @@ # RU translations for PODKOP package. # Copyright (C) 2026 THE PODKOP'S COPYRIGHT HOLDER # This file is distributed under the same license as the PODKOP package. -# divocatt, 2026. +# yandexru45, 2026. # msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 16:40+0300\n" -"PO-Revision-Date: 2026-05-29 16:40+0300\n" -"Last-Translator: divocatt\n" +"POT-Creation-Date: 2026-06-02 14:25+0300\n" +"PO-Revision-Date: 2026-06-02 14:25+0300\n" +"Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -29,6 +29,12 @@ msgstr "✘ Отключено" msgid "✘ Stopped" msgstr "✘ Остановлен" +msgid "Группировать по странам" +msgstr "" + +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "" + msgid "Active Connections" msgstr "Активные соединения" @@ -227,18 +233,39 @@ msgstr "Введите доменные имена без протоколов, msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса" +msgid "Enter the subscription URL to fetch proxy configurations from your provider" +msgstr "" + msgid "Every 1 minute" msgstr "Каждую минуту" +msgid "Every 12 hours" +msgstr "" + +msgid "Every 3 hours" +msgstr "" + msgid "Every 3 minutes" msgstr "Каждые 3 минуты" +msgid "Every 30 minutes" +msgstr "" + msgid "Every 30 seconds" msgstr "Каждые 30 секунд" msgid "Every 5 minutes" msgstr "Каждые 5 минут" +msgid "Every 6 hours" +msgstr "" + +msgid "Every day" +msgstr "" + +msgid "Every hour" +msgstr "" + msgid "Exclude NTP" msgstr "Исключить NTP" @@ -263,9 +290,18 @@ msgstr "Получить глобальную проверку" msgid "Global check" msgstr "Глобальная проверка" +msgid "How often to automatically update the subscription" +msgstr "" + msgid "HTTP error" msgstr "Ошибка HTTP" +msgid "Install extended" +msgstr "Установить extended" + +msgid "Install stable" +msgstr "Установить stable" + msgid "Interface Monitoring" msgstr "Мониторинг интерфейса" @@ -626,6 +662,9 @@ msgstr "Sing-box" msgid "Sing-box autostart disabled" msgstr "Автостарт sing-box отключен" +msgid "Sing-box core changed, version:" +msgstr "Ядро sing-box изменено, версия:" + msgid "Sing-box installed" msgstr "Sing-box установлен" @@ -665,6 +704,15 @@ msgstr "Запустить podkop" msgid "Stop podkop" msgstr "Остановить podkop" +msgid "Subscription" +msgstr "" + +msgid "Subscription Update Interval" +msgstr "" + +msgid "Subscription URL" +msgstr "" + msgid "Successfully copied!" msgstr "Успешно скопировано!" diff --git a/luci-app-podkop/po/templates/podkop.pot b/luci-app-podkop/po/templates/podkop.pot index d96b948e..a722424c 100644 --- a/luci-app-podkop/po/templates/podkop.pot +++ b/luci-app-podkop/po/templates/podkop.pot @@ -1,1115 +1,1183 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PODKOP package. -# divocatt <210179590+divocatt@users.noreply.github.com>, 2026. +# yandexru45 , 2026. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-29 13:40+0300\n" -"PO-Revision-Date: 2026-05-29 13:40+0300\n" -"Last-Translator: divocatt <210179590+divocatt@users.noreply.github.com>\n" +"POT-Creation-Date: 2026-06-02 11:25+0300\n" +"PO-Revision-Date: 2026-06-02 11:25+0300\n" +"Last-Translator: yandexru45 \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/podkop/tabs/dashboard/initController.ts:345 +#: src\podkop\tabs\dashboard\initController.ts:345 msgid "✔ Enabled" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:356 +#: src\podkop\tabs\dashboard\initController.ts:356 msgid "✔ Running" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:346 +#: src\podkop\tabs\dashboard\initController.ts:346 msgid "✘ Disabled" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:357 +#: src\podkop\tabs\dashboard\initController.ts:357 msgid "✘ Stopped" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:307 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:127 +msgid "Группировать по странам" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:128 +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "" + +#: src\podkop\tabs\dashboard\initController.ts:307 msgid "Active Connections" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:106 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:106 msgid "Additional marking rules found" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:247 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:247 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:199 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:251 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:444 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:496 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:525 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:577 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:43 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:47 msgid "Available actions" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:65 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:65 msgid "Bootsrap DNS" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:45 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:45 msgid "Bootstrap DNS server" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:58 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:58 msgid "Browser is not using FakeIP" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:57 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:57 msgid "Browser is using FakeIP correctly" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:348 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:348 msgid "Cache File Path" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:362 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:362 msgid "Cache file path cannot be empty" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:27 -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:28 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:27 -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:25 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:27 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:28 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:27 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:25 msgid "Cannot receive checks result" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:15 -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:15 -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:13 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:15 -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:13 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:15 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:15 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:13 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:15 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:13 msgid "Checking, please wait" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getCheckTitle.ts:2 +#: src\podkop\tabs\diagnostic\helpers\getCheckTitle.ts:2 msgid "checks" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getMeta.ts:26 +#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:26 msgid "Checks failed" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getMeta.ts:13 +#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:13 msgid "Checks passed" msgstr "" -#: src/validators/validateSubnet.ts:33 +#: src\validators\validateSubnet.ts:33 msgid "CIDR must be between 0 and 32" msgstr "" -#: src/partials/modal/renderModal.ts:26 +#: src\partials\modal\renderModal.ts:26 msgid "Close" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:299 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:351 msgid "Community Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:335 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:335 msgid "Config File Path" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:27 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:27 msgid "Configuration for Podkop service" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:23 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:23 msgid "Configuration Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:12 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:12 msgid "Connection Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:26 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:26 msgid "Connection URL" msgstr "" -#: src/partials/modal/renderModal.ts:20 +#: src\partials\modal\renderModal.ts:20 msgid "Copy" msgstr "" -#: src/podkop/tabs/dashboard/partials/renderWidget.ts:22 +#: src\podkop\tabs\dashboard\partials\renderWidget.ts:22 msgid "Currently unavailable" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:80 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:80 msgid "Dashboard" msgstr "" -#: src/podkop/tabs/dashboard/partials/renderSections.ts:19 +#: src\podkop\tabs\dashboard\partials\renderSections.ts:19 msgid "Dashboard currently unavailable" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:222 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:222 msgid "Delay in milliseconds before reloading podkop after interface UP" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:229 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:229 msgid "Delay value cannot be empty" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:82 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:82 msgid "DHCP has DNS server" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:65 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:65 msgid "Diagnostics" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:79 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:83 msgid "Disable autostart" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:265 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:265 msgid "Disable QUIC" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:266 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:266 msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:390 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:470 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:442 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:522 msgid "Disabled" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:77 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:77 msgid "DNS on router" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:267 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:15 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:319 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:268 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:16 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:320 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:264 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:12 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:316 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:12 msgid "DNS Protocol Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:68 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:68 msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:277 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:24 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:329 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:24 msgid "DNS Server" msgstr "" -#: src/validators/validateDns.ts:7 +#: src\validators\validateDns.ts:7 msgid "DNS server address cannot be empty" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26 +#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:26 msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:254 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:306 msgid "Domain Resolver" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:326 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:326 msgid "Dont Touch My DHCP!" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:241 -#: src/podkop/tabs/dashboard/initController.ts:275 +#: src\podkop\tabs\dashboard\initController.ts:241 +#: src\podkop\tabs\dashboard\initController.ts:275 msgid "Downlink" msgstr "" -#: src/partials/modal/renderModal.ts:15 +#: src\partials\modal\renderModal.ts:15 msgid "Download" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:288 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:288 msgid "Download Lists via Proxy/VPN" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:297 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:297 msgid "Download Lists via specific proxy section" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:289 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:298 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:289 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:298 msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:391 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:471 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:443 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:523 msgid "Dynamic List" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:89 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:93 msgid "Enable autostart" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:255 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:307 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:691 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:746 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:665 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:717 msgid "Enable Mixed Proxy" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:126 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:126 msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:666 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:718 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:237 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:237 msgid "Enable YACD" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:246 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:246 msgid "Enable YACD WAN Access" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:66 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:67 msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:426 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:478 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:400 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:452 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:480 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:532 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:138 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:90 +msgid "Enter the subscription URL to fetch proxy configurations from your provider" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:187 msgid "Every 1 minute" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:139 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:119 +msgid "Every 12 hours" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:117 +msgid "Every 3 hours" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:188 msgid "Every 3 minutes" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:137 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:115 +msgid "Every 30 minutes" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:186 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:140 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:189 msgid "Every 5 minutes" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:402 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:118 +msgid "Every 6 hours" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:120 +msgid "Every day" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:116 +msgid "Every hour" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:402 msgid "Exclude NTP" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:403 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:403 msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: src/helpers/copyToClipboard.ts:12 +#: src\helpers\copyToClipboard.ts:12 msgid "Failed to copy!" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:227 -#: src/podkop/tabs/diagnostic/initController.ts:231 -#: src/podkop/tabs/diagnostic/initController.ts:261 -#: src/podkop/tabs/diagnostic/initController.ts:265 -#: src/podkop/tabs/diagnostic/initController.ts:302 -#: src/podkop/tabs/diagnostic/initController.ts:306 +#: src\podkop\tabs\diagnostic\initController.ts:229 +#: src\podkop\tabs\diagnostic\initController.ts:233 +#: src\podkop\tabs\diagnostic\initController.ts:263 +#: src\podkop\tabs\diagnostic\initController.ts:267 +#: src\podkop\tabs\diagnostic\initController.ts:304 +#: src\podkop\tabs\diagnostic\initController.ts:308 +#: src\podkop\tabs\diagnostic\initController.ts:342 +#: src\podkop\tabs\diagnostic\initController.ts:346 msgid "Failed to execute!" msgstr "" -#: src/podkop/methods/custom/getDashboardSections.ts:148 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:59 +#: src\podkop\methods\custom\getDashboardSections.ts:150 +#: src\podkop\methods\custom\getDashboardSections.ts:181 +#: src\podkop\methods\custom\getDashboardSections.ts:218 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:59 msgid "Fastest" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:638 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:690 msgid "Fully Routed IPs" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:98 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:102 msgid "Get global check" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:222 +#: src\podkop\tabs\diagnostic\initController.ts:224 msgid "Global check" msgstr "" -#: src/podkop/api.ts:27 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:113 +msgid "How often to automatically update the subscription" +msgstr "" + +#: src\podkop\api.ts:27 msgid "HTTP error" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:189 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:129 +msgid "Install extended" +msgstr "" + +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:129 +msgid "Install stable" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:189 msgid "Interface Monitoring" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:221 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:221 msgid "Interface Monitoring Delay" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:190 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:190 msgid "Interface monitoring for Bad WAN" msgstr "" -#: src/validators/validateDns.ts:23 +#: src\validators\validateDns.ts:23 msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" msgstr "" -#: src/validators/validateDomain.ts:18 -#: src/validators/validateDomain.ts:27 +#: src\validators\validateDomain.ts:18 +#: src\validators\validateDomain.ts:27 msgid "Invalid domain address" msgstr "" -#: src/validators/validateSubnet.ts:11 +#: src\validators\validateSubnet.ts:11 msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" msgstr "" -#: src/validators/validateHysteriaUrl.ts:90 +#: src\validators\validateHysteriaUrl.ts:90 msgid "Invalid HY2 URL: insecure must be 0 or 1" msgstr "" -#: src/validators/validateHysteriaUrl.ts:77 +#: src\validators\validateHysteriaUrl.ts:77 msgid "Invalid HY2 URL: invalid port number" msgstr "" -#: src/validators/validateHysteriaUrl.ts:30 +#: src\validators\validateHysteriaUrl.ts:30 msgid "Invalid HY2 URL: missing credentials/server" msgstr "" -#: src/validators/validateHysteriaUrl.ts:47 +#: src\validators\validateHysteriaUrl.ts:47 msgid "Invalid HY2 URL: missing host" msgstr "" -#: src/validators/validateHysteriaUrl.ts:41 +#: src\validators\validateHysteriaUrl.ts:41 msgid "Invalid HY2 URL: missing host & port" msgstr "" -#: src/validators/validateHysteriaUrl.ts:36 +#: src\validators\validateHysteriaUrl.ts:36 msgid "Invalid HY2 URL: missing password" msgstr "" -#: src/validators/validateHysteriaUrl.ts:50 +#: src\validators\validateHysteriaUrl.ts:50 msgid "Invalid HY2 URL: missing port" msgstr "" -#: src/validators/validateHysteriaUrl.ts:18 +#: src\validators\validateHysteriaUrl.ts:18 msgid "Invalid HY2 URL: must not contain spaces" msgstr "" -#: src/validators/validateHysteriaUrl.ts:12 +#: src\validators\validateHysteriaUrl.ts:12 msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" msgstr "" -#: src/validators/validateHysteriaUrl.ts:108 +#: src\validators\validateHysteriaUrl.ts:108 msgid "Invalid HY2 URL: obfs-password required when obfs is set" msgstr "" -#: src/validators/validateHysteriaUrl.ts:122 +#: src\validators\validateHysteriaUrl.ts:122 msgid "Invalid HY2 URL: parsing failed" msgstr "" -#: src/validators/validateHysteriaUrl.ts:116 +#: src\validators\validateHysteriaUrl.ts:116 msgid "Invalid HY2 URL: sni cannot be empty" msgstr "" -#: src/validators/validateHysteriaUrl.ts:98 +#: src\validators\validateHysteriaUrl.ts:98 msgid "Invalid HY2 URL: unsupported obfs type" msgstr "" -#: src/validators/validateIp.ts:11 +#: src\validators\validateIp.ts:11 msgid "Invalid IP address" msgstr "" -#: src/validators/validateOutboundJson.ts:9 +#: src\validators\validateOutboundJson.ts:9 msgid "Invalid JSON format" msgstr "" -#: src/validators/validatePath.ts:22 +#: src\validators\validatePath.ts:22 msgid "Invalid path format. Path must start with \"/\" and contain valid characters" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:85 +#: src\validators\validateShadowsocksUrl.ts:85 msgid "Invalid port number. Must be between 1 and 65535" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:37 +#: src\validators\validateShadowsocksUrl.ts:37 msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:27 +#: src\validators\validateShadowsocksUrl.ts:27 msgid "Invalid Shadowsocks URL: missing credentials" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:46 +#: src\validators\validateShadowsocksUrl.ts:46 msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:76 +#: src\validators\validateShadowsocksUrl.ts:76 msgid "Invalid Shadowsocks URL: missing port" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:67 +#: src\validators\validateShadowsocksUrl.ts:67 msgid "Invalid Shadowsocks URL: missing server" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:58 +#: src\validators\validateShadowsocksUrl.ts:58 msgid "Invalid Shadowsocks URL: missing server address" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:16 +#: src\validators\validateShadowsocksUrl.ts:16 msgid "Invalid Shadowsocks URL: must not contain spaces" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:8 +#: src\validators\validateShadowsocksUrl.ts:8 msgid "Invalid Shadowsocks URL: must start with ss://" msgstr "" -#: src/validators/validateShadowsocksUrl.ts:91 +#: src\validators\validateShadowsocksUrl.ts:91 msgid "Invalid Shadowsocks URL: parsing failed" msgstr "" -#: src/validators/validateSocksUrl.ts:73 +#: src\validators\validateSocksUrl.ts:73 msgid "Invalid SOCKS URL: invalid host format" msgstr "" -#: src/validators/validateSocksUrl.ts:63 +#: src\validators\validateSocksUrl.ts:63 msgid "Invalid SOCKS URL: invalid port number" msgstr "" -#: src/validators/validateSocksUrl.ts:42 +#: src\validators\validateSocksUrl.ts:42 msgid "Invalid SOCKS URL: missing host and port" msgstr "" -#: src/validators/validateSocksUrl.ts:51 +#: src\validators\validateSocksUrl.ts:51 msgid "Invalid SOCKS URL: missing hostname or IP" msgstr "" -#: src/validators/validateSocksUrl.ts:56 +#: src\validators\validateSocksUrl.ts:56 msgid "Invalid SOCKS URL: missing port" msgstr "" -#: src/validators/validateSocksUrl.ts:34 +#: src\validators\validateSocksUrl.ts:34 msgid "Invalid SOCKS URL: missing username" msgstr "" -#: src/validators/validateSocksUrl.ts:19 +#: src\validators\validateSocksUrl.ts:19 msgid "Invalid SOCKS URL: must not contain spaces" msgstr "" -#: src/validators/validateSocksUrl.ts:10 +#: src\validators\validateSocksUrl.ts:10 msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" msgstr "" -#: src/validators/validateSocksUrl.ts:77 +#: src\validators\validateSocksUrl.ts:77 msgid "Invalid SOCKS URL: parsing failed" msgstr "" -#: src/validators/validateTrojanUrl.ts:15 +#: src\validators\validateTrojanUrl.ts:15 msgid "Invalid Trojan URL: must not contain spaces" msgstr "" -#: src/validators/validateTrojanUrl.ts:8 +#: src\validators\validateTrojanUrl.ts:8 msgid "Invalid Trojan URL: must start with trojan://" msgstr "" -#: src/validators/validateTrojanUrl.ts:56 +#: src\validators\validateTrojanUrl.ts:56 msgid "Invalid Trojan URL: parsing failed" msgstr "" -#: src/validators/validateUrl.ts:8 -#: src/validators/validateUrl.ts:31 +#: src\validators\validateUrl.ts:8 +#: src\validators\validateUrl.ts:31 msgid "Invalid URL format" msgstr "" -#: src/validators/validateVlessUrl.ts:110 +#: src\validators\validateVlessUrl.ts:110 msgid "Invalid VLESS URL: parsing failed" msgstr "" -#: src/validators/validateSubnet.ts:18 +#: src\validators\validateSubnet.ts:18 msgid "IP address 0.0.0.0 is not allowed" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getMeta.ts:20 +#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:20 msgid "Issues detected" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:48 +#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:48 msgid "Latest" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:276 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:276 msgid "List Update Frequency" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:546 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:598 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:569 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:621 msgid "Local Subnet Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:384 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:384 msgid "Log Level" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runDnsCheck.ts:72 +#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:72 msgid "Main DNS" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:311 +#: src\podkop\tabs\dashboard\initController.ts:311 msgid "Memory Usage" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:678 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:730 msgid "Mixed Proxy Port" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:198 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:198 msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:164 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:215 msgid "Must be a number in the range of 50 - 1000" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:208 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:260 msgid "Network Interface" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:105 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:105 msgid "No other marking rules found" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderCheckSection.ts:189 +#: src\podkop\tabs\diagnostic\partials\renderCheckSection.ts:189 msgid "Not implement yet" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:75 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:81 -#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:100 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:75 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:81 +#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:100 msgid "Not responding" msgstr "" -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:55 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:63 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:71 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:79 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:87 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:59 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:67 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:75 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:83 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:91 msgid "Not running" msgstr "" -#: src/helpers/withTimeout.ts:7 +#: src\helpers\withTimeout.ts:7 msgid "Operation timed out" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:29 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:30 msgid "Outbound Config" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:65 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:66 msgid "Outbound Configuration" msgstr "" -#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:38 +#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:38 msgid "Outdated" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:135 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:135 msgid "Output Network Interface" msgstr "" -#: src/validators/validatePath.ts:7 +#: src\validators\validatePath.ts:7 msgid "Path cannot be empty" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:366 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:366 msgid "Path must be absolute (start with /)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:375 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:375 msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:370 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:370 msgid "Path must end with cache.db" msgstr "" -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:103 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:111 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:119 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:127 -#: src/podkop/tabs/diagnostic/diagnostic.store.ts:135 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:107 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:115 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:123 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:131 +#: src\podkop\tabs\diagnostic\diagnostic.store.ts:139 msgid "Pending" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:343 +#: src\podkop\tabs\dashboard\initController.ts:343 msgid "Podkop" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:26 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:26 msgid "Podkop Settings" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:327 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:327 msgid "Podkop will not modify your DHCP configuration" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:36 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:37 msgid "Proxy Configuration URL" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:66 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:66 msgid "Proxy traffic is not routed via FakeIP" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:65 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:65 msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:333 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:385 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:592 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:644 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:615 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:667 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:690 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:745 msgid "Resolve real IP for routing" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:49 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:53 msgid "Restart podkop" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:51 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:51 msgid "Router DNS is not routed through sing-box" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:50 +#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:50 msgid "Router DNS is routed through sing-box" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:413 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:413 msgid "Routing Excluded IPs" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:79 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:79 msgid "Rules mangle counters" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:74 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:74 msgid "Rules mangle exist" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:89 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:89 msgid "Rules mangle output counters" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:84 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:84 msgid "Rules mangle output exist" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:99 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:99 msgid "Rules proxy counters" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:94 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:94 msgid "Rules proxy exist" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderRunAction.ts:15 +#: src\podkop\tabs\diagnostic\partials\renderRunAction.ts:15 msgid "Run Diagnostic" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:352 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:404 msgid "Russia inside restrictions" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:257 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:257 msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:36 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:36 msgid "Sections" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:300 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:352 msgid "Select a predefined list for routing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:13 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:13 msgid "Select between VPN and Proxy connection methods for traffic routing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:13 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:13 msgid "Select DNS protocol to use" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:277 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:277 msgid "Select how often the domain or subnet lists are updated automatically" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:24 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:24 msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:209 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:261 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:278 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:25 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:330 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:25 msgid "Select or enter DNS server address" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:349 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:349 msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:336 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:336 msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:265 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:317 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:388 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:440 msgid "Select the list type for adding custom domains" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:468 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:520 msgid "Select the list type for adding custom subnets" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:385 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:385 msgid "Select the log level for sing-box" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:90 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:90 msgid "Select the network interface from which the traffic will originate" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:136 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:136 msgid "Select the network interface to which the traffic will originate" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:199 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:199 msgid "Select the WAN interfaces to be monitored" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:27 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:27 msgid "Selector" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:88 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:137 msgid "Selector Proxy Links" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:340 +#: src\podkop\tabs\dashboard\initController.ts:340 msgid "Services info" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js:49 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:49 msgid "Settings" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:290 -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:116 +#: src\podkop\tabs\diagnostic\initController.ts:292 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:120 msgid "Show sing-box config" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:354 +#: src\podkop\tabs\dashboard\initController.ts:354 msgid "Sing-box" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:77 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:77 msgid "Sing-box autostart disabled" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:62 +#: src\podkop\tabs\diagnostic\initController.ts:337 +msgid "Sing-box core changed, version:" +msgstr "" + +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:62 msgid "Sing-box installed" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:87 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:87 msgid "Sing-box listening ports" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:82 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:82 msgid "Sing-box process running" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:72 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:72 msgid "Sing-box service exist" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts:67 +#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:67 msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:89 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:89 msgid "Source Network Interface" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:414 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:414 msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:639 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:691 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:593 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:645 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:616 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:668 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:547 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:570 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:599 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:622 msgid "Specify the path to the list file located on the router filesystem" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:69 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:73 msgid "Start podkop" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:59 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:63 msgid "Stop podkop" msgstr "" -#: src/helpers/copyToClipboard.ts:10 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:29 +msgid "Subscription" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:112 +msgid "Subscription Update Interval" +msgstr "" + +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:89 +msgid "Subscription URL" +msgstr "" + +#: src\helpers\copyToClipboard.ts:10 msgid "Successfully copied!" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:304 +#: src\podkop\tabs\dashboard\initController.ts:304 msgid "System info" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts:21 +#: src\podkop\tabs\diagnostic\partials\renderSystemInfo.ts:21 msgid "System information" msgstr "" -#: src/podkop/tabs/diagnostic/checks/runNftCheck.ts:69 +#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:69 msgid "Table exist" msgstr "" -#: src/podkop/tabs/dashboard/partials/renderSections.ts:108 +#: src\podkop\tabs\dashboard\partials\renderSections.ts:108 msgid "Test latency" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:392 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:472 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:444 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:524 msgid "Text List" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:46 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:46 msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:135 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:184 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:148 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:198 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:171 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:222 msgid "The URL used to test server connectivity" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:69 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:69 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:238 +#: src\podkop\tabs\dashboard\initController.ts:238 msgid "Traffic" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:268 +#: src\podkop\tabs\dashboard\initController.ts:268 msgid "Traffic Total" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts:25 +#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:25 msgid "Troubleshooting" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:80 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:80 msgid "TTL must be a positive number" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:75 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:75 msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:269 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:17 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:321 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:198 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:250 msgid "UDP over TCP" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:38 -#: src/podkop/tabs/diagnostic/initController.ts:39 -#: src/podkop/tabs/diagnostic/initController.ts:40 -#: src/podkop/tabs/diagnostic/initController.ts:41 -#: src/podkop/tabs/diagnostic/initController.ts:42 -#: src/podkop/tabs/diagnostic/initController.ts:43 -#: src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts:7 +#: src\podkop\tabs\diagnostic\initController.ts:39 +#: src\podkop\tabs\diagnostic\initController.ts:40 +#: src\podkop\tabs\diagnostic\initController.ts:41 +#: src\podkop\tabs\diagnostic\initController.ts:42 +#: src\podkop\tabs\diagnostic\initController.ts:43 +#: src\podkop\tabs\diagnostic\initController.ts:44 +#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:7 msgid "unknown" msgstr "" -#: src/podkop/api.ts:40 +#: src\podkop\api.ts:40 msgid "Unknown error" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:240 -#: src/podkop/tabs/dashboard/initController.ts:271 +#: src\podkop\tabs\dashboard\initController.ts:240 +#: src\podkop\tabs\dashboard\initController.ts:271 msgid "Uplink" msgstr "" -#: src/validators/validateProxyUrl.ts:37 +#: src\validators\validateProxyUrl.ts:37 msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" -#: src/validators/validateUrl.ts:17 +#: src\validators\validateUrl.ts:17 msgid "URL must use one of the following protocols:" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:28 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:28 msgid "URLTest" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:134 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:183 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:111 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:160 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:170 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:221 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:147 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:197 msgid "URLTest Tolerance" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:387 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:439 msgid "User Domain List Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:399 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:451 msgid "User Domains" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:425 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:477 msgid "User Domains List" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:467 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:519 msgid "User Subnet List Type" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:479 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:531 msgid "User Subnets" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:505 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:557 msgid "User Subnets List" msgstr "" -#: src/validators/validateDns.ts:14 -#: src/validators/validateDns.ts:18 -#: src/validators/validateDomain.ts:13 -#: src/validators/validateDomain.ts:30 -#: src/validators/validateHysteriaUrl.ts:120 -#: src/validators/validateIp.ts:8 -#: src/validators/validateOutboundJson.ts:7 -#: src/validators/validatePath.ts:16 -#: src/validators/validateShadowsocksUrl.ts:95 -#: src/validators/validateSocksUrl.ts:80 -#: src/validators/validateSubnet.ts:38 -#: src/validators/validateTrojanUrl.ts:59 -#: src/validators/validateUrl.ts:28 -#: src/validators/validateVlessUrl.ts:108 +#: src\validators\validateDns.ts:14 +#: src\validators\validateDns.ts:18 +#: src\validators\validateDomain.ts:13 +#: src\validators\validateDomain.ts:30 +#: src\validators\validateHysteriaUrl.ts:120 +#: src\validators\validateIp.ts:8 +#: src\validators\validateOutboundJson.ts:7 +#: src\validators\validatePath.ts:16 +#: src\validators\validateShadowsocksUrl.ts:95 +#: src\validators\validateSocksUrl.ts:80 +#: src\validators\validateSubnet.ts:38 +#: src\validators\validateTrojanUrl.ts:59 +#: src\validators\validateUrl.ts:28 +#: src\validators\validateVlessUrl.ts:108 msgid "Valid" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:458 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:537 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:510 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:589 msgid "Validation errors:" msgstr "" -#: src/podkop/tabs/diagnostic/initController.ts:256 -#: src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts:107 +#: src\podkop\tabs\diagnostic\initController.ts:258 +#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:111 msgid "View logs" msgstr "" -#: src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts:31 +#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:31 msgid "Visit Wiki" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:37 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:89 -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:112 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:38 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:138 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:161 msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:335 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:387 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:354 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:406 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:256 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:256 msgid "YACD Secret Key" msgstr "" -#: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:127 +#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:127 msgid "You can select Output Network Interface, by default autodetect" msgstr "" diff --git a/podkop/files/usr/bin/podkop b/podkop/files/usr/bin/podkop index 0254b42e..736a516f 100755 --- a/podkop/files/usr/bin/podkop +++ b/podkop/files/usr/bin/podkop @@ -20,6 +20,7 @@ check_required_file "$PODKOP_LIB/sing_box_config_manager.sh" check_required_file "$PODKOP_LIB/sing_box_config_facade.sh" check_required_file "$PODKOP_LIB/logging.sh" check_required_file "$PODKOP_LIB/rulesets.sh" +check_required_file "$PODKOP_LIB/updater.sh" . /lib/functions.sh . /lib/config/uci.sh . /lib/functions/network.sh @@ -30,6 +31,7 @@ check_required_file "$PODKOP_LIB/rulesets.sh" . "$PODKOP_LIB/sing_box_config_facade.sh" . "$PODKOP_LIB/logging.sh" . "$PODKOP_LIB/rulesets.sh" +. "$PODKOP_LIB/updater.sh" config_load "$PODKOP_CONFIG" @@ -2840,7 +2842,7 @@ show_system_info() { } get_system_info() { - local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model + local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model sing_box_extended podkop_version="$PODKOP_VERSION" @@ -2860,6 +2862,11 @@ get_system_info() { sing_box_version="not installed" fi + sing_box_extended=0 + if command -v sing-box > /dev/null 2>&1 && is_sing_box_extended "$sing_box_version"; then + sing_box_extended=1 + fi + if [ -f /etc/os-release ]; then openwrt_version=$(grep OPENWRT_RELEASE /etc/os-release | cut -d'"' -f2) [ -z "$openwrt_version" ] && openwrt_version="unknown" @@ -2874,7 +2881,7 @@ get_system_info() { device_model="unknown" fi - echo "{\"podkop_version\": \"$podkop_version\", \"podkop_latest_version\": \"$podkop_latest_version\", \"luci_app_version\": \"$luci_app_version\", \"sing_box_version\": \"$sing_box_version\", \"openwrt_version\": \"$openwrt_version\", \"device_model\": \"$device_model\"}" | jq . + echo "{\"podkop_version\": \"$podkop_version\", \"podkop_latest_version\": \"$podkop_latest_version\", \"luci_app_version\": \"$luci_app_version\", \"sing_box_version\": \"$sing_box_version\", \"sing_box_extended\": $sing_box_extended, \"openwrt_version\": \"$openwrt_version\", \"device_model\": \"$device_model\"}" | jq . } get_sing_box_status() { @@ -3333,7 +3340,7 @@ global_check() { system_info_json=$(get_system_info) if [ -n "$system_info_json" ]; then - local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model + local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model sing_box_extended podkop_version=$(echo "$system_info_json" | jq -r '.podkop_version // "unknown"') podkop_latest_version=$(echo "$system_info_json" | jq -r '.podkop_latest_version // "unknown"') @@ -3669,6 +3676,9 @@ Available commands: get_system_info Get system information in JSON format check_dns_available Check DNS server availability global_check Run global system check + component_action Run component action: + (e.g. sing_box install_extended|install_stable|check_update) + component_action_async Run component_action in background, returns job file path EOF } @@ -3748,6 +3758,13 @@ check_dns_available) global_check) global_check "${2:-}" ;; +component_action) + component_action "$2" "$3" + ;; +component_action_async) + "$0" component_action "$2" "$3" > "/tmp/podkop-component-$$.json" 2>&1 & + echo "{\"success\":true,\"job\":\"/tmp/podkop-component-$$.json\"}" + ;; *) show_help exit 1 diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 3ce3d3e5..7d1e8660 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -609,11 +609,26 @@ get_kernel_version() { get_sing_box_version() { local version="" if command -v sing-box >/dev/null 2>&1; then - version="$(sing-box version 2>/dev/null | head -1 | awk '{print $NF}')" + version="$(sing-box version 2>/dev/null | head -n1 | awk '{print $NF}')" fi echo "${version:-1.0}" } +# Returns 0 if the given (or detected) sing-box version is an "extended" build +# Arguments: +# $1 - optional sing-box version string (defaults to get_sing_box_version) +is_sing_box_extended() { + local version="${1:-}" + + [ -n "$version" ] || version="$(get_sing_box_version)" + + case "$version" in + *extended*) return 0 ;; + esac + + return 1 +} + # Generates a deterministic HWID based on WAN MAC address and device model # Format: xxxx-xxxx-xxxx-xxxx # Same router always produces the same HWID diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index e54da74a..a0884a7c 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -188,7 +188,7 @@ _add_outbound_security() { case "$security" in tls | reality) - local sni insecure alpn fingerprint public_key short_id + local sni insecure alpn fingerprint public_key short_id transport_type sni=$(url_get_query_param "$url" "sni") insecure=$(_get_insecure_query_param_from_url "$url") alpn=$(comma_string_to_json_array "$(url_get_query_param "$url" "alpn")") @@ -196,6 +196,12 @@ _add_outbound_security() { public_key=$(url_get_query_param "$url" "pbk") short_id=$(url_get_query_param "$url" "sid") + # XHTTP transport defaults its ALPN to h2/http/1.1 when none is provided. + transport_type=$(url_get_query_param "$url" "type") + if [ "$transport_type" = "xhttp" ] && [ "$alpn" = "[]" ]; then + alpn='["h2","http/1.1"]' + fi + if [ "$scheme" = "hysteria2" ] || [ "$scheme" = "hy2" ]; then fingerprint="" fi @@ -261,6 +267,20 @@ _add_outbound_transport() { sing_box_cm_set_grpc_transport_for_outbound "$config" "$outbound_tag" "$grpc_service_name" ) ;; + xhttp) + if ! is_sing_box_extended; then + log "XHTTP transport requires sing-box-extended. Install sing-box-extended and retry." "error" + echo "$config" + return 0 + fi + local xhttp_path xhttp_host xhttp_sni xhttp_mode + xhttp_path=$(url_get_query_param "$url" "path") + xhttp_host=$(url_get_query_param "$url" "host") + xhttp_sni=$(url_get_query_param "$url" "sni") + [ -n "$xhttp_host" ] || xhttp_host="$xhttp_sni" + xhttp_mode=$(url_get_query_param "$url" "mode") + config=$(sing_box_cm_set_xhttp_transport_for_outbound "$config" "$outbound_tag" "$xhttp_path" "$xhttp_host" "$xhttp_mode") + ;; *) log "Unknown transport '$transport' detected." "error" ;; @@ -415,6 +435,15 @@ sing_box_cf_add_subscription_outbounds() { continue fi + # XHTTP transport requires sing-box-extended; skip such outbounds otherwise. + local outbound_transport_type + outbound_transport_type=$(echo "$outbound_json" | jq -r '.transport.type // ""' 2>/dev/null) + if [ "$outbound_transport_type" = "xhttp" ] && ! is_sing_box_extended; then + log "Skip unsupported XHTTP outbound (requires sing-box-extended): '$display_name'" "warn" + i=$((i + 1)) + continue + fi + # Keep original tag from the subscription for dashboard readability. preferred_tag=$(echo "$outbound_json" | jq -r '.tag // .remark // "server-'"$i"'"' 2>/dev/null) if [ -z "$preferred_tag" ] || [ "$preferred_tag" = "null" ]; then diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index c1a2943b..635b4312 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -833,6 +833,61 @@ sing_box_cm_set_ws_transport_for_outbound() { )' } +####################################### +# Set XHTTP transport settings for an outbound in a sing-box JSON configuration. +# Requires sing-box-extended on the router. +# Arguments: +# config: string (JSON), sing-box configuration to modify +# tag: string, identifier of the outbound to modify +# path: string, XHTTP path (defaults to "/" if empty) +# host: string, Host header for XHTTP (optional) +# mode: string, XHTTP mode (auto|packet-up|stream-up|stream-one; defaults to "auto") +# Outputs: +# Writes updated JSON configuration to stdout +# Example: +# CONFIG=$(sing_box_cm_set_xhttp_transport_for_outbound "$CONFIG" "vless-xhttp-out" "/path" "example.com" "auto") +####################################### +sing_box_cm_set_xhttp_transport_for_outbound() { + local config="$1" + local tag="$2" + local path="$3" + local host="$4" + local mode="$5" + + case "$mode" in + auto | packet-up | stream-up | stream-one) ;; + *) mode="auto" ;; + esac + + [ -n "$path" ] || path="/" + + echo "$config" | jq \ + --arg tag "$tag" \ + --arg path "$path" \ + --arg host "$host" \ + --arg mode "$mode" \ + '.outbounds |= map( + if .tag == $tag then + . + { + transport: ( + { + type: "xhttp", + mode: $mode, + path: $path, + x_padding_bytes: "100-1000", + no_grpc_header: false, + sc_max_each_post_bytes: 1000000, + sc_min_posts_interval_ms: 30 + } + + (if $host != "" then {host: $host} else {} end) + ) + } + else + . + end + )' +} + ####################################### # Set TLS settings for an outbound in a sing-box JSON configuration. # Arguments: diff --git a/podkop/files/usr/lib/updater.sh b/podkop/files/usr/lib/updater.sh new file mode 100644 index 00000000..fd8f6fbf --- /dev/null +++ b/podkop/files/usr/lib/updater.sh @@ -0,0 +1,368 @@ +# shellcheck shell=ash + +# Runtime updater for sing-box-extended and stock sing-box. +# JSON parsing is done with jq (no ucode, no extra package deps). +# This file is sourced from /usr/bin/podkop, so log() is available. + +SB_EXT_ARCH_SUFFIX="" +UPDATES_SING_BOX_EXTENDED_REPO="shtorm-7/sing-box-extended" + +updates_log() { + local message="$1" + local level="${2:-info}" + + log "Updater: $message" "$level" +} + +# Returns 0 if the system uses musl libc. +updates_system_uses_musl() { + ls /lib/ld-musl-*.so* >/dev/null 2>&1 && return 0 + + ldd --version 2>&1 | grep -qi 'musl' +} + +# Reads a value from /etc/openwrt_release (e.g. DISTRIB_ARCH). +updates_read_openwrt_release_value() { + local key="$1" + + [ -f /etc/openwrt_release ] || return 0 + sed -n "s/^${key}='\(.*\)'/\1/p" /etc/openwrt_release 2>/dev/null | head -n 1 +} + +# Resolves the sing-box-extended release asset arch suffix into SB_EXT_ARCH_SUFFIX. +# Returns 1 if the architecture is unsupported. +updates_resolve_sing_box_extended_arch_suffix() { + local host_arch distrib_arch + + host_arch="$(uname -m 2>/dev/null || true)" + distrib_arch="$(updates_read_openwrt_release_value "DISTRIB_ARCH")" + + case "$distrib_arch" in + *mipsel* | *mipsle*) host_arch="mipsel" ;; + *mips64el* | *mips64le*) host_arch="mips64el" ;; + esac + + case "$host_arch" in + aarch64) SB_EXT_ARCH_SUFFIX="arm64" ;; + armv7*) SB_EXT_ARCH_SUFFIX="armv7" ;; + armv6*) SB_EXT_ARCH_SUFFIX="armv6" ;; + x86_64) SB_EXT_ARCH_SUFFIX="amd64" ;; + i386 | i686) SB_EXT_ARCH_SUFFIX="386" ;; + mips) SB_EXT_ARCH_SUFFIX="mips-softfloat" ;; + mipsel | mipsle) SB_EXT_ARCH_SUFFIX="mipsle-softfloat" ;; + mips64) SB_EXT_ARCH_SUFFIX="mips64" ;; + mips64el | mips64le) SB_EXT_ARCH_SUFFIX="mips64le" ;; + riscv64) SB_EXT_ARCH_SUFFIX="riscv64" ;; + s390x) SB_EXT_ARCH_SUFFIX="s390x" ;; + *) return 1 ;; + esac +} + +# Fetches the sing-box-extended GitHub releases JSON (echoes to stdout). +updates_fetch_sing_box_extended_releases() { + local url response + url="https://api.github.com/repos/${UPDATES_SING_BOX_EXTENDED_REPO}/releases?per_page=30" + + if command -v curl >/dev/null 2>&1; then + response="$(curl -m 15 -sL "$url" 2>/dev/null)" + fi + if [ -z "$response" ] && command -v wget >/dev/null 2>&1; then + response="$(wget -q -O- "$url" 2>/dev/null)" + fi + + [ -n "$response" ] || return 1 + printf '%s' "$response" +} + +# Picks the newest non-draft, non-prerelease, stable (no alpha/beta/rc) tag. +updates_extended_release_tag() { + local json="$1" + + printf '%s' "$json" | jq -r ' + map(select((.draft != true) and (.prerelease != true))) + | map(.tag_name) + | map(select(. != null and . != "")) + | map(select((ascii_downcase | test("alpha|beta|rc")) | not)) + | .[0] // empty + ' 2>/dev/null +} + +# Extracts the release object matching the given tag. +updates_extended_release_object() { + local json="$1" + local tag="$2" + + printf '%s' "$json" | jq -c --arg t "$tag" ' + map(select((.draft != true) and (.prerelease != true) and (.tag_name == $t))) + | .[0] // empty + ' 2>/dev/null +} + +# Resolves the download URL for the matching asset of a release object. +updates_extended_asset_url() { + local rel="$1" + local suffix url + + if updates_system_uses_musl; then + suffix="linux-${SB_EXT_ARCH_SUFFIX}-musl.tar.gz" + url="$(printf '%s' "$rel" | jq -r --arg s "$suffix" ' + .assets // [] + | map(select(.name != null and (.name | endswith($s)))) + | .[0].browser_download_url // empty + ' 2>/dev/null)" + if [ -n "$url" ]; then + printf '%s' "$url" + return 0 + fi + fi + + suffix="linux-${SB_EXT_ARCH_SUFFIX}.tar.gz" + url="$(printf '%s' "$rel" | jq -r --arg s "$suffix" ' + .assets // [] + | map(select(.name != null and (.name | endswith($s)))) + | .[0].browser_download_url // empty + ' 2>/dev/null)" + if [ -n "$url" ]; then + printf '%s' "$url" + return 0 + fi + + return 1 +} + +# Downloads a URL to a file path (curl, fall back to wget). Returns 0 on success. +updates_download_to_file() { + local url="$1" + local dest="$2" + + if command -v curl >/dev/null 2>&1; then + curl -m 120 -fsSL "$url" -o "$dest" && [ -s "$dest" ] && return 0 + fi + if command -v wget >/dev/null 2>&1; then + wget -q -O "$dest" "$url" && [ -s "$dest" ] && return 0 + fi + + return 1 +} + +# Restarts podkop if its init script is present (best-effort). +updates_restart_podkop() { + if [ -x /etc/init.d/podkop ]; then + updates_log "Restarting podkop after component change" + /etc/init.d/podkop restart >/dev/null 2>&1 || true + fi +} + +# Downloads and installs sing-box-extended, replacing /usr/bin/sing-box. +# Echoes a JSON result on stdout. +updates_install_sing_box_extended() { + local tmp_dir archive releases tag rel asset_url + local binary_path cronet_path + local backup_binary backup_cronet new_version + + if ! updates_resolve_sing_box_extended_arch_suffix; then + updates_log "Unsupported architecture for sing-box-extended" "error" + echo "{\"success\":false,\"message\":\"Unsupported architecture for sing-box-extended\"}" + return 1 + fi + + releases="$(updates_fetch_sing_box_extended_releases)" + if [ -z "$releases" ]; then + updates_log "Failed to fetch sing-box-extended releases" "error" + echo "{\"success\":false,\"message\":\"Failed to fetch sing-box-extended releases\"}" + return 1 + fi + + tag="$(updates_extended_release_tag "$releases")" + if [ -z "$tag" ]; then + updates_log "Failed to resolve sing-box-extended release tag" "error" + echo "{\"success\":false,\"message\":\"Failed to resolve sing-box-extended release tag\"}" + return 1 + fi + + rel="$(updates_extended_release_object "$releases" "$tag")" + asset_url="$(updates_extended_asset_url "$rel")" + if [ -z "$asset_url" ]; then + updates_log "Failed to resolve sing-box-extended asset for arch $SB_EXT_ARCH_SUFFIX" "error" + echo "{\"success\":false,\"message\":\"Failed to resolve sing-box-extended asset\"}" + return 1 + fi + + tmp_dir="$(mktemp -d /tmp/podkop-sbext.XXXXXX 2>/dev/null)" + if [ -z "$tmp_dir" ]; then + updates_log "Failed to create temporary directory" "error" + echo "{\"success\":false,\"message\":\"Failed to create temporary directory\"}" + return 1 + fi + + archive="$tmp_dir/sing-box-extended.tar.gz" + updates_log "Downloading sing-box-extended $tag ($SB_EXT_ARCH_SUFFIX)" + if ! updates_download_to_file "$asset_url" "$archive"; then + rm -rf "$tmp_dir" + updates_log "Failed to download sing-box-extended" "error" + echo "{\"success\":false,\"message\":\"Failed to download sing-box-extended\"}" + return 1 + fi + + binary_path="$(tar -tzf "$archive" 2>/dev/null | grep -E '(^|/)sing-box$' | sed -n '1p')" + if [ -z "$binary_path" ]; then + rm -rf "$tmp_dir" + updates_log "sing-box binary not found in archive" "error" + echo "{\"success\":false,\"message\":\"sing-box binary not found in archive\"}" + return 1 + fi + cronet_path="$(tar -tzf "$archive" 2>/dev/null | grep -E '(^|/)libcronet\.so$' | sed -n '1p')" + + backup_binary="" + if [ -e /usr/bin/sing-box ]; then + backup_binary="$tmp_dir/sing-box.backup" + if ! cp -p /usr/bin/sing-box "$backup_binary"; then + rm -rf "$tmp_dir" + updates_log "Failed to backup current sing-box binary" "error" + echo "{\"success\":false,\"message\":\"Failed to backup current sing-box binary\"}" + return 1 + fi + rm -f /usr/bin/sing-box + fi + + backup_cronet="" + if [ -n "$cronet_path" ] && [ -e /usr/lib/libcronet.so ]; then + backup_cronet="$tmp_dir/libcronet.so.backup" + if ! cp -p /usr/lib/libcronet.so "$backup_cronet"; then + [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box + rm -rf "$tmp_dir" + updates_log "Failed to backup current libcronet.so" "error" + echo "{\"success\":false,\"message\":\"Failed to backup current libcronet.so\"}" + return 1 + fi + rm -f /usr/lib/libcronet.so + fi + + if ! tar -xzf "$archive" -O "$binary_path" > /usr/bin/sing-box 2>/dev/null || [ ! -s /usr/bin/sing-box ]; then + rm -f /usr/bin/sing-box + [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box + [ -n "$backup_cronet" ] && mv -f "$backup_cronet" /usr/lib/libcronet.so + rm -rf "$tmp_dir" + updates_log "Failed to extract sing-box-extended binary" "error" + echo "{\"success\":false,\"message\":\"Failed to extract sing-box-extended binary\"}" + return 1 + fi + chmod 0755 /usr/bin/sing-box + + if [ -n "$cronet_path" ]; then + if ! tar -xzf "$archive" -O "$cronet_path" > /usr/lib/libcronet.so 2>/dev/null || [ ! -s /usr/lib/libcronet.so ]; then + rm -f /usr/bin/sing-box /usr/lib/libcronet.so + [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box + [ -n "$backup_cronet" ] && mv -f "$backup_cronet" /usr/lib/libcronet.so + rm -rf "$tmp_dir" + updates_log "Failed to extract libcronet.so" "error" + echo "{\"success\":false,\"message\":\"Failed to extract libcronet.so\"}" + return 1 + fi + chmod 0644 /usr/lib/libcronet.so + fi + + new_version="$(LD_LIBRARY_PATH=/usr/lib /usr/bin/sing-box version 2>/dev/null | head -1 | awk '{print $NF}')" + case "$new_version" in + *extended*) ;; + *) + rm -f /usr/bin/sing-box + [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box + [ -n "$cronet_path" ] && rm -f /usr/lib/libcronet.so + [ -n "$backup_cronet" ] && mv -f "$backup_cronet" /usr/lib/libcronet.so + rm -rf "$tmp_dir" + updates_log "Installed sing-box failed extended validation; previous binary restored" "error" + echo "{\"success\":false,\"message\":\"Installed sing-box failed extended validation; previous binary restored\"}" + return 1 + ;; + esac + + rm -f "$backup_binary" "$backup_cronet" + rm -rf "$tmp_dir" + updates_restart_podkop + updates_log "Installed sing-box-extended $new_version" + echo "{\"success\":true,\"version\":\"$new_version\"}" + return 0 +} + +# Reinstalls the stock (stable) sing-box via the system package manager. +# Echoes a JSON result on stdout. +updates_install_sing_box_stable() { + local new_version + + if command -v apk >/dev/null 2>&1; then + updates_log "Updating apk package lists" + apk update /dev/null 2>&1 || true + updates_log "Installing stable sing-box via apk" + if ! apk add --allow-downgrade sing-box /dev/null 2>&1; then + apk fix sing-box /dev/null 2>&1 || true + fi + elif command -v opkg >/dev/null 2>&1; then + updates_log "Updating opkg package lists" + opkg update /dev/null 2>&1 || true + updates_log "Installing stable sing-box via opkg" + if ! opkg install --force-reinstall --force-downgrade sing-box /dev/null 2>&1; then + opkg install --force-downgrade sing-box /dev/null 2>&1 || true + fi + else + updates_log "No supported package manager (apk/opkg) found" "error" + echo "{\"success\":false,\"message\":\"No supported package manager found\"}" + return 1 + fi + + updates_restart_podkop + new_version="$(get_sing_box_version)" + updates_log "Stable sing-box installed: ${new_version:-unknown}" + echo "{\"success\":true,\"version\":\"$new_version\"}" + return 0 +} + +# Checks whether a newer sing-box-extended release is available. +# Echoes a JSON status (latest|outdated) on stdout. +updates_check_sing_box_extended() { + local current_version releases tag status + + current_version="$(get_sing_box_version)" + + releases="$(updates_fetch_sing_box_extended_releases)" + if [ -z "$releases" ]; then + echo "{\"success\":false,\"message\":\"Failed to fetch sing-box-extended releases\"}" + return 1 + fi + + tag="$(updates_extended_release_tag "$releases")" + if [ -z "$tag" ]; then + echo "{\"success\":false,\"message\":\"Failed to resolve sing-box-extended release tag\"}" + return 1 + fi + + status="outdated" + case "$current_version" in + *"$tag"*) status="latest" ;; + esac + + echo "{\"success\":true,\"current_version\":\"$current_version\",\"latest_version\":\"$tag\",\"status\":\"$status\"}" + return 0 +} + +# Dispatcher for component-related actions. +component_action() { + local component="$1" + local action="$2" + + case "$component:$action" in + sing_box:install_extended) + updates_install_sing_box_extended + ;; + sing_box:install_stable) + updates_install_sing_box_stable + ;; + sing_box:check_update) + updates_check_sing_box_extended + ;; + *) + echo '{"success":false,"message":"Unknown component action"}' + return 1 + ;; + esac +} From 897b93664be5f9eca67c346c903009a52e92e73d Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 17:34:44 +0300 Subject: [PATCH 20/75] rebrand: podkop -> NetShift (v0.8.0) with migration Full project rebrand to reduce association with upstream podkop, per upstream maintainers' request. Renames (git mv, history preserved): - package podkop -> netshift (/usr/bin/netshift, /etc/config/netshift, /etc/init.d/netshift, /etc/netshift state dir, /usr/lib/netshift) - luci-app-podkop -> luci-app-netshift; view namespace view/podkop -> view/netshift - fe-app-podkop -> fe-app-netshift; TS namespace Podkop -> NetShift - nft table PodkopTable -> NetShiftTable; rt_table podkop -> netshift - nft sets, dnsmasq backup keys, cron self-calls, PID/tmp paths - i18n podkop.pot/po -> netshift.pot/po; web title Podkop -> NetShift - build-arg PODKOP_VERSION -> NETSHIFT_VERSION; Dockerfiles + CI workflows Migration (install.sh migrate_from_podkop): on detecting an old podkop install, stops the old service (clean teardown of nft/dnsmasq/rt_tables), copies /etc/config/podkop -> /etc/config/netshift (backup at /etc/config/podkop.bak.pre-netshift), migrates state dir, strips old cron/rt_tables entries, removes old packages, then installs NetShift. Config schema is compatible so the VPN keeps working after migration. Docs: README + TRADEMARK rewritten for NetShift. Intentionally kept: upstream attribution (itdoginfo/podkop), podkop.net docs links, *.podkop.fyi check domains, yandexru45/podkop-evolution repo URL (GitHub repo not renamed yet), maintainer email. Verified: bash -n clean, yarn lint/test (283) pass, main.js idempotent, Docker ipk build produces netshift_v0.8.0 / luci-app-netshift_v0.8.0. --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +- .github/ISSUE_TEMPLATE/config.yml | 4 +- .github/ISSUE_TEMPLATE/feature_request.yml | 6 +- .github/workflows/build.yml | 14 +- .github/workflows/frontend-ci.yml | 8 +- .github/workflows/shellcheck.yml | 12 +- .gitignore | 4 +- Dockerfile-apk | 12 +- Dockerfile-ipk | 12 +- README.md | 62 +- TRADEMARK.md | 38 +- TRADEMARK_RU.md | 38 +- fe-app-netshift/.env.example | 16 + .../.prettierrc | 0 .../distribute-locales.js | 10 +- .../eslint.config.js | 0 .../extract-calls.js | 4 +- .../generate-po.js | 10 +- .../generate-pot.js | 4 +- .../locales/calls.json | 596 ++++----- fe-app-netshift/locales/netshift.pot | 1183 +++++++++++++++++ .../locales/netshift.ru.po | 50 +- .../package.json | 4 +- .../src/constants.ts | 2 +- .../src/helpers/copyToClipboard.ts | 0 .../src/helpers/downloadAsTxt.ts | 0 .../src/helpers/executeShellCommand.ts | 0 .../src/helpers/getClashApiUrl.ts | 0 .../src/helpers/getProxyUrlName.ts | 0 .../src/helpers/index.ts | 0 .../src/helpers/injectGlobalStyles.ts | 0 .../src/helpers/insertIf.ts | 0 .../src/helpers/maskIP.ts | 0 .../src/helpers/normalizeCompiledVersion.ts | 0 .../src/helpers/onMount.ts | 0 .../src/helpers/parseQueryString.ts | 0 .../src/helpers/parseValueList.ts | 0 .../src/helpers/preserveScrollForPage.ts | 0 .../src/helpers/prettyBytes.ts | 0 .../src/helpers/removeVersionPrefix.ts | 0 .../src/helpers/showToast.ts | 0 .../src/helpers/splitProxyString.ts | 0 .../src/helpers/svgEl.ts | 0 .../src/helpers/tests/maskIp.test.js | 0 .../src/helpers/withTimeout.ts | 2 +- .../src/icons/index.ts | 0 .../src/icons/renderBookOpenTextIcon24.ts | 0 .../src/icons/renderCheckIcon24.ts | 0 .../src/icons/renderCircleAlertIcon24.ts | 0 .../src/icons/renderCircleCheckBigIcon24.ts | 0 .../src/icons/renderCircleCheckIcon24.ts | 0 .../src/icons/renderCirclePlayIcon24.ts | 0 .../src/icons/renderCircleSlashIcon24.ts | 0 .../src/icons/renderCircleStopIcon24.ts | 0 .../src/icons/renderCircleXIcon24.ts | 0 .../src/icons/renderCogIcon24.ts | 0 .../src/icons/renderLoaderCircleIcon24.ts | 0 .../src/icons/renderPauseIcon24.ts | 0 .../src/icons/renderPlayIcon24.ts | 0 .../src/icons/renderRotateCcwIcon24.ts | 0 .../src/icons/renderSearchIcon24.ts | 0 .../src/icons/renderSquareChartGanttIcon24.ts | 0 .../src/icons/renderTriangleAlertIcon24.ts | 0 .../src/icons/renderXIcon24.ts | 0 .../src/luci.d.ts | 0 .../src/main.ts | 2 +- .../src/netshift}/api.ts | 0 .../netshift/fetchers/fetchServicesInfo.ts | 32 + .../src/netshift}/fetchers/index.ts | 0 .../src/netshift}/index.ts | 0 .../methods/custom/getClashApiSecret.ts | 0 .../methods/custom/getConfigSections.ts | 5 + .../methods/custom/getDashboardSections.ts | 8 +- .../src/netshift}/methods/custom/index.ts | 2 +- .../methods/fakeip/getFakeIpCheck.ts | 0 .../netshift}/methods/fakeip/getIpCheck.ts | 0 .../src/netshift}/methods/fakeip/index.ts | 0 .../src/netshift}/methods/index.ts | 0 .../netshift}/methods/shell/callBaseMethod.ts | 8 +- .../src/netshift/methods/shell/index.ts | 129 ++ .../src/netshift}/services/core.service.ts | 10 +- .../src/netshift}/services/index.ts | 0 .../src/netshift}/services/logger.service.ts | 0 .../services/netshiftLogWatcher.service.ts | 36 +- .../src/netshift}/services/socket.service.ts | 0 .../src/netshift}/services/store.service.ts | 12 +- .../src/netshift}/services/tab.service.ts | 0 .../src/netshift}/tabs/dashboard/index.ts | 0 .../tabs/dashboard/initController.ts | 16 +- .../tabs/dashboard/partials/index.ts | 0 .../tabs/dashboard/partials/renderSections.ts | 6 +- .../tabs/dashboard/partials/renderWidget.ts | 0 .../src/netshift}/tabs/dashboard/render.ts | 0 .../src/netshift}/tabs/dashboard/styles.ts | 4 +- .../tabs/diagnostic/checks/contstants.ts | 0 .../tabs/diagnostic/checks/runDnsCheck.ts | 4 +- .../tabs/diagnostic/checks/runFakeIPCheck.ts | 4 +- .../tabs/diagnostic/checks/runNftCheck.ts | 4 +- .../diagnostic/checks/runSectionsCheck.ts | 9 +- .../tabs/diagnostic/checks/runSingBoxCheck.ts | 4 +- .../diagnostic/checks/updateCheckStore.ts | 0 .../tabs/diagnostic/diagnostic.store.ts | 4 +- .../tabs/diagnostic/helpers/getCheckTitle.ts | 0 .../tabs/diagnostic/helpers/getMeta.ts | 0 .../helpers/getNetshiftVersionRow.ts | 18 +- .../src/netshift}/tabs/diagnostic/index.ts | 0 .../tabs/diagnostic/initController.ts | 40 +- .../tabs/diagnostic/partials/index.ts | 0 .../partials/renderAvailableActions.ts | 6 +- .../diagnostic/partials/renderCheckSection.ts | 0 .../diagnostic/partials/renderRunAction.ts | 0 .../diagnostic/partials/renderSystemInfo.ts | 0 .../partials/renderWikiDisclaimer.ts | 2 +- .../tabs/diagnostic/renderDiagnostic.ts | 0 .../src/netshift}/tabs/diagnostic/styles.ts | 4 +- .../tests/getNetshiftVersionRow.test.ts | 30 +- .../src/netshift}/tabs/index.ts | 0 .../src/netshift}/types.ts | 28 +- .../src/partials/button/renderButton.ts | 0 .../src/partials/button/styles.ts | 0 .../src/partials/index.ts | 0 .../src/partials/modal/renderModal.ts | 0 .../src/partials/modal/styles.ts | 0 .../src/styles.ts | 8 +- .../src/validators/bulkValidate.ts | 0 .../src/validators/index.ts | 0 .../src/validators/tests/validateDns.test.js | 0 .../validators/tests/validateDomain.test.js | 0 .../tests/validateHysteriaUrl.test.js | 0 .../src/validators/tests/validateIp.test.js | 0 .../src/validators/tests/validatePath.test.js | 0 .../tests/validateShadowsocksUrl.test.js | 0 .../validators/tests/validateSocksUrl.test.js | 0 .../validators/tests/validateSubnet.test.js | 0 .../tests/validateTrojanUrl.test.js | 0 .../src/validators/tests/validateUrl.test.js | 0 .../validators/tests/validateVlessUrl.test.js | 0 .../src/validators/types.ts | 0 .../src/validators/validateDns.ts | 0 .../src/validators/validateDomain.ts | 0 .../src/validators/validateHysteriaUrl.ts | 0 .../src/validators/validateIp.ts | 0 .../src/validators/validateOutboundJson.ts | 0 .../src/validators/validatePath.ts | 0 .../src/validators/validateProxyUrl.ts | 0 .../src/validators/validateShadowsocksUrl.ts | 0 .../src/validators/validateSocksUrl.ts | 0 .../src/validators/validateSubnet.ts | 0 .../src/validators/validateTrojanUrl.ts | 0 .../src/validators/validateUrl.ts | 0 .../src/validators/validateVlessUrl.ts | 0 .../tests/setup/global-mocks.ts | 0 .../tsconfig.json | 0 .../tsup.config.ts | 4 +- .../vitest.config.js | 0 .../watch-upload.js | 12 +- {fe-app-podkop => fe-app-netshift}/yarn.lock | 0 fe-app-podkop/.env.example | 16 - fe-app-podkop/locales/podkop.pot | 1183 ----------------- .../src/podkop/fetchers/fetchServicesInfo.ts | 29 - .../methods/custom/getConfigSections.ts | 5 - .../src/podkop/methods/shell/index.ts | 129 -- install.sh | 204 ++- .../Makefile | 10 +- .../resources/view/netshift}/dashboard.js | 2 +- .../resources/view/netshift}/diagnostic.js | 2 +- .../resources/view/netshift}/main.js | 333 ++--- .../resources/view/netshift/netshift.js | 30 +- .../resources/view/netshift}/section.js | 2 +- .../resources/view/netshift}/settings.js | 10 +- .../msgmerge.sh | 4 +- .../po/ru/netshift.po | 50 +- luci-app-netshift/po/templates/netshift.pot | 1183 +++++++++++++++++ .../root/etc/uci-defaults/50_luci-netshift | 2 +- .../share/luci/menu.d/luci-app-netshift.json | 14 + .../share/rpcd/acl.d/luci-app-netshift.json | 14 +- .../xgettext.sh | 6 +- luci-app-podkop/po/templates/podkop.pot | 1183 ----------------- .../share/luci/menu.d/luci-app-podkop.json | 14 - netshift/Makefile | 64 + .../files/etc/config/netshift | 0 .../files/etc/init.d/netshift | 10 +- .../podkop => netshift/files/usr/bin/netshift | 220 +-- .../files/usr/lib/constants.sh | 16 +- {podkop => netshift}/files/usr/lib/helpers.jq | 0 {podkop => netshift}/files/usr/lib/helpers.sh | 4 +- {podkop => netshift}/files/usr/lib/logging.sh | 2 +- {podkop => netshift}/files/usr/lib/nft.sh | 0 .../files/usr/lib/rulesets.sh | 0 .../files/usr/lib/sing_box_config_facade.sh | 6 +- .../files/usr/lib/sing_box_config_manager.sh | 10 +- {podkop => netshift}/files/usr/lib/updater.sh | 18 +- podkop/Makefile | 64 - 193 files changed, 3775 insertions(+), 3612 deletions(-) create mode 100644 fe-app-netshift/.env.example rename {fe-app-podkop => fe-app-netshift}/.prettierrc (100%) rename {fe-app-podkop => fe-app-netshift}/distribute-locales.js (80%) rename {fe-app-podkop => fe-app-netshift}/eslint.config.js (100%) rename {fe-app-podkop => fe-app-netshift}/extract-calls.js (93%) rename {fe-app-podkop => fe-app-netshift}/generate-po.js (92%) rename {fe-app-podkop => fe-app-netshift}/generate-pot.js (96%) rename {fe-app-podkop => fe-app-netshift}/locales/calls.json (61%) create mode 100644 fe-app-netshift/locales/netshift.pot rename luci-app-podkop/po/ru/podkop.po => fe-app-netshift/locales/netshift.ru.po (96%) rename {fe-app-podkop => fe-app-netshift}/package.json (90%) rename {fe-app-podkop => fe-app-netshift}/src/constants.ts (97%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/copyToClipboard.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/downloadAsTxt.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/executeShellCommand.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/getClashApiUrl.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/getProxyUrlName.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/index.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/injectGlobalStyles.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/insertIf.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/maskIP.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/normalizeCompiledVersion.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/onMount.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/parseQueryString.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/parseValueList.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/preserveScrollForPage.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/prettyBytes.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/removeVersionPrefix.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/showToast.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/splitProxyString.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/svgEl.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/tests/maskIp.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/helpers/withTimeout.ts (94%) rename {fe-app-podkop => fe-app-netshift}/src/icons/index.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderBookOpenTextIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderCheckIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderCircleAlertIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderCircleCheckBigIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderCircleCheckIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderCirclePlayIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderCircleSlashIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderCircleStopIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderCircleXIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderCogIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderLoaderCircleIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderPauseIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderPlayIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderRotateCcwIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderSearchIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderSquareChartGanttIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderTriangleAlertIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/icons/renderXIcon24.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/luci.d.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/main.ts (90%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/api.ts (100%) create mode 100644 fe-app-netshift/src/netshift/fetchers/fetchServicesInfo.ts rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/fetchers/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/methods/custom/getClashApiSecret.ts (100%) create mode 100644 fe-app-netshift/src/netshift/methods/custom/getConfigSections.ts rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/methods/custom/getDashboardSections.ts (97%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/methods/custom/index.ts (86%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/methods/fakeip/getFakeIpCheck.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/methods/fakeip/getIpCheck.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/methods/fakeip/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/methods/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/methods/shell/callBaseMethod.ts (77%) create mode 100644 fe-app-netshift/src/netshift/methods/shell/index.ts rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/services/core.service.ts (71%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/services/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/services/logger.service.ts (100%) rename fe-app-podkop/src/podkop/services/podkopLogWatcher.service.ts => fe-app-netshift/src/netshift/services/netshiftLogWatcher.service.ts (68%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/services/socket.service.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/services/store.service.ts (95%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/services/tab.service.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/dashboard/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/dashboard/initController.ts (95%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/dashboard/partials/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/dashboard/partials/renderSections.ts (96%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/dashboard/partials/renderWidget.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/dashboard/render.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/dashboard/styles.ts (97%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/checks/contstants.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/checks/runDnsCheck.ts (94%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/checks/runFakeIPCheck.ts (93%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/checks/runNftCheck.ts (95%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/checks/runSectionsCheck.ts (92%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/checks/runSingBoxCheck.ts (95%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/checks/updateCheckStore.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/diagnostic.store.ts (97%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/helpers/getCheckTitle.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/helpers/getMeta.ts (100%) rename fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts => fe-app-netshift/src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts (71%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/initController.ts (93%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/partials/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/partials/renderAvailableActions.ts (97%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/partials/renderCheckSection.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/partials/renderRunAction.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/partials/renderSystemInfo.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/partials/renderWikiDisclaimer.ts (94%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/renderDiagnostic.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/diagnostic/styles.ts (98%) rename fe-app-podkop/src/podkop/tabs/diagnostic/tests/getPodkopVersionRow.test.ts => fe-app-netshift/src/netshift/tabs/diagnostic/tests/getNetshiftVersionRow.test.ts (66%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/tabs/index.ts (100%) rename {fe-app-podkop/src/podkop => fe-app-netshift/src/netshift}/types.ts (88%) rename {fe-app-podkop => fe-app-netshift}/src/partials/button/renderButton.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/partials/button/styles.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/partials/index.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/partials/modal/renderModal.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/partials/modal/styles.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/styles.ts (91%) rename {fe-app-podkop => fe-app-netshift}/src/validators/bulkValidate.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/index.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateDns.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateDomain.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateHysteriaUrl.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateIp.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validatePath.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateShadowsocksUrl.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateSocksUrl.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateSubnet.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateTrojanUrl.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateUrl.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/tests/validateVlessUrl.test.js (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/types.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateDns.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateDomain.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateHysteriaUrl.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateIp.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateOutboundJson.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validatePath.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateProxyUrl.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateShadowsocksUrl.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateSocksUrl.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateSubnet.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateTrojanUrl.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateUrl.ts (100%) rename {fe-app-podkop => fe-app-netshift}/src/validators/validateVlessUrl.ts (100%) rename {fe-app-podkop => fe-app-netshift}/tests/setup/global-mocks.ts (100%) rename {fe-app-podkop => fe-app-netshift}/tsconfig.json (100%) rename {fe-app-podkop => fe-app-netshift}/tsup.config.ts (85%) rename {fe-app-podkop => fe-app-netshift}/vitest.config.js (100%) rename {fe-app-podkop => fe-app-netshift}/watch-upload.js (87%) rename {fe-app-podkop => fe-app-netshift}/yarn.lock (100%) delete mode 100644 fe-app-podkop/.env.example delete mode 100644 fe-app-podkop/locales/podkop.pot delete mode 100644 fe-app-podkop/src/podkop/fetchers/fetchServicesInfo.ts delete mode 100644 fe-app-podkop/src/podkop/methods/custom/getConfigSections.ts delete mode 100644 fe-app-podkop/src/podkop/methods/shell/index.ts rename {luci-app-podkop => luci-app-netshift}/Makefile (69%) rename {luci-app-podkop/htdocs/luci-static/resources/view/podkop => luci-app-netshift/htdocs/luci-static/resources/view/netshift}/dashboard.js (91%) rename {luci-app-podkop/htdocs/luci-static/resources/view/podkop => luci-app-netshift/htdocs/luci-static/resources/view/netshift}/diagnostic.js (91%) rename {luci-app-podkop/htdocs/luci-static/resources/view/podkop => luci-app-netshift/htdocs/luci-static/resources/view/netshift}/main.js (93%) rename luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js => luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js (73%) rename {luci-app-podkop/htdocs/luci-static/resources/view/podkop => luci-app-netshift/htdocs/luci-static/resources/view/netshift}/section.js (99%) rename {luci-app-podkop/htdocs/luci-static/resources/view/podkop => luci-app-netshift/htdocs/luci-static/resources/view/netshift}/settings.js (97%) rename {luci-app-podkop => luci-app-netshift}/msgmerge.sh (89%) rename fe-app-podkop/locales/podkop.ru.po => luci-app-netshift/po/ru/netshift.po (96%) create mode 100644 luci-app-netshift/po/templates/netshift.pot rename luci-app-podkop/root/etc/uci-defaults/50_luci-podkop => luci-app-netshift/root/etc/uci-defaults/50_luci-netshift (66%) create mode 100644 luci-app-netshift/root/usr/share/luci/menu.d/luci-app-netshift.json rename luci-app-podkop/root/usr/share/rpcd/acl.d/luci-app-podkop.json => luci-app-netshift/root/usr/share/rpcd/acl.d/luci-app-netshift.json (55%) rename {luci-app-podkop => luci-app-netshift}/xgettext.sh (82%) delete mode 100644 luci-app-podkop/po/templates/podkop.pot delete mode 100644 luci-app-podkop/root/usr/share/luci/menu.d/luci-app-podkop.json create mode 100644 netshift/Makefile rename podkop/files/etc/config/podkop => netshift/files/etc/config/netshift (100%) rename podkop/files/etc/init.d/podkop => netshift/files/etc/init.d/netshift (88%) rename podkop/files/usr/bin/podkop => netshift/files/usr/bin/netshift (95%) rename {podkop => netshift}/files/usr/lib/constants.sh (89%) rename {podkop => netshift}/files/usr/lib/helpers.jq (100%) rename {podkop => netshift}/files/usr/lib/helpers.sh (99%) rename {podkop => netshift}/files/usr/lib/logging.sh (92%) rename {podkop => netshift}/files/usr/lib/nft.sh (100%) rename {podkop => netshift}/files/usr/lib/rulesets.sh (100%) rename {podkop => netshift}/files/usr/lib/sing_box_config_facade.sh (99%) rename {podkop => netshift}/files/usr/lib/sing_box_config_manager.sh (99%) rename {podkop => netshift}/files/usr/lib/updater.sh (96%) delete mode 100644 podkop/Makefile diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d5df5cc7..e68028f6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,7 +11,7 @@ body: Спасибо за создание отчета об ошибке! Перед отправкой, пожалуйста: - - Проверьте [существующие issues](https://github.com/itdoginfo/podkop/issues) + - Проверьте [существующие issues](https://github.com/yandexru45/podkop-evolution/issues) - Просмотрите [документацию](https://podkop.net) - type: textarea @@ -53,7 +53,7 @@ body: Информация о вашей системе (заполните всё применимое) value: | - **OpenWrt версия**: - - **Podkop версия**: + - **NetShift версия**: - **Роутер модель**: - **Sing-box версия**: render: markdown @@ -68,7 +68,7 @@ body: Релевантные части конфигурации (удалите чувствительную информацию!) placeholder: | Например: - - Содержимое /etc/config/podkop + - Содержимое /etc/config/netshift - Конфигурация sing-box (если релевантно) - Дополнительные конфиги, которые потребуются wireless/network/dhcp и т.д. render: shell \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8b68badd..578d5bb3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: 💬 Если у вас что-то не работает, прежде всего прочитайте README проекта - url: https://github.com/itdoginfo/podkop + url: https://github.com/yandexru45/podkop-evolution about: README проекта - name: 📚 Если вы не нашли в README документацию, то вот ссылка на неё url: https://podkop.net - about: Официальная документация PodKop \ No newline at end of file + about: Официальная документация NetShift \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index d2d15f2a..b9196b95 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ --- name: ✨ Запрос новой функции -description: Предложите новую функцию или улучшение для Podkop +description: Предложите новую функцию или улучшение для NetShift title: "[FEATURE] " labels: ["enhancement", "needs-discussion"] assignees: [] @@ -11,7 +11,7 @@ body: Спасибо за предложение новой функции! Перед отправкой, пожалуйста: - - Проверьте [существующие запросы](https://github.com/itdoginfo/podkop/issues?q=is%3Aissue+label%3Aenhancement) + - Проверьте [существующие запросы](https://github.com/yandexru45/podkop-evolution/issues?q=is%3Aissue+label%3Aenhancement) - Убедитесь, что функции не существует в [документации](https://podkop.net) - type: textarea @@ -40,7 +40,7 @@ body: label: 💡 Предлагаемое решение description: Четкое и краткое описание того, что вы хотите реализовать placeholder: | - Я хочу, чтобы Podkop мог [...] + Я хочу, чтобы NetShift мог [...] Предлагаю добавить функцию, которая [...] Можно было бы улучшить [...] путем [...] validations: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d754a949..fc3c705d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" build: - name: Builder for ${{ matrix.package_type }} podkop and luci-app-podkop + name: Builder for ${{ matrix.package_type }} netshift and luci-app-netshift runs-on: ubuntu-latest needs: preparation strategy: @@ -41,12 +41,12 @@ jobs: with: file: ./Dockerfile-${{ matrix.package_type }} context: . - tags: podkop:ci-${{ matrix.package_type }} + tags: netshift:ci-${{ matrix.package_type }} build-args: | - PODKOP_VERSION=${{ needs.preparation.outputs.version }} + NETSHIFT_VERSION=${{ needs.preparation.outputs.version }} - name: Create ${{ matrix.package_type }} Docker container - run: docker create --name ${{ matrix.package_type }} podkop:ci-${{ matrix.package_type }} + run: docker create --name ${{ matrix.package_type }} netshift:ci-${{ matrix.package_type }} - name: Copy files from ${{ matrix.package_type }} Docker container run: | @@ -73,9 +73,9 @@ jobs: VERSION="${{ needs.preparation.outputs.version }}" mkdir -p ./filtered-bin/${{ matrix.package_type }} - cp ./bin/${{ matrix.package_type }}/luci-i18n-podkop-ru-*.${{ matrix.package_type }} "./filtered-bin/${{ matrix.package_type }}/luci-i18n-podkop-ru-${VERSION}.${{ matrix.package_type }}" - cp ./bin/${{ matrix.package_type }}/podkop-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/ - cp ./bin/${{ matrix.package_type }}/luci-app-podkop-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/ + cp ./bin/${{ matrix.package_type }}/luci-i18n-netshift-ru-*.${{ matrix.package_type }} "./filtered-bin/${{ matrix.package_type }}/luci-i18n-netshift-ru-${VERSION}.${{ matrix.package_type }}" + cp ./bin/${{ matrix.package_type }}/netshift-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/ + cp ./bin/${{ matrix.package_type }}/luci-app-netshift-*.${{ matrix.package_type }} ./filtered-bin/${{ matrix.package_type }}/ - name: Remove Docker container run: docker rm ${{ matrix.package_type }} diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index e1d1f39b..fb154de1 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -3,7 +3,7 @@ name: Frontend CI on: pull_request: paths: - - 'fe-app-podkop/**' + - 'fe-app-netshift/**' - '.github/workflows/frontend-ci.yml' jobs: @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-24.04 defaults: run: - working-directory: fe-app-podkop + working-directory: fe-app-netshift steps: - name: Checkout code @@ -28,14 +28,14 @@ jobs: - name: Get yarn cache directory path id: yarn-cache-dir-path - working-directory: fe-app-podkop + working-directory: fe-app-netshift run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT - name: Cache yarn dependencies uses: actions/cache@v4.3.0 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('fe-app-podkop/yarn.lock') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('fe-app-netshift/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 41cbebc8..6c0b785e 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -7,8 +7,8 @@ on: - 'rc/**' paths: - 'install.sh' - - 'podkop/files/usr/bin/**' - - 'podkop/files/usr/lib/**' + - 'netshift/files/usr/bin/**' + - 'netshift/files/usr/lib/**' - '.github/workflows/shellcheck.yml' pull_request: branches: @@ -16,8 +16,8 @@ on: - 'rc/**' paths: - 'install.sh' - - 'podkop/files/usr/bin/**' - - 'podkop/files/usr/lib/**' + - 'netshift/files/usr/bin/**' + - 'netshift/files/usr/lib/**' - '.github/workflows/shellcheck.yml' permissions: @@ -43,7 +43,7 @@ jobs: with: severity: error include-path: | - podkop/files/usr/bin/podkop - podkop/files/usr/lib/**.sh + netshift/files/usr/bin/netshift + netshift/files/usr/lib/**.sh install.sh token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 4a470951..1c415c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .idea -fe-app-podkop/node_modules -fe-app-podkop/.env +fe-app-netshift/node_modules +fe-app-netshift/.env .DS_Store *.txt diff --git a/Dockerfile-apk b/Dockerfile-apk index 9c880a4a..f96b34d6 100644 --- a/Dockerfile-apk +++ b/Dockerfile-apk @@ -1,11 +1,11 @@ FROM itdoginfo/openwrt-sdk-apk:25.12.3 -ARG PODKOP_VERSION -ENV PODKOP_VERSION=${PODKOP_VERSION} +ARG NETSHIFT_VERSION +ENV NETSHIFT_VERSION=${NETSHIFT_VERSION} -COPY ./podkop /builder/package/feeds/utilities/podkop -COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop +COPY ./netshift /builder/package/feeds/utilities/netshift +COPY ./luci-app-netshift /builder/package/feeds/luci/luci-app-netshift RUN make defconfig && \ - make package/podkop/compile -j4 V=s && \ - make package/luci-app-podkop/compile -j4 V=s \ No newline at end of file + make package/netshift/compile -j4 V=s && \ + make package/luci-app-netshift/compile -j4 V=s diff --git a/Dockerfile-ipk b/Dockerfile-ipk index 3dfac2ab..9b5c38a4 100644 --- a/Dockerfile-ipk +++ b/Dockerfile-ipk @@ -1,11 +1,11 @@ FROM itdoginfo/openwrt-sdk-ipk:24.10.6 -ARG PODKOP_VERSION +ARG NETSHIFT_VERSION -COPY ./podkop /builder/package/feeds/utilities/podkop -COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop +COPY ./netshift /builder/package/feeds/utilities/netshift +COPY ./luci-app-netshift /builder/package/feeds/luci/luci-app-netshift -RUN export PODKOP_VERSION="v${PODKOP_VERSION}" && \ +RUN export NETSHIFT_VERSION="v${NETSHIFT_VERSION}" && \ make defconfig && \ - make package/podkop/compile V=s -j4 && \ - make package/luci-app-podkop/compile V=s -j4 \ No newline at end of file + make package/netshift/compile V=s -j4 && \ + make package/luci-app-netshift/compile V=s -j4 diff --git a/README.md b/README.md index ccdbb5a6..db3976a9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Podkop Evolution +# NetShift -> **Podkop's fork with HWID and Subscription URL support** +> **Форк с поддержкой Subscription URL + HWID и переключаемым ядром sing-box-extended (xhttp)** > -> Этот форк добавляет поддержку ссылок подписки (subscription URL) с кастомными заголовками (HWID, Device-OS, Device-Model) и автоматическим обновлением. Основан на [itdoginfo/podkop](https://github.com/itdoginfo/podkop). +> NetShift добавляет поддержку ссылок подписки (subscription URL) с кастомными заголовками (HWID, Device-OS, Device-Model) и автоматическим обновлением, а также переключение ядра на sing-box-extended с поддержкой клиентского транспорта xhttp. Основан на [itdoginfo/podkop](https://github.com/itdoginfo/podkop). Маршрутизация трафика для OpenWrt. @@ -18,8 +18,8 @@ ### Обновления и конфигурация - При обновлении **обязательно** [очищайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/). - После обновления проверяйте конфигурацию — она может изменяться между версиями. -- При старте Podkop модифицируется конфигурация Dnsmasq. -- Podkop изменяет конфигурацию sing-box. Если вы используете собственную конфигурацию, заранее сохраните её. +- При старте NetShift модифицируется конфигурация Dnsmasq. +- NetShift изменяет конфигурацию sing-box. Если вы используете собственную конфигурацию, заранее сохраните её. ### Системные требования - Требуется OpenWrt 24.10 или выше. @@ -38,7 +38,7 @@ # Документация https://podkop.net/ -# Установка Podkop Evolution +# Установка NetShift Полная информация в [документации](https://podkop.net/docs/install/) Для установки и обновления достаточно выполнить один скрипт: @@ -46,7 +46,7 @@ https://podkop.net/ sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/install.sh) ``` -## Новое в этом форке: Подписки (Subscription) +## Новое в NetShift: Подписки (Subscription) Добавлена поддержка subscription URL — ссылки подписки от провайдера прокси. При выборе типа конфигурации **Subscription** в LuCI: @@ -65,22 +65,44 @@ sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/ref Пример конфигурации через UCI: ``` -uci set podkop.my_sub=section -uci set podkop.my_sub.connection_type='proxy' -uci set podkop.my_sub.proxy_config_type='subscription' -uci set podkop.my_sub.subscription_url='https://your-provider.com/api/sub' -uci set podkop.my_sub.subscription_update_interval='1h' -uci add_list podkop.my_sub.community_lists='russia_inside' -uci commit podkop +uci set netshift.my_sub=section +uci set netshift.my_sub.connection_type='proxy' +uci set netshift.my_sub.proxy_config_type='subscription' +uci set netshift.my_sub.subscription_url='https://your-provider.com/api/sub' +uci set netshift.my_sub.subscription_update_interval='1h' +uci add_list netshift.my_sub.community_lists='russia_inside' +uci commit netshift ``` Ручное обновление подписки: ``` -/usr/bin/podkop subscription_update +/usr/bin/netshift subscription_update ``` +## Новое в NetShift: ядро sing-box-extended (xhttp) + +NetShift позволяет переключать ядро между стабильным sing-box и сборкой +sing-box-extended прямо из вкладки **Diagnostics** в LuCI: + +- **Install extended** — установить расширенное ядро sing-box-extended. +- **Install stable** — вернуться на стабильное ядро sing-box. + +После установки расширенного ядра становится доступен клиентский транспорт +**xhttp**. Поддерживается только клиентский режим xhttp (не серверный). + +## Изменения 0.8.0 — переименование в NetShift +Начиная с версии 0.8.0 проект переименован из `podkop` в **NetShift**. Пакет +теперь называется `netshift` (бинарь `/usr/bin/netshift`), а конфигурация +переехала на `/etc/config/netshift`. LuCI-приложение — `luci-app-netshift`. + +При обновлении старый конфиг `/etc/config/podkop` автоматически мигрируется в +`/etc/config/netshift`, а резервная копия сохраняется в +`/etc/config/podkop.bak.pre-netshift`. + ## Изменения 0.7.0 -Начиная с версии 0.7.0 изменена структура конфига `/etc/config/podkop`. Старые значения несовместимы с новыми. Нужно заново настроить Podkop. +Начиная с версии 0.7.0 изменена структура конфига `/etc/config/netshift` +(на тот момент — `/etc/config/podkop`). Старые значения несовместимы с новыми. +Нужно заново настроить NetShift. Скрипт установки обнаружит старую версию и предупредит вас об этом. Если вы согласитесь, то он сделает автоматически написанное ниже. @@ -89,13 +111,13 @@ uci commit podkop 0. Не ныть в issue и чатик. 1. Забэкапить старый конфиг: ``` -mv /etc/config/podkop /etc/config/podkop-070 +mv /etc/config/netshift /etc/config/netshift-070 ``` 2. Стянуть новый дефолтный конфиг: ``` -wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/podkop/files/etc/config/podkop +wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/netshift/files/etc/config/netshift ``` -3. Настроить заново ваш Podkop через Luci или UCI. +3. Настроить заново ваш NetShift через Luci или UCI. # ToDo @@ -103,7 +125,7 @@ wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-e > Pull Request принимаются только после согласования с авторами в Telegram-чате. На данный момент PR без предварительного обсуждения не рассматриваются. ## Будущее -- [x] [Подписка](https://github.com/itdoginfo/podkop/issues/118) — **реализовано в этом форке!** +- [x] [Подписка](https://github.com/itdoginfo/podkop/issues/118) — **реализовано в NetShift!** - [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне. - [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. [Issue](https://github.com/itdoginfo/podkop/issues/111) - [ ] Галочка, которая режет доступ к doh серверам. diff --git a/TRADEMARK.md b/TRADEMARK.md index f7fbf71b..b949d1a4 100644 --- a/TRADEMARK.md +++ b/TRADEMARK.md @@ -1,51 +1,51 @@ Trademark Guidelines -Version 1.0 dated May 28, 2026 +Version 1.0 dated 2026 -This trademark policy was prepared to help you understand how to use the Podkop trademarks, service marks, and logos in connection with the Podkop open source project and related software. +This trademark policy was prepared to help you understand how to use the NetShift trademarks, service marks, and logos in connection with the NetShift open source project and related software. -While the Podkop software is available under an open source license, that license does not grant permission to use the Podkop trademarks, service marks, or logos. This policy explains acceptable use of the Podkop brand and related marks. +While the NetShift software is available under an open source license, that license does not grant permission to use the NetShift trademarks, service marks, or logos. This policy explains acceptable use of the NetShift brand and related marks. This Policy covers: -1. Our word trademarks and service marks: Podkop -2. Our logos, icons, and other Podkop brand assets +1. Our word trademarks and service marks: NetShift +2. Our logos, icons, and other NetShift brand assets This policy encompasses all trademarks and service marks, whether they are registered or not. ## 1. General Guidelines -Whenever you use one of our marks, you must always do so in a way that does not mislead anyone about what they are getting and from whom. For example, you cannot say you are distributing Podkop software when you are distributing a modified version of it, because recipients may not understand the differences between your modified versions and our own. +Whenever you use one of our marks, you must always do so in a way that does not mislead anyone about what they are getting and from whom. For example, you cannot say you are distributing NetShift software when you are distributing a modified version of it, because recipients may not understand the differences between your modified versions and our own. You also cannot use our logo on your website in a way that suggests that your website is an official website or that we endorse your website. -You can, however, say that you like the Podkop project, that you participate in the Podkop community, or that you are providing an unmodified version of the Podkop software. +You can, however, say that you like the NetShift project, that you participate in the NetShift community, or that you are providing an unmodified version of the NetShift software. You may not use or register our marks, or variations of them as part of your own trademark, service mark, domain name, company name, trade name, product name or service name. Trademark law does not allow your use of names or trademarks that are too similar to ours. You therefore may not use an obvious variation of any of our marks or any phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or compatible product or service. For example, we would consider the following too similar to one of our Marks: -- MyPodkop -- Open-Podkop -- PodkopX -- Podkop Lite -- Podkop Pro +- MyNetShift +- Open-NetShift +- NetShiftX +- NetShift Lite +- NetShift Pro ## 2. Acceptable Uses ### Unmodified Code -When you redistribute an unmodified copy of Podkop software, you must not remove any Podkop trademarks, notices, or branding included in the original distribution. +When you redistribute an unmodified copy of NetShift software, you must not remove any NetShift trademarks, notices, or branding included in the original distribution. ### Modified Code -If you distribute a modified version of Podkop software, you may not use the Podkop name, trademarks, or logos in connection with your modified version, except to accurately describe the origin of the software in factual statements. +If you distribute a modified version of NetShift software, you may not use the NetShift name, trademarks, or logos in connection with your modified version, except to accurately describe the origin of the software in factual statements. -You must replace any Podkop branding, including names displayed in user interfaces, logs, documentation, and other user-facing elements, with your own distinct name and branding, so that your modified version is clearly distinguishable from the original Podkop software. +You must replace any NetShift branding, including names displayed in user interfaces, logs, documentation, and other user-facing elements, with your own distinct name and branding, so that your modified version is clearly distinguishable from the original NetShift software. -You must remove all Podkop logos and any other brand assets from the modified version. +You must remove all NetShift logos and any other brand assets from the modified version. -You may not present your modified version as Podkop or as an official Podkop release, nor may you use the Podkop name in a way that suggests endorsement, affiliation, or official status. +You may not present your modified version as NetShift or as an official NetShift release, nor may you use the NetShift name in a way that suggests endorsement, affiliation, or official status. -You may only refer to Podkop in a factual and descriptive manner, for example: “This software is derived from Podkop open-source software.” +You may only refer to NetShift in a factual and descriptive manner, for example: “This software is derived from NetShift open-source software.” ### Statements about Compatibility @@ -60,4 +60,4 @@ You must not register any domain that includes our word marks or any variant or Always use trademarks in their exact form with correct spelling. They must not be abbreviated, modified, hyphenated, or combined with other words in a way that creates a new product or service name. -Unacceptable: Podcop \ No newline at end of file +Unacceptable: NetShfit diff --git a/TRADEMARK_RU.md b/TRADEMARK_RU.md index f5181578..64186381 100644 --- a/TRADEMARK_RU.md +++ b/TRADEMARK_RU.md @@ -1,52 +1,52 @@ Руководство по использованию товарных знаков -Версия 1.0 от 28 мая 2026 года +Версия 1.0 от 2026 года -Настоящая политика в отношении товарных знаков подготовлена для того, чтобы помочь вам понять, как использовать товарные знаки, знаки обслуживания и логотипы Podkop в связи с открытым исходным кодом проекта Podkop и связанным программным обеспечением. +Настоящая политика в отношении товарных знаков подготовлена для того, чтобы помочь вам понять, как использовать товарные знаки, знаки обслуживания и логотипы NetShift в связи с открытым исходным кодом проекта NetShift и связанным программным обеспечением. -Хотя программное обеспечение Podkop распространяется под лицензией с открытым исходным кодом, эта лицензия не предоставляет разрешения на использование товарных знаков Podkop, знаков обслуживания или логотипов. Данная политика объясняет допустимое использование бренда Podkop и связанных обозначений. +Хотя программное обеспечение NetShift распространяется под лицензией с открытым исходным кодом, эта лицензия не предоставляет разрешения на использование товарных знаков NetShift, знаков обслуживания или логотипов. Данная политика объясняет допустимое использование бренда NetShift и связанных обозначений. Настоящая Политика охватывает: -1. Наши словесные товарные знаки и знаки обслуживания: Podkop -2. Наши логотипы, иконки и другие бренд-активы Podkop +1. Наши словесные товарные знаки и знаки обслуживания: NetShift +2. Наши логотипы, иконки и другие бренд-активы NetShift Данная политика распространяется на все товарные знаки и знаки обслуживания, независимо от того, зарегистрированы они или нет. ## 1. Общие рекомендации -При использовании любого из наших знаков вы всегда должны делать это таким образом, чтобы никого не вводить в заблуждение относительно того, что именно они получают и от кого. Например, вы не можете утверждать, что распространяете программное обеспечение Podkop, если вы распространяете его модифицированную версию, поскольку получатели могут не понимать различий между вашей модифицированной версией и нашей оригинальной. +При использовании любого из наших знаков вы всегда должны делать это таким образом, чтобы никого не вводить в заблуждение относительно того, что именно они получают и от кого. Например, вы не можете утверждать, что распространяете программное обеспечение NetShift, если вы распространяете его модифицированную версию, поскольку получатели могут не понимать различий между вашей модифицированной версией и нашей оригинальной. Вы также не можете использовать наш логотип на своём сайте таким образом, чтобы это создавало впечатление, что ваш сайт является официальным сайтом или что мы одобряем ваш сайт. -Однако вы можете указывать, что вам нравится проект Podkop, что вы участвуете в сообществе Podkop или что вы распространяете немодифицированную версию программного обеспечения Podkop. +Однако вы можете указывать, что вам нравится проект NetShift, что вы участвуете в сообществе NetShift или что вы распространяете немодифицированную версию программного обеспечения NetShift. Вы не имеете права использовать или регистрировать наши знаки, а также их вариации, как часть вашего собственного товарного знака, знака обслуживания, доменного имени, названия компании, коммерческого наименования, названия продукта или услуги. Закон о товарных знаках не допускает использование названий или знаков, которые слишком похожи на наши. Поэтому вы не можете использовать очевидные вариации наших знаков или любые фонетически, иностранно-языковые эквиваленты, производные, аббревиатуры для похожего или совместимого продукта или услуги. Например, мы считаем слишком похожими на наши знаки следующие варианты: -- MyPodkop -- Open-Podkop -- PodkopX -- Podkop Lite -- Podkop Pro +- MyNetShift +- Open-NetShift +- NetShiftX +- NetShift Lite +- NetShift Pro ## 2. Допустимое использование ### Немодифицированный код -При распространении немодифицированной копии программного обеспечения Podkop вы не должны удалять товарные знаки, уведомления или брендинг Podkop, включённые в исходное распространение. +При распространении немодифицированной копии программного обеспечения NetShift вы не должны удалять товарные знаки, уведомления или брендинг NetShift, включённые в исходное распространение. ### Модифицированный код -Если вы распространяете модифицированную версию программного обеспечения Podkop, вы не можете использовать название Podkop, товарные знаки или логотипы в связи с вашей модифицированной версией, за исключением точного описания происхождения программного обеспечения в фактических утверждениях. +Если вы распространяете модифицированную версию программного обеспечения NetShift, вы не можете использовать название NetShift, товарные знаки или логотипы в связи с вашей модифицированной версией, за исключением точного описания происхождения программного обеспечения в фактических утверждениях. -Вы обязаны заменить все элементы брендинга Podkop, включая названия, отображаемые в пользовательском интерфейсе, логах, документации и других пользовательских элементах, на собственное отличительное название и брендинг, чтобы ваша модифицированная версия была явно отличима от оригинального программного обеспечения Podkop. +Вы обязаны заменить все элементы брендинга NetShift, включая названия, отображаемые в пользовательском интерфейсе, логах, документации и других пользовательских элементах, на собственное отличительное название и брендинг, чтобы ваша модифицированная версия была явно отличима от оригинального программного обеспечения NetShift. -Вы должны удалить все логотипы Podkop и любые другие бренд-материалы из модифицированной версии. +Вы должны удалить все логотипы NetShift и любые другие бренд-материалы из модифицированной версии. -Вы не можете представлять вашу модифицированную версию как Podkop или как официальную версию Podkop, а также использовать название Podkop таким образом, чтобы это подразумевало одобрение, аффилированность или официальный статус. +Вы не можете представлять вашу модифицированную версию как NetShift или как официальную версию NetShift, а также использовать название NetShift таким образом, чтобы это подразумевало одобрение, аффилированность или официальный статус. -Вы можете ссылаться на Podkop только в фактическом и описательном контексте, например: «Это программное обеспечение основано на программном обеспечении Podkop с открытым исходным кодом». +Вы можете ссылаться на NetShift только в фактическом и описательном контексте, например: «Это программное обеспечение основано на программном обеспечении NetShift с открытым исходным кодом». ### Упоминания о совместимости @@ -62,4 +62,4 @@ Всегда используйте товарные знаки в их точной форме с корректным написанием. Их нельзя сокращать, изменять, соединять дефисами или объединять с другими словами таким образом, чтобы это создавало новое название продукта или услуги. -Недопустимо: Podcop \ No newline at end of file +Недопустимо: NetShfit diff --git a/fe-app-netshift/.env.example b/fe-app-netshift/.env.example new file mode 100644 index 00000000..4cf8dfa0 --- /dev/null +++ b/fe-app-netshift/.env.example @@ -0,0 +1,16 @@ +SFTP_HOST=192.168.160.129 +SFTP_PORT=22 +SFTP_USER=root +SFTP_PASS= + +# you can use key if needed +# SFTP_PRIVATE_KEY=~/.ssh/id_rsa + +LOCAL_DIR_FE=../luci-app-netshift/htdocs/luci-static/resources/view/netshift +REMOTE_DIR_FE=/www/luci-static/resources/view/netshift + +LOCAL_DIR_BIN=../netshift/files/usr/bin/ +REMOTE_DIR_BIN=/usr/bin/ + +LOCAL_DIR_LIB=../netshift/files/usr/lib/ +REMOTE_DIR_LIB=/usr/lib/netshift/ diff --git a/fe-app-podkop/.prettierrc b/fe-app-netshift/.prettierrc similarity index 100% rename from fe-app-podkop/.prettierrc rename to fe-app-netshift/.prettierrc diff --git a/fe-app-podkop/distribute-locales.js b/fe-app-netshift/distribute-locales.js similarity index 80% rename from fe-app-podkop/distribute-locales.js rename to fe-app-netshift/distribute-locales.js index 0bc27b23..48c303cc 100644 --- a/fe-app-podkop/distribute-locales.js +++ b/fe-app-netshift/distribute-locales.js @@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const sourceDir = path.resolve(__dirname, 'locales'); -const targetRoot = path.resolve(__dirname, '../luci-app-podkop/po'); +const targetRoot = path.resolve(__dirname, '../luci-app-netshift/po'); async function main() { const files = await fs.readdir(sourceDir); @@ -14,17 +14,17 @@ async function main() { for (const file of files) { const filePath = path.join(sourceDir, file); - if (file === 'podkop.pot') { - const potTarget = path.join(targetRoot, 'templates', 'podkop.pot'); + if (file === 'netshift.pot') { + const potTarget = path.join(targetRoot, 'templates', 'netshift.pot'); await fs.mkdir(path.dirname(potTarget), { recursive: true }); await fs.copyFile(filePath, potTarget); console.log(`✅ Copied POT: ${filePath} → ${potTarget}`); } - const match = file.match(/^podkop\.([a-zA-Z_]+)\.po$/); + const match = file.match(/^netshift\.([a-zA-Z_]+)\.po$/); if (match) { const lang = match[1]; - const poTarget = path.join(targetRoot, lang, 'podkop.po'); + const poTarget = path.join(targetRoot, lang, 'netshift.po'); await fs.mkdir(path.dirname(poTarget), { recursive: true }); await fs.copyFile(filePath, poTarget); console.log(`✅ Copied ${lang.toUpperCase()}: ${filePath} → ${poTarget}`); diff --git a/fe-app-podkop/eslint.config.js b/fe-app-netshift/eslint.config.js similarity index 100% rename from fe-app-podkop/eslint.config.js rename to fe-app-netshift/eslint.config.js diff --git a/fe-app-podkop/extract-calls.js b/fe-app-netshift/extract-calls.js similarity index 93% rename from fe-app-podkop/extract-calls.js rename to fe-app-netshift/extract-calls.js index 6b7d765e..0bab3b73 100644 --- a/fe-app-podkop/extract-calls.js +++ b/fe-app-netshift/extract-calls.js @@ -19,12 +19,12 @@ function stripIllegalReturn(code) { const files = await glob([ 'src/**/*.ts', - '../luci-app-podkop/htdocs/luci-static/resources/view/podkop/**/*.js', + '../luci-app-netshift/htdocs/luci-static/resources/view/netshift/**/*.js', ], { ignore: [ '**/*.test.ts', '**/main.js', - '../luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js', + '../luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js', ], absolute: true, }); diff --git a/fe-app-podkop/generate-po.js b/fe-app-netshift/generate-po.js similarity index 92% rename from fe-app-podkop/generate-po.js rename to fe-app-netshift/generate-po.js index 234d6b8e..1859666a 100644 --- a/fe-app-podkop/generate-po.js +++ b/fe-app-netshift/generate-po.js @@ -8,7 +8,7 @@ if (!lang) { } const callsPath = 'locales/calls.json'; -const poPath = `locales/podkop.${lang}.po`; +const poPath = `locales/netshift.${lang}.po`; function getGitUser() { try { @@ -36,14 +36,14 @@ function getHeader(lang) { : 'nplurals=2; plural=(n != 1);'; return [ - `# ${lang.toUpperCase()} translations for PODKOP package.`, - `# Copyright (C) ${now.getFullYear()} THE PODKOP'S COPYRIGHT HOLDER`, - `# This file is distributed under the same license as the PODKOP package.`, + `# ${lang.toUpperCase()} translations for NETSHIFT package.`, + `# Copyright (C) ${now.getFullYear()} THE NETSHIFT'S COPYRIGHT HOLDER`, + `# This file is distributed under the same license as the NETSHIFT package.`, `# ${translator}, ${now.getFullYear()}.`, '#', 'msgid ""', 'msgstr ""', - `"Project-Id-Version: PODKOP\\n"`, + `"Project-Id-Version: NETSHIFT\\n"`, `"Report-Msgid-Bugs-To: \\n"`, `"POT-Creation-Date: ${date} ${time}${tzOffset}\\n"`, `"PO-Revision-Date: ${date} ${time}${tzOffset}\\n"`, diff --git a/fe-app-podkop/generate-pot.js b/fe-app-netshift/generate-pot.js similarity index 96% rename from fe-app-podkop/generate-pot.js rename to fe-app-netshift/generate-pot.js index b7390440..3af0f84a 100644 --- a/fe-app-podkop/generate-pot.js +++ b/fe-app-netshift/generate-pot.js @@ -2,8 +2,8 @@ import fs from 'fs/promises'; import { execSync } from 'child_process'; const inputFile = 'locales/calls.json'; -const outputFile = 'locales/podkop.pot'; -const projectId = 'PODKOP'; +const outputFile = 'locales/netshift.pot'; +const projectId = 'NETSHIFT'; function getGitUser() { const name = execSync('git config user.name').toString().trim(); diff --git a/fe-app-podkop/locales/calls.json b/fe-app-netshift/locales/calls.json similarity index 61% rename from fe-app-podkop/locales/calls.json rename to fe-app-netshift/locales/calls.json index 033db57f..8a57d517 100644 --- a/fe-app-podkop/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -3,175 +3,175 @@ "call": "✔ Enabled", "key": "✔ Enabled", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:345" + "src\\netshift\\tabs\\dashboard\\initController.ts:345" ] }, { "call": "✔ Running", "key": "✔ Running", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:356" + "src\\netshift\\tabs\\dashboard\\initController.ts:356" ] }, { "call": "✘ Disabled", "key": "✘ Disabled", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:346" + "src\\netshift\\tabs\\dashboard\\initController.ts:346" ] }, { "call": "✘ Stopped", "key": "✘ Stopped", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:357" + "src\\netshift\\tabs\\dashboard\\initController.ts:357" ] }, { "call": "Группировать по странам", "key": "Группировать по странам", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:127" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:127" ] }, { "call": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", "key": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:128" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:128" ] }, { "call": "Active Connections", "key": "Active Connections", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:307" + "src\\netshift\\tabs\\dashboard\\initController.ts:307" ] }, { "call": "Additional marking rules found", "key": "Additional marking rules found", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:106" + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:106" ] }, { "call": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "key": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:247" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:247" ] }, { "call": "Applicable for SOCKS and Shadowsocks proxy", "key": "Applicable for SOCKS and Shadowsocks proxy", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:251" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:251" ] }, { "call": "At least one valid domain must be specified. Comments-only content is not allowed.", "key": "At least one valid domain must be specified. Comments-only content is not allowed.", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:496" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:496" ] }, { "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:577" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:577" ] }, { "call": "Available actions", "key": "Available actions", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:47" + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:47" ] }, { "call": "Bootsrap DNS", "key": "Bootsrap DNS", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:65" + "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:65" ] }, { "call": "Bootstrap DNS server", "key": "Bootstrap DNS server", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:45" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:45" ] }, { "call": "Browser is not using FakeIP", "key": "Browser is not using FakeIP", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:58" + "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:58" ] }, { "call": "Browser is using FakeIP correctly", "key": "Browser is using FakeIP correctly", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:57" + "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:57" ] }, { "call": "Cache File Path", "key": "Cache File Path", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:348" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:348" ] }, { "call": "Cache file path cannot be empty", "key": "Cache file path cannot be empty", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:362" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:362" ] }, { "call": "Cannot receive checks result", "key": "Cannot receive checks result", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:27", - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:28", - "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:27", - "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:25" + "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:27", + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:28", + "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:27", + "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:25" ] }, { "call": "Checking, please wait", "key": "Checking, please wait", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:15", - "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:15", - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:13", - "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:15", - "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:13" + "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:15", + "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:15", + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:13", + "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:15", + "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:13" ] }, { "call": "checks", "key": "checks", "places": [ - "src\\podkop\\tabs\\diagnostic\\helpers\\getCheckTitle.ts:2" + "src\\netshift\\tabs\\diagnostic\\helpers\\getCheckTitle.ts:2" ] }, { "call": "Checks failed", "key": "Checks failed", "places": [ - "src\\podkop\\tabs\\diagnostic\\helpers\\getMeta.ts:26" + "src\\netshift\\tabs\\diagnostic\\helpers\\getMeta.ts:26" ] }, { "call": "Checks passed", "key": "Checks passed", "places": [ - "src\\podkop\\tabs\\diagnostic\\helpers\\getMeta.ts:13" + "src\\netshift\\tabs\\diagnostic\\helpers\\getMeta.ts:13" ] }, { @@ -192,42 +192,42 @@ "call": "Community Lists", "key": "Community Lists", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:351" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:351" ] }, { "call": "Config File Path", "key": "Config File Path", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:335" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:335" ] }, { - "call": "Configuration for Podkop service", - "key": "Configuration for Podkop service", + "call": "Configuration for NetShift service", + "key": "Configuration for NetShift service", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:27" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:27" ] }, { "call": "Configuration Type", "key": "Configuration Type", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:23" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:23" ] }, { "call": "Connection Type", "key": "Connection Type", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:12" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:12" ] }, { "call": "Connection URL", "key": "Connection URL", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:26" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:26" ] }, { @@ -241,124 +241,124 @@ "call": "Currently unavailable", "key": "Currently unavailable", "places": [ - "src\\podkop\\tabs\\dashboard\\partials\\renderWidget.ts:22" + "src\\netshift\\tabs\\dashboard\\partials\\renderWidget.ts:22" ] }, { "call": "Dashboard", "key": "Dashboard", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:80" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:80" ] }, { "call": "Dashboard currently unavailable", "key": "Dashboard currently unavailable", "places": [ - "src\\podkop\\tabs\\dashboard\\partials\\renderSections.ts:19" + "src\\netshift\\tabs\\dashboard\\partials\\renderSections.ts:19" ] }, { - "call": "Delay in milliseconds before reloading podkop after interface UP", - "key": "Delay in milliseconds before reloading podkop after interface UP", + "call": "Delay in milliseconds before reloading NetShift after interface UP", + "key": "Delay in milliseconds before reloading NetShift after interface UP", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:222" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:222" ] }, { "call": "Delay value cannot be empty", "key": "Delay value cannot be empty", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:229" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:229" ] }, { "call": "DHCP has DNS server", "key": "DHCP has DNS server", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:82" + "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:82" ] }, { "call": "Diagnostics", "key": "Diagnostics", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:65" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:65" ] }, { "call": "Disable autostart", "key": "Disable autostart", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:83" + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:83" ] }, { "call": "Disable QUIC", "key": "Disable QUIC", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:265" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:265" ] }, { "call": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "key": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:266" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:266" ] }, { "call": "Disabled", "key": "Disabled", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:442", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:522" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:442", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:522" ] }, { "call": "DNS on router", "key": "DNS on router", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:77" + "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:77" ] }, { "call": "DNS over HTTPS (DoH)", "key": "DNS over HTTPS (DoH)", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:319", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:15" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:319", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:15" ] }, { "call": "DNS over TLS (DoT)", "key": "DNS over TLS (DoT)", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:320", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:16" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:320", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:16" ] }, { "call": "DNS Protocol Type", "key": "DNS Protocol Type", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:316", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:12" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:316", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:12" ] }, { "call": "DNS Rewrite TTL", "key": "DNS Rewrite TTL", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:68" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:68" ] }, { "call": "DNS Server", "key": "DNS Server", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:329", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:24" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:329", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:24" ] }, { @@ -372,29 +372,29 @@ "call": "Do not panic, everything can be fixed, just...", "key": "Do not panic, everything can be fixed, just...", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:26" + "src\\netshift\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:26" ] }, { "call": "Domain Resolver", "key": "Domain Resolver", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:306" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:306" ] }, { "call": "Dont Touch My DHCP!", "key": "Dont Touch My DHCP!", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:326" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:326" ] }, { "call": "Downlink", "key": "Downlink", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:241", - "src\\podkop\\tabs\\dashboard\\initController.ts:275" + "src\\netshift\\tabs\\dashboard\\initController.ts:241", + "src\\netshift\\tabs\\dashboard\\initController.ts:275" ] }, { @@ -408,205 +408,205 @@ "call": "Download Lists via Proxy/VPN", "key": "Download Lists via Proxy/VPN", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:288" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:288" ] }, { "call": "Download Lists via specific proxy section", "key": "Download Lists via specific proxy section", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:297" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:297" ] }, { "call": "Downloading all lists via specific Proxy/VPN", "key": "Downloading all lists via specific Proxy/VPN", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:289", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:298" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:289", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:298" ] }, { "call": "Dynamic List", "key": "Dynamic List", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:443", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:523" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:443", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:523" ] }, { "call": "Enable autostart", "key": "Enable autostart", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:93" + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:93" ] }, { "call": "Enable built-in DNS resolver for domains handled by this section", "key": "Enable built-in DNS resolver for domains handled by this section", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:307" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:307" ] }, { "call": "Enable DNS resolve to get real IP when routing", "key": "Enable DNS resolve to get real IP when routing", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:746" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:746" ] }, { "call": "Enable Mixed Proxy", "key": "Enable Mixed Proxy", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:717" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:717" ] }, { "call": "Enable Output Network Interface", "key": "Enable Output Network Interface", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:126" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:126" ] }, { "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:718" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:718" ] }, { "call": "Enable YACD", "key": "Enable YACD", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:237" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:237" ] }, { "call": "Enable YACD WAN Access", "key": "Enable YACD WAN Access", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:246" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:246" ] }, { "call": "Enter complete outbound configuration in JSON format", "key": "Enter complete outbound configuration in JSON format", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:67" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:67" ] }, { "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:478" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:478" ] }, { "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:452" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:452" ] }, { "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:532" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:532" ] }, { "call": "Enter the subscription URL to fetch proxy configurations from your provider", "key": "Enter the subscription URL to fetch proxy configurations from your provider", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:90" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:90" ] }, { "call": "Every 1 minute", "key": "Every 1 minute", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:187" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:187" ] }, { "call": "Every 12 hours", "key": "Every 12 hours", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:119" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:119" ] }, { "call": "Every 3 hours", "key": "Every 3 hours", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:117" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:117" ] }, { "call": "Every 3 minutes", "key": "Every 3 minutes", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:188" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:188" ] }, { "call": "Every 30 minutes", "key": "Every 30 minutes", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:115" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:115" ] }, { "call": "Every 30 seconds", "key": "Every 30 seconds", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:186" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:186" ] }, { "call": "Every 5 minutes", "key": "Every 5 minutes", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:189" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:189" ] }, { "call": "Every 6 hours", "key": "Every 6 hours", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:118" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:118" ] }, { "call": "Every day", "key": "Every day", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:120" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:120" ] }, { "call": "Every hour", "key": "Every hour", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:116" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:116" ] }, { "call": "Exclude NTP", "key": "Exclude NTP", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:402" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:402" ] }, { "call": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "key": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:403" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:403" ] }, { @@ -620,94 +620,94 @@ "call": "Failed to execute!", "key": "Failed to execute!", "places": [ - "src\\podkop\\tabs\\diagnostic\\initController.ts:229", - "src\\podkop\\tabs\\diagnostic\\initController.ts:233", - "src\\podkop\\tabs\\diagnostic\\initController.ts:263", - "src\\podkop\\tabs\\diagnostic\\initController.ts:267", - "src\\podkop\\tabs\\diagnostic\\initController.ts:304", - "src\\podkop\\tabs\\diagnostic\\initController.ts:308", - "src\\podkop\\tabs\\diagnostic\\initController.ts:342", - "src\\podkop\\tabs\\diagnostic\\initController.ts:346" + "src\\netshift\\tabs\\diagnostic\\initController.ts:229", + "src\\netshift\\tabs\\diagnostic\\initController.ts:233", + "src\\netshift\\tabs\\diagnostic\\initController.ts:263", + "src\\netshift\\tabs\\diagnostic\\initController.ts:267", + "src\\netshift\\tabs\\diagnostic\\initController.ts:304", + "src\\netshift\\tabs\\diagnostic\\initController.ts:308", + "src\\netshift\\tabs\\diagnostic\\initController.ts:342", + "src\\netshift\\tabs\\diagnostic\\initController.ts:346" ] }, { "call": "Fastest", "key": "Fastest", "places": [ - "src\\podkop\\methods\\custom\\getDashboardSections.ts:150", - "src\\podkop\\methods\\custom\\getDashboardSections.ts:181", - "src\\podkop\\methods\\custom\\getDashboardSections.ts:218", - "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:59" + "src\\netshift\\methods\\custom\\getDashboardSections.ts:150", + "src\\netshift\\methods\\custom\\getDashboardSections.ts:181", + "src\\netshift\\methods\\custom\\getDashboardSections.ts:218", + "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:58" ] }, { "call": "Fully Routed IPs", "key": "Fully Routed IPs", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:690" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:690" ] }, { "call": "Get global check", "key": "Get global check", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:102" + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:102" ] }, { "call": "Global check", "key": "Global check", "places": [ - "src\\podkop\\tabs\\diagnostic\\initController.ts:224" + "src\\netshift\\tabs\\diagnostic\\initController.ts:224" ] }, { "call": "How often to automatically update the subscription", "key": "How often to automatically update the subscription", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:113" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:113" ] }, { "call": "HTTP error", "key": "HTTP error", "places": [ - "src\\podkop\\api.ts:27" + "src\\netshift\\api.ts:27" ] }, { "call": "Install extended", "key": "Install extended", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:129" + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:129" ] }, { "call": "Install stable", "key": "Install stable", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:129" + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:129" ] }, { "call": "Interface Monitoring", "key": "Interface Monitoring", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:189" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:189" ] }, { "call": "Interface Monitoring Delay", "key": "Interface Monitoring Delay", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:221" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:221" ] }, { "call": "Interface monitoring for Bad WAN", "key": "Interface monitoring for Bad WAN", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:190" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:190" ] }, { @@ -1024,118 +1024,139 @@ "call": "Issues detected", "key": "Issues detected", "places": [ - "src\\podkop\\tabs\\diagnostic\\helpers\\getMeta.ts:20" + "src\\netshift\\tabs\\diagnostic\\helpers\\getMeta.ts:20" ] }, { "call": "Latest", "key": "Latest", "places": [ - "src\\podkop\\tabs\\diagnostic\\helpers\\getPodkopVersionRow.ts:48" + "src\\netshift\\tabs\\diagnostic\\helpers\\getNetshiftVersionRow.ts:48" ] }, { "call": "List Update Frequency", "key": "List Update Frequency", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:276" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:276" ] }, { "call": "Local Domain Lists", "key": "Local Domain Lists", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:598" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:598" ] }, { "call": "Local Subnet Lists", "key": "Local Subnet Lists", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:621" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:621" ] }, { "call": "Log Level", "key": "Log Level", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:384" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:384" ] }, { "call": "Main DNS", "key": "Main DNS", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runDnsCheck.ts:72" + "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:72" ] }, { "call": "Memory Usage", "key": "Memory Usage", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:311" + "src\\netshift\\tabs\\dashboard\\initController.ts:311" ] }, { "call": "Mixed Proxy Port", "key": "Mixed Proxy Port", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:730" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:730" ] }, { "call": "Monitored Interfaces", "key": "Monitored Interfaces", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:198" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:198" ] }, { "call": "Must be a number in the range of 50 - 1000", "key": "Must be a number in the range of 50 - 1000", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:215" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:215" + ] + }, + { + "call": "NetShift", + "key": "NetShift", + "places": [ + "src\\netshift\\tabs\\dashboard\\initController.ts:343" + ] + }, + { + "call": "NetShift Settings", + "key": "NetShift Settings", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:26" + ] + }, + { + "call": "NetShift will not modify your DHCP configuration", + "key": "NetShift will not modify your DHCP configuration", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:327" ] }, { "call": "Network Interface", "key": "Network Interface", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:260" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:260" ] }, { "call": "No other marking rules found", "key": "No other marking rules found", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:105" + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:105" ] }, { "call": "Not implement yet", "key": "Not implement yet", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderCheckSection.ts:189" + "src\\netshift\\tabs\\diagnostic\\partials\\renderCheckSection.ts:189" ] }, { "call": "Not responding", "key": "Not responding", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:75", - "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:81", - "src\\podkop\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:100" + "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:74", + "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:80", + "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:99" ] }, { "call": "Not running", "key": "Not running", "places": [ - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:59", - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:67", - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:75", - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:83", - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:91" + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:59", + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:67", + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:75", + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:83", + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:91" ] }, { @@ -1149,28 +1170,28 @@ "call": "Outbound Config", "key": "Outbound Config", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:30" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:30" ] }, { "call": "Outbound Configuration", "key": "Outbound Configuration", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:66" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:66" ] }, { "call": "Outdated", "key": "Outdated", "places": [ - "src\\podkop\\tabs\\diagnostic\\helpers\\getPodkopVersionRow.ts:38" + "src\\netshift\\tabs\\diagnostic\\helpers\\getNetshiftVersionRow.ts:38" ] }, { "call": "Output Network Interface", "key": "Output Network Interface", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:135" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:135" ] }, { @@ -1184,483 +1205,462 @@ "call": "Path must be absolute (start with /)", "key": "Path must be absolute (start with /)", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:366" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:366" ] }, { "call": "Path must contain at least one directory (like /tmp/cache.db)", "key": "Path must contain at least one directory (like /tmp/cache.db)", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:375" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:375" ] }, { "call": "Path must end with cache.db", "key": "Path must end with cache.db", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:370" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:370" ] }, { "call": "Pending", "key": "Pending", "places": [ - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:107", - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:115", - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:123", - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:131", - "src\\podkop\\tabs\\diagnostic\\diagnostic.store.ts:139" - ] - }, - { - "call": "Podkop", - "key": "Podkop", - "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:343" - ] - }, - { - "call": "Podkop Settings", - "key": "Podkop Settings", - "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:26" - ] - }, - { - "call": "Podkop will not modify your DHCP configuration", - "key": "Podkop will not modify your DHCP configuration", - "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:327" + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:107", + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:115", + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:123", + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:131", + "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:139" ] }, { "call": "Proxy Configuration URL", "key": "Proxy Configuration URL", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:37" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:37" ] }, { "call": "Proxy traffic is not routed via FakeIP", "key": "Proxy traffic is not routed via FakeIP", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:66" + "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:66" ] }, { "call": "Proxy traffic is routed via FakeIP", "key": "Proxy traffic is routed via FakeIP", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:65" + "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:65" ] }, { "call": "Regional options cannot be used together", "key": "Regional options cannot be used together", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:385" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:385" ] }, { "call": "Remote Domain Lists", "key": "Remote Domain Lists", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:644" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:644" ] }, { "call": "Remote Subnet Lists", "key": "Remote Subnet Lists", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:667" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:667" ] }, { "call": "Resolve real IP for routing", "key": "Resolve real IP for routing", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:745" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:745" ] }, { - "call": "Restart podkop", - "key": "Restart podkop", + "call": "Restart NetShift", + "key": "Restart NetShift", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:53" + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:53" ] }, { "call": "Router DNS is not routed through sing-box", "key": "Router DNS is not routed through sing-box", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:51" + "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:51" ] }, { "call": "Router DNS is routed through sing-box", "key": "Router DNS is routed through sing-box", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:50" + "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:50" ] }, { "call": "Routing Excluded IPs", "key": "Routing Excluded IPs", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:413" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:413" ] }, { "call": "Rules mangle counters", "key": "Rules mangle counters", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:79" + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:79" ] }, { "call": "Rules mangle exist", "key": "Rules mangle exist", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:74" + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:74" ] }, { "call": "Rules mangle output counters", "key": "Rules mangle output counters", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:89" + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:89" ] }, { "call": "Rules mangle output exist", "key": "Rules mangle output exist", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:84" + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:84" ] }, { "call": "Rules proxy counters", "key": "Rules proxy counters", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:99" + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:99" ] }, { "call": "Rules proxy exist", "key": "Rules proxy exist", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:94" + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:94" ] }, { "call": "Run Diagnostic", "key": "Run Diagnostic", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderRunAction.ts:15" + "src\\netshift\\tabs\\diagnostic\\partials\\renderRunAction.ts:15" ] }, { "call": "Russia inside restrictions", "key": "Russia inside restrictions", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:404" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:404" ] }, { "call": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "key": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:257" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:257" ] }, { "call": "Sections", "key": "Sections", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:36" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:36" ] }, { "call": "Select a predefined list for routing", "key": "Select a predefined list for routing", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:352" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:352" ] }, { "call": "Select between VPN and Proxy connection methods for traffic routing", "key": "Select between VPN and Proxy connection methods for traffic routing", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:13" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:13" ] }, { "call": "Select DNS protocol to use", "key": "Select DNS protocol to use", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:13" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:13" ] }, { "call": "Select how often the domain or subnet lists are updated automatically", "key": "Select how often the domain or subnet lists are updated automatically", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:277" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:277" ] }, { "call": "Select how to configure the proxy", "key": "Select how to configure the proxy", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:24" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:24" ] }, { "call": "Select network interface for VPN connection", "key": "Select network interface for VPN connection", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:261" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:261" ] }, { "call": "Select or enter DNS server address", "key": "Select or enter DNS server address", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:330", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:25" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:330", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:25" ] }, { "call": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "key": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:349" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:349" ] }, { "call": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "key": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:336" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:336" ] }, { "call": "Select the DNS protocol type for the domain resolver", "key": "Select the DNS protocol type for the domain resolver", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:317" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:317" ] }, { "call": "Select the list type for adding custom domains", "key": "Select the list type for adding custom domains", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:440" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:440" ] }, { "call": "Select the list type for adding custom subnets", "key": "Select the list type for adding custom subnets", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:520" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:520" ] }, { "call": "Select the log level for sing-box", "key": "Select the log level for sing-box", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:385" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:385" ] }, { "call": "Select the network interface from which the traffic will originate", "key": "Select the network interface from which the traffic will originate", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:90" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:90" ] }, { "call": "Select the network interface to which the traffic will originate", "key": "Select the network interface to which the traffic will originate", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:136" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:136" ] }, { "call": "Select the WAN interfaces to be monitored", "key": "Select the WAN interfaces to be monitored", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:199" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:199" ] }, { "call": "Selector", "key": "Selector", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:27" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:27" ] }, { "call": "Selector Proxy Links", "key": "Selector Proxy Links", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:137" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:137" ] }, { "call": "Services info", "key": "Services info", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:340" + "src\\netshift\\tabs\\dashboard\\initController.ts:340" ] }, { "call": "Settings", "key": "Settings", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\podkop.js:49" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:49" ] }, { "call": "Show sing-box config", "key": "Show sing-box config", "places": [ - "src\\podkop\\tabs\\diagnostic\\initController.ts:292", - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:120" + "src\\netshift\\tabs\\diagnostic\\initController.ts:292", + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:120" ] }, { "call": "Sing-box", "key": "Sing-box", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:354" + "src\\netshift\\tabs\\dashboard\\initController.ts:354" ] }, { "call": "Sing-box autostart disabled", "key": "Sing-box autostart disabled", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:77" + "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:77" ] }, { "call": "Sing-box core changed, version:", "key": "Sing-box core changed, version:", "places": [ - "src\\podkop\\tabs\\diagnostic\\initController.ts:337" + "src\\netshift\\tabs\\diagnostic\\initController.ts:337" ] }, { "call": "Sing-box installed", "key": "Sing-box installed", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:62" + "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:62" ] }, { "call": "Sing-box listening ports", "key": "Sing-box listening ports", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:87" + "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:87" ] }, { "call": "Sing-box process running", "key": "Sing-box process running", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:82" + "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:82" ] }, { "call": "Sing-box service exist", "key": "Sing-box service exist", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:72" + "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:72" ] }, { "call": "Sing-box version is compatible (newer than 1.12.4)", "key": "Sing-box version is compatible (newer than 1.12.4)", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:67" + "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:67" ] }, { "call": "Source Network Interface", "key": "Source Network Interface", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:89" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:89" ] }, { "call": "Specify a local IP address to be excluded from routing", "key": "Specify a local IP address to be excluded from routing", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:414" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:414" ] }, { "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:691" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:691" ] }, { "call": "Specify remote URLs to download and use domain lists", "key": "Specify remote URLs to download and use domain lists", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:645" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:645" ] }, { "call": "Specify remote URLs to download and use subnet lists", "key": "Specify remote URLs to download and use subnet lists", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:668" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:668" ] }, { "call": "Specify the path to the list file located on the router filesystem", "key": "Specify the path to the list file located on the router filesystem", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:599", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:622" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:599", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:622" ] }, { - "call": "Start podkop", - "key": "Start podkop", + "call": "Start NetShift", + "key": "Start NetShift", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:73" + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:73" ] }, { - "call": "Stop podkop", - "key": "Stop podkop", + "call": "Stop NetShift", + "key": "Stop NetShift", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:63" + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:63" ] }, { "call": "Subscription", "key": "Subscription", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:29" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:29" ] }, { "call": "Subscription Update Interval", "key": "Subscription Update Interval", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:112" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:112" ] }, { "call": "Subscription URL", "key": "Subscription URL", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:89" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:89" ] }, { @@ -1674,149 +1674,149 @@ "call": "System info", "key": "System info", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:304" + "src\\netshift\\tabs\\dashboard\\initController.ts:304" ] }, { "call": "System information", "key": "System information", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderSystemInfo.ts:21" + "src\\netshift\\tabs\\diagnostic\\partials\\renderSystemInfo.ts:21" ] }, { "call": "Table exist", "key": "Table exist", "places": [ - "src\\podkop\\tabs\\diagnostic\\checks\\runNftCheck.ts:69" + "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:69" ] }, { "call": "Test latency", "key": "Test latency", "places": [ - "src\\podkop\\tabs\\dashboard\\partials\\renderSections.ts:108" + "src\\netshift\\tabs\\dashboard\\partials\\renderSections.ts:108" ] }, { "call": "Text List", "key": "Text List", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:444", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:524" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:444", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:524" ] }, { "call": "The DNS server used to look up the IP address of an upstream DNS server", "key": "The DNS server used to look up the IP address of an upstream DNS server", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:46" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:46" ] }, { "call": "The interval between connectivity tests", "key": "The interval between connectivity tests", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:184" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:184" ] }, { "call": "The maximum difference in response times (ms) allowed when comparing servers", "key": "The maximum difference in response times (ms) allowed when comparing servers", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:198" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:198" ] }, { "call": "The URL used to test server connectivity", "key": "The URL used to test server connectivity", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:222" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:222" ] }, { "call": "Time in seconds for DNS record caching (default: 60)", "key": "Time in seconds for DNS record caching (default: 60)", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:69" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:69" ] }, { "call": "Traffic", "key": "Traffic", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:238" + "src\\netshift\\tabs\\dashboard\\initController.ts:238" ] }, { "call": "Traffic Total", "key": "Traffic Total", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:268" + "src\\netshift\\tabs\\dashboard\\initController.ts:268" ] }, { "call": "Troubleshooting", "key": "Troubleshooting", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:25" + "src\\netshift\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:25" ] }, { "call": "TTL must be a positive number", "key": "TTL must be a positive number", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:80" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:80" ] }, { "call": "TTL value cannot be empty", "key": "TTL value cannot be empty", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:75" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:75" ] }, { "call": "UDP (Unprotected DNS)", "key": "UDP (Unprotected DNS)", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:321", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:17" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:321", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:17" ] }, { "call": "UDP over TCP", "key": "UDP over TCP", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:250" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:250" ] }, { "call": "unknown", "key": "unknown", "places": [ - "src\\podkop\\tabs\\diagnostic\\initController.ts:39", - "src\\podkop\\tabs\\diagnostic\\initController.ts:40", - "src\\podkop\\tabs\\diagnostic\\initController.ts:41", - "src\\podkop\\tabs\\diagnostic\\initController.ts:42", - "src\\podkop\\tabs\\diagnostic\\initController.ts:43", - "src\\podkop\\tabs\\diagnostic\\initController.ts:44", - "src\\podkop\\tabs\\diagnostic\\helpers\\getPodkopVersionRow.ts:7" + "src\\netshift\\tabs\\diagnostic\\initController.ts:39", + "src\\netshift\\tabs\\diagnostic\\initController.ts:40", + "src\\netshift\\tabs\\diagnostic\\initController.ts:41", + "src\\netshift\\tabs\\diagnostic\\initController.ts:42", + "src\\netshift\\tabs\\diagnostic\\initController.ts:43", + "src\\netshift\\tabs\\diagnostic\\initController.ts:44", + "src\\netshift\\tabs\\diagnostic\\helpers\\getNetshiftVersionRow.ts:7" ] }, { "call": "Unknown error", "key": "Unknown error", "places": [ - "src\\podkop\\api.ts:40" + "src\\netshift\\api.ts:40" ] }, { "call": "Uplink", "key": "Uplink", "places": [ - "src\\podkop\\tabs\\dashboard\\initController.ts:240", - "src\\podkop\\tabs\\dashboard\\initController.ts:271" + "src\\netshift\\tabs\\dashboard\\initController.ts:240", + "src\\netshift\\tabs\\dashboard\\initController.ts:271" ] }, { @@ -1837,77 +1837,77 @@ "call": "URLTest", "key": "URLTest", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:28" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:28" ] }, { "call": "URLTest Check Interval", "key": "URLTest Check Interval", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:183" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:183" ] }, { "call": "URLTest Proxy Links", "key": "URLTest Proxy Links", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:160" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:160" ] }, { "call": "URLTest Testing URL", "key": "URLTest Testing URL", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:221" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:221" ] }, { "call": "URLTest Tolerance", "key": "URLTest Tolerance", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:197" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:197" ] }, { "call": "User Domain List Type", "key": "User Domain List Type", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:439" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:439" ] }, { "call": "User Domains", "key": "User Domains", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:451" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:451" ] }, { "call": "User Domains List", "key": "User Domains List", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:477" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:477" ] }, { "call": "User Subnet List Type", "key": "User Subnet List Type", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:519" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:519" ] }, { "call": "User Subnets", "key": "User Subnets", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:531" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:531" ] }, { "call": "User Subnets List", "key": "User Subnets List", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:557" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:557" ] }, { @@ -1934,60 +1934,60 @@ "call": "Validation errors:", "key": "Validation errors:", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:510", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:589" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:510", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:589" ] }, { "call": "View logs", "key": "View logs", "places": [ - "src\\podkop\\tabs\\diagnostic\\initController.ts:258", - "src\\podkop\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:111" + "src\\netshift\\tabs\\diagnostic\\initController.ts:258", + "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:111" ] }, { "call": "Visit Wiki", "key": "Visit Wiki", "places": [ - "src\\podkop\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:31" + "src\\netshift\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:31" ] }, { "call": "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "key": "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:38", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:138", - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:161" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:38", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:138", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:161" ] }, { "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:387" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:387" ] }, { "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\section.js:406" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:406" ] }, { "call": "YACD Secret Key", "key": "YACD Secret Key", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:256" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:256" ] }, { "call": "You can select Output Network Interface, by default autodetect", "key": "You can select Output Network Interface, by default autodetect", "places": [ - "..\\luci-app-podkop\\htdocs\\luci-static\\resources\\view\\podkop\\settings.js:127" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:127" ] } ] \ No newline at end of file diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot new file mode 100644 index 00000000..6e8ed43a --- /dev/null +++ b/fe-app-netshift/locales/netshift.pot @@ -0,0 +1,1183 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the NETSHIFT package. +# yandexru45 , 2026. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: NETSHIFT\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-06-02 14:15+0300\n" +"PO-Revision-Date: 2026-06-02 14:15+0300\n" +"Last-Translator: yandexru45 \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src\netshift\tabs\dashboard\initController.ts:345 +msgid "✔ Enabled" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:356 +msgid "✔ Running" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:346 +msgid "✘ Disabled" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:357 +msgid "✘ Stopped" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:127 +msgid "Группировать по странам" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:128 +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:307 +msgid "Active Connections" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:106 +msgid "Additional marking rules found" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:247 +msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:251 +msgid "Applicable for SOCKS and Shadowsocks proxy" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:496 +msgid "At least one valid domain must be specified. Comments-only content is not allowed." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:577 +msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:47 +msgid "Available actions" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:65 +msgid "Bootsrap DNS" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:45 +msgid "Bootstrap DNS server" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:58 +msgid "Browser is not using FakeIP" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:57 +msgid "Browser is using FakeIP correctly" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:348 +msgid "Cache File Path" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:362 +msgid "Cache file path cannot be empty" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:27 +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:28 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:27 +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:25 +msgid "Cannot receive checks result" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:15 +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:15 +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:13 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:15 +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:13 +msgid "Checking, please wait" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getCheckTitle.ts:2 +msgid "checks" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:26 +msgid "Checks failed" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:13 +msgid "Checks passed" +msgstr "" + +#: src\validators\validateSubnet.ts:33 +msgid "CIDR must be between 0 and 32" +msgstr "" + +#: src\partials\modal\renderModal.ts:26 +msgid "Close" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:351 +msgid "Community Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:335 +msgid "Config File Path" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:27 +msgid "Configuration for NetShift service" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:23 +msgid "Configuration Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:12 +msgid "Connection Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:26 +msgid "Connection URL" +msgstr "" + +#: src\partials\modal\renderModal.ts:20 +msgid "Copy" +msgstr "" + +#: src\netshift\tabs\dashboard\partials\renderWidget.ts:22 +msgid "Currently unavailable" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:80 +msgid "Dashboard" +msgstr "" + +#: src\netshift\tabs\dashboard\partials\renderSections.ts:19 +msgid "Dashboard currently unavailable" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:222 +msgid "Delay in milliseconds before reloading NetShift after interface UP" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:229 +msgid "Delay value cannot be empty" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:82 +msgid "DHCP has DNS server" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:65 +msgid "Diagnostics" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:83 +msgid "Disable autostart" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:265 +msgid "Disable QUIC" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:266 +msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:442 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:522 +msgid "Disabled" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:77 +msgid "DNS on router" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:319 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:15 +msgid "DNS over HTTPS (DoH)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:320 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:16 +msgid "DNS over TLS (DoT)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:316 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:12 +msgid "DNS Protocol Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:68 +msgid "DNS Rewrite TTL" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:329 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:24 +msgid "DNS Server" +msgstr "" + +#: src\validators\validateDns.ts:7 +msgid "DNS server address cannot be empty" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:26 +msgid "Do not panic, everything can be fixed, just..." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:306 +msgid "Domain Resolver" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:326 +msgid "Dont Touch My DHCP!" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:241 +#: src\netshift\tabs\dashboard\initController.ts:275 +msgid "Downlink" +msgstr "" + +#: src\partials\modal\renderModal.ts:15 +msgid "Download" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:288 +msgid "Download Lists via Proxy/VPN" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:297 +msgid "Download Lists via specific proxy section" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:289 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:298 +msgid "Downloading all lists via specific Proxy/VPN" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:443 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:523 +msgid "Dynamic List" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:93 +msgid "Enable autostart" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:307 +msgid "Enable built-in DNS resolver for domains handled by this section" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:746 +msgid "Enable DNS resolve to get real IP when routing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:717 +msgid "Enable Mixed Proxy" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:126 +msgid "Enable Output Network Interface" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:718 +msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:237 +msgid "Enable YACD" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:246 +msgid "Enable YACD WAN Access" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:67 +msgid "Enter complete outbound configuration in JSON format" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:478 +msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:452 +msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:532 +msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:90 +msgid "Enter the subscription URL to fetch proxy configurations from your provider" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:187 +msgid "Every 1 minute" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:119 +msgid "Every 12 hours" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:117 +msgid "Every 3 hours" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:188 +msgid "Every 3 minutes" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:115 +msgid "Every 30 minutes" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:186 +msgid "Every 30 seconds" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:189 +msgid "Every 5 minutes" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:118 +msgid "Every 6 hours" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:120 +msgid "Every day" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:116 +msgid "Every hour" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:402 +msgid "Exclude NTP" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:403 +msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" +msgstr "" + +#: src\helpers\copyToClipboard.ts:12 +msgid "Failed to copy!" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:229 +#: src\netshift\tabs\diagnostic\initController.ts:233 +#: src\netshift\tabs\diagnostic\initController.ts:263 +#: src\netshift\tabs\diagnostic\initController.ts:267 +#: src\netshift\tabs\diagnostic\initController.ts:304 +#: src\netshift\tabs\diagnostic\initController.ts:308 +#: src\netshift\tabs\diagnostic\initController.ts:342 +#: src\netshift\tabs\diagnostic\initController.ts:346 +msgid "Failed to execute!" +msgstr "" + +#: src\netshift\methods\custom\getDashboardSections.ts:150 +#: src\netshift\methods\custom\getDashboardSections.ts:181 +#: src\netshift\methods\custom\getDashboardSections.ts:218 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:58 +msgid "Fastest" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:690 +msgid "Fully Routed IPs" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:102 +msgid "Get global check" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:224 +msgid "Global check" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:113 +msgid "How often to automatically update the subscription" +msgstr "" + +#: src\netshift\api.ts:27 +msgid "HTTP error" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 +msgid "Install extended" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 +msgid "Install stable" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:189 +msgid "Interface Monitoring" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:221 +msgid "Interface Monitoring Delay" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:190 +msgid "Interface monitoring for Bad WAN" +msgstr "" + +#: src\validators\validateDns.ts:23 +msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" +msgstr "" + +#: src\validators\validateDomain.ts:18 +#: src\validators\validateDomain.ts:27 +msgid "Invalid domain address" +msgstr "" + +#: src\validators\validateSubnet.ts:11 +msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:90 +msgid "Invalid HY2 URL: insecure must be 0 or 1" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:77 +msgid "Invalid HY2 URL: invalid port number" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:30 +msgid "Invalid HY2 URL: missing credentials/server" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:47 +msgid "Invalid HY2 URL: missing host" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:41 +msgid "Invalid HY2 URL: missing host & port" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:36 +msgid "Invalid HY2 URL: missing password" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:50 +msgid "Invalid HY2 URL: missing port" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:18 +msgid "Invalid HY2 URL: must not contain spaces" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:12 +msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:108 +msgid "Invalid HY2 URL: obfs-password required when obfs is set" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:122 +msgid "Invalid HY2 URL: parsing failed" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:116 +msgid "Invalid HY2 URL: sni cannot be empty" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:98 +msgid "Invalid HY2 URL: unsupported obfs type" +msgstr "" + +#: src\validators\validateIp.ts:11 +msgid "Invalid IP address" +msgstr "" + +#: src\validators\validateOutboundJson.ts:9 +msgid "Invalid JSON format" +msgstr "" + +#: src\validators\validatePath.ts:22 +msgid "Invalid path format. Path must start with \"/\" and contain valid characters" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:85 +msgid "Invalid port number. Must be between 1 and 65535" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:37 +msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:27 +msgid "Invalid Shadowsocks URL: missing credentials" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:46 +msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:76 +msgid "Invalid Shadowsocks URL: missing port" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:67 +msgid "Invalid Shadowsocks URL: missing server" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:58 +msgid "Invalid Shadowsocks URL: missing server address" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:16 +msgid "Invalid Shadowsocks URL: must not contain spaces" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:8 +msgid "Invalid Shadowsocks URL: must start with ss://" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:91 +msgid "Invalid Shadowsocks URL: parsing failed" +msgstr "" + +#: src\validators\validateSocksUrl.ts:73 +msgid "Invalid SOCKS URL: invalid host format" +msgstr "" + +#: src\validators\validateSocksUrl.ts:63 +msgid "Invalid SOCKS URL: invalid port number" +msgstr "" + +#: src\validators\validateSocksUrl.ts:42 +msgid "Invalid SOCKS URL: missing host and port" +msgstr "" + +#: src\validators\validateSocksUrl.ts:51 +msgid "Invalid SOCKS URL: missing hostname or IP" +msgstr "" + +#: src\validators\validateSocksUrl.ts:56 +msgid "Invalid SOCKS URL: missing port" +msgstr "" + +#: src\validators\validateSocksUrl.ts:34 +msgid "Invalid SOCKS URL: missing username" +msgstr "" + +#: src\validators\validateSocksUrl.ts:19 +msgid "Invalid SOCKS URL: must not contain spaces" +msgstr "" + +#: src\validators\validateSocksUrl.ts:10 +msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" +msgstr "" + +#: src\validators\validateSocksUrl.ts:77 +msgid "Invalid SOCKS URL: parsing failed" +msgstr "" + +#: src\validators\validateTrojanUrl.ts:15 +msgid "Invalid Trojan URL: must not contain spaces" +msgstr "" + +#: src\validators\validateTrojanUrl.ts:8 +msgid "Invalid Trojan URL: must start with trojan://" +msgstr "" + +#: src\validators\validateTrojanUrl.ts:56 +msgid "Invalid Trojan URL: parsing failed" +msgstr "" + +#: src\validators\validateUrl.ts:8 +#: src\validators\validateUrl.ts:31 +msgid "Invalid URL format" +msgstr "" + +#: src\validators\validateVlessUrl.ts:110 +msgid "Invalid VLESS URL: parsing failed" +msgstr "" + +#: src\validators\validateSubnet.ts:18 +msgid "IP address 0.0.0.0 is not allowed" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:20 +msgid "Issues detected" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:48 +msgid "Latest" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:276 +msgid "List Update Frequency" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:598 +msgid "Local Domain Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:621 +msgid "Local Subnet Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:384 +msgid "Log Level" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:72 +msgid "Main DNS" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:311 +msgid "Memory Usage" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:730 +msgid "Mixed Proxy Port" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:198 +msgid "Monitored Interfaces" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:215 +msgid "Must be a number in the range of 50 - 1000" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:343 +msgid "NetShift" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:26 +msgid "NetShift Settings" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:327 +msgid "NetShift will not modify your DHCP configuration" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:260 +msgid "Network Interface" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:105 +msgid "No other marking rules found" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderCheckSection.ts:189 +msgid "Not implement yet" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:74 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:80 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:99 +msgid "Not responding" +msgstr "" + +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:59 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:67 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:75 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:83 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:91 +msgid "Not running" +msgstr "" + +#: src\helpers\withTimeout.ts:7 +msgid "Operation timed out" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:30 +msgid "Outbound Config" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:66 +msgid "Outbound Configuration" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:38 +msgid "Outdated" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:135 +msgid "Output Network Interface" +msgstr "" + +#: src\validators\validatePath.ts:7 +msgid "Path cannot be empty" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:366 +msgid "Path must be absolute (start with /)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:375 +msgid "Path must contain at least one directory (like /tmp/cache.db)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:370 +msgid "Path must end with cache.db" +msgstr "" + +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:107 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:115 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:123 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:131 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:139 +msgid "Pending" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:37 +msgid "Proxy Configuration URL" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:66 +msgid "Proxy traffic is not routed via FakeIP" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:65 +msgid "Proxy traffic is routed via FakeIP" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:385 +msgid "Regional options cannot be used together" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:644 +msgid "Remote Domain Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:667 +msgid "Remote Subnet Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:745 +msgid "Resolve real IP for routing" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:53 +msgid "Restart NetShift" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:51 +msgid "Router DNS is not routed through sing-box" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:50 +msgid "Router DNS is routed through sing-box" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:413 +msgid "Routing Excluded IPs" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:79 +msgid "Rules mangle counters" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:74 +msgid "Rules mangle exist" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:89 +msgid "Rules mangle output counters" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:84 +msgid "Rules mangle output exist" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:99 +msgid "Rules proxy counters" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:94 +msgid "Rules proxy exist" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderRunAction.ts:15 +msgid "Run Diagnostic" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:404 +msgid "Russia inside restrictions" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:257 +msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:36 +msgid "Sections" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:352 +msgid "Select a predefined list for routing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:13 +msgid "Select between VPN and Proxy connection methods for traffic routing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:13 +msgid "Select DNS protocol to use" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:277 +msgid "Select how often the domain or subnet lists are updated automatically" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:24 +msgid "Select how to configure the proxy" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:261 +msgid "Select network interface for VPN connection" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:330 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:25 +msgid "Select or enter DNS server address" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:349 +msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:336 +msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:317 +msgid "Select the DNS protocol type for the domain resolver" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:440 +msgid "Select the list type for adding custom domains" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:520 +msgid "Select the list type for adding custom subnets" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:385 +msgid "Select the log level for sing-box" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:90 +msgid "Select the network interface from which the traffic will originate" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:136 +msgid "Select the network interface to which the traffic will originate" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:199 +msgid "Select the WAN interfaces to be monitored" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:27 +msgid "Selector" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:137 +msgid "Selector Proxy Links" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:340 +msgid "Services info" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:49 +msgid "Settings" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:292 +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:120 +msgid "Show sing-box config" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:354 +msgid "Sing-box" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:77 +msgid "Sing-box autostart disabled" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:337 +msgid "Sing-box core changed, version:" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:62 +msgid "Sing-box installed" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:87 +msgid "Sing-box listening ports" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:82 +msgid "Sing-box process running" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:72 +msgid "Sing-box service exist" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:67 +msgid "Sing-box version is compatible (newer than 1.12.4)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:89 +msgid "Source Network Interface" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:414 +msgid "Specify a local IP address to be excluded from routing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:691 +msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:645 +msgid "Specify remote URLs to download and use domain lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:668 +msgid "Specify remote URLs to download and use subnet lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:599 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:622 +msgid "Specify the path to the list file located on the router filesystem" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:73 +msgid "Start NetShift" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:63 +msgid "Stop NetShift" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:29 +msgid "Subscription" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:112 +msgid "Subscription Update Interval" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:89 +msgid "Subscription URL" +msgstr "" + +#: src\helpers\copyToClipboard.ts:10 +msgid "Successfully copied!" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:304 +msgid "System info" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderSystemInfo.ts:21 +msgid "System information" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:69 +msgid "Table exist" +msgstr "" + +#: src\netshift\tabs\dashboard\partials\renderSections.ts:108 +msgid "Test latency" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:444 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:524 +msgid "Text List" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:46 +msgid "The DNS server used to look up the IP address of an upstream DNS server" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:184 +msgid "The interval between connectivity tests" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:198 +msgid "The maximum difference in response times (ms) allowed when comparing servers" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:222 +msgid "The URL used to test server connectivity" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:69 +msgid "Time in seconds for DNS record caching (default: 60)" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:238 +msgid "Traffic" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:268 +msgid "Traffic Total" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:25 +msgid "Troubleshooting" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:80 +msgid "TTL must be a positive number" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:75 +msgid "TTL value cannot be empty" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:321 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:17 +msgid "UDP (Unprotected DNS)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:250 +msgid "UDP over TCP" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:39 +#: src\netshift\tabs\diagnostic\initController.ts:40 +#: src\netshift\tabs\diagnostic\initController.ts:41 +#: src\netshift\tabs\diagnostic\initController.ts:42 +#: src\netshift\tabs\diagnostic\initController.ts:43 +#: src\netshift\tabs\diagnostic\initController.ts:44 +#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:7 +msgid "unknown" +msgstr "" + +#: src\netshift\api.ts:40 +msgid "Unknown error" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:240 +#: src\netshift\tabs\dashboard\initController.ts:271 +msgid "Uplink" +msgstr "" + +#: src\validators\validateProxyUrl.ts:37 +msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" +msgstr "" + +#: src\validators\validateUrl.ts:17 +msgid "URL must use one of the following protocols:" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:28 +msgid "URLTest" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:183 +msgid "URLTest Check Interval" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:160 +msgid "URLTest Proxy Links" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:221 +msgid "URLTest Testing URL" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:197 +msgid "URLTest Tolerance" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:439 +msgid "User Domain List Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:451 +msgid "User Domains" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:477 +msgid "User Domains List" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:519 +msgid "User Subnet List Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:531 +msgid "User Subnets" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:557 +msgid "User Subnets List" +msgstr "" + +#: src\validators\validateDns.ts:14 +#: src\validators\validateDns.ts:18 +#: src\validators\validateDomain.ts:13 +#: src\validators\validateDomain.ts:30 +#: src\validators\validateHysteriaUrl.ts:120 +#: src\validators\validateIp.ts:8 +#: src\validators\validateOutboundJson.ts:7 +#: src\validators\validatePath.ts:16 +#: src\validators\validateShadowsocksUrl.ts:95 +#: src\validators\validateSocksUrl.ts:80 +#: src\validators\validateSubnet.ts:38 +#: src\validators\validateTrojanUrl.ts:59 +#: src\validators\validateUrl.ts:28 +#: src\validators\validateVlessUrl.ts:108 +msgid "Valid" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:510 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:589 +msgid "Validation errors:" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:258 +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:111 +msgid "View logs" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:31 +msgid "Visit Wiki" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:38 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:138 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:161 +msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:387 +msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:406 +msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:256 +msgid "YACD Secret Key" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:127 +msgid "You can select Output Network Interface, by default autodetect" +msgstr "" diff --git a/luci-app-podkop/po/ru/podkop.po b/fe-app-netshift/locales/netshift.ru.po similarity index 96% rename from luci-app-podkop/po/ru/podkop.po rename to fe-app-netshift/locales/netshift.ru.po index 8bd2c236..2b4db66d 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -1,14 +1,14 @@ -# RU translations for PODKOP package. -# Copyright (C) 2026 THE PODKOP'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PODKOP package. +# RU translations for NETSHIFT package. +# Copyright (C) 2026 THE NETSHIFT'S COPYRIGHT HOLDER +# This file is distributed under the same license as the NETSHIFT package. # yandexru45, 2026. # msgid "" msgstr "" -"Project-Id-Version: PODKOP\n" +"Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-02 14:25+0300\n" -"PO-Revision-Date: 2026-06-02 14:25+0300\n" +"POT-Creation-Date: 2026-06-02 17:15+0300\n" +"PO-Revision-Date: 2026-06-02 17:15+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -101,8 +101,8 @@ msgstr "Списки сообщества" msgid "Config File Path" msgstr "Путь к файлу конфигурации" -msgid "Configuration for Podkop service" -msgstr "Настройки сервиса Podkop" +msgid "Configuration for NetShift service" +msgstr "" msgid "Configuration Type" msgstr "Тип конфигурации" @@ -125,8 +125,8 @@ msgstr "Дашборд" msgid "Dashboard currently unavailable" msgstr "Дашборд сейчас недоступен" -msgid "Delay in milliseconds before reloading podkop after interface UP" -msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса" +msgid "Delay in milliseconds before reloading NetShift after interface UP" +msgstr "" msgid "Delay value cannot be empty" msgstr "Значение задержки не может быть пустым" @@ -476,6 +476,15 @@ msgstr "Наблюдаемые интерфейсы" msgid "Must be a number in the range of 50 - 1000" msgstr "Должно быть числом от 50 до 1000" +msgid "NetShift" +msgstr "" + +msgid "NetShift Settings" +msgstr "" + +msgid "NetShift will not modify your DHCP configuration" +msgstr "" + msgid "Network Interface" msgstr "Сетевой интерфейс" @@ -521,15 +530,6 @@ msgstr "Путь должен заканчиваться на cache.db" msgid "Pending" msgstr "Ожидает запуска" -msgid "Podkop" -msgstr "Podkop" - -msgid "Podkop Settings" -msgstr "Настройки podkop" - -msgid "Podkop will not modify your DHCP configuration" -msgstr "Podkop не будет изменять вашу конфигурацию DHCP." - msgid "Proxy Configuration URL" msgstr "URL конфигурации прокси" @@ -551,8 +551,8 @@ msgstr "Внешние списки подсетей" msgid "Resolve real IP for routing" msgstr "Разрешение реальных IP-адресов" -msgid "Restart podkop" -msgstr "Перезапустить Podkop" +msgid "Restart NetShift" +msgstr "" msgid "Router DNS is not routed through sing-box" msgstr "DNS роутера не проходит через sing-box" @@ -698,11 +698,11 @@ msgstr "Укажите URL-адреса для загрузки и исполь msgid "Specify the path to the list file located on the router filesystem" msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора." -msgid "Start podkop" -msgstr "Запустить podkop" +msgid "Start NetShift" +msgstr "" -msgid "Stop podkop" -msgstr "Остановить podkop" +msgid "Stop NetShift" +msgstr "" msgid "Subscription" msgstr "" diff --git a/fe-app-podkop/package.json b/fe-app-netshift/package.json similarity index 90% rename from fe-app-podkop/package.json rename to fe-app-netshift/package.json index 3b60630c..aebbf833 100644 --- a/fe-app-podkop/package.json +++ b/fe-app-netshift/package.json @@ -1,11 +1,11 @@ { - "name": "fe-app-podkop", + "name": "fe-app-netshift", "version": "1.0.0", "license": "MIT", "type": "module", "scripts": { "format": "prettier --write src", - "format:js": "prettier --write ../luci-app-podkop/htdocs/luci-static/resources/view/podkop", + "format:js": "prettier --write ../luci-app-netshift/htdocs/luci-static/resources/view/netshift", "lint": "eslint src --ext .ts,.tsx", "lint:fix": "eslint src --ext .ts,.tsx --fix", "build": "tsup src/main.ts", diff --git a/fe-app-podkop/src/constants.ts b/fe-app-netshift/src/constants.ts similarity index 97% rename from fe-app-podkop/src/constants.ts rename to fe-app-netshift/src/constants.ts index 19e0907c..60596254 100644 --- a/fe-app-podkop/src/constants.ts +++ b/fe-app-netshift/src/constants.ts @@ -4,7 +4,7 @@ export const STATUS_COLORS = { WARNING: '#ff9800', }; -export const PODKOP_LUCI_APP_VERSION = '__COMPILED_VERSION_VARIABLE__'; +export const NETSHIFT_LUCI_APP_VERSION = '__COMPILED_VERSION_VARIABLE__'; export const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi'; export const IP_CHECK_DOMAIN = 'ip.podkop.fyi'; diff --git a/fe-app-podkop/src/helpers/copyToClipboard.ts b/fe-app-netshift/src/helpers/copyToClipboard.ts similarity index 100% rename from fe-app-podkop/src/helpers/copyToClipboard.ts rename to fe-app-netshift/src/helpers/copyToClipboard.ts diff --git a/fe-app-podkop/src/helpers/downloadAsTxt.ts b/fe-app-netshift/src/helpers/downloadAsTxt.ts similarity index 100% rename from fe-app-podkop/src/helpers/downloadAsTxt.ts rename to fe-app-netshift/src/helpers/downloadAsTxt.ts diff --git a/fe-app-podkop/src/helpers/executeShellCommand.ts b/fe-app-netshift/src/helpers/executeShellCommand.ts similarity index 100% rename from fe-app-podkop/src/helpers/executeShellCommand.ts rename to fe-app-netshift/src/helpers/executeShellCommand.ts diff --git a/fe-app-podkop/src/helpers/getClashApiUrl.ts b/fe-app-netshift/src/helpers/getClashApiUrl.ts similarity index 100% rename from fe-app-podkop/src/helpers/getClashApiUrl.ts rename to fe-app-netshift/src/helpers/getClashApiUrl.ts diff --git a/fe-app-podkop/src/helpers/getProxyUrlName.ts b/fe-app-netshift/src/helpers/getProxyUrlName.ts similarity index 100% rename from fe-app-podkop/src/helpers/getProxyUrlName.ts rename to fe-app-netshift/src/helpers/getProxyUrlName.ts diff --git a/fe-app-podkop/src/helpers/index.ts b/fe-app-netshift/src/helpers/index.ts similarity index 100% rename from fe-app-podkop/src/helpers/index.ts rename to fe-app-netshift/src/helpers/index.ts diff --git a/fe-app-podkop/src/helpers/injectGlobalStyles.ts b/fe-app-netshift/src/helpers/injectGlobalStyles.ts similarity index 100% rename from fe-app-podkop/src/helpers/injectGlobalStyles.ts rename to fe-app-netshift/src/helpers/injectGlobalStyles.ts diff --git a/fe-app-podkop/src/helpers/insertIf.ts b/fe-app-netshift/src/helpers/insertIf.ts similarity index 100% rename from fe-app-podkop/src/helpers/insertIf.ts rename to fe-app-netshift/src/helpers/insertIf.ts diff --git a/fe-app-podkop/src/helpers/maskIP.ts b/fe-app-netshift/src/helpers/maskIP.ts similarity index 100% rename from fe-app-podkop/src/helpers/maskIP.ts rename to fe-app-netshift/src/helpers/maskIP.ts diff --git a/fe-app-podkop/src/helpers/normalizeCompiledVersion.ts b/fe-app-netshift/src/helpers/normalizeCompiledVersion.ts similarity index 100% rename from fe-app-podkop/src/helpers/normalizeCompiledVersion.ts rename to fe-app-netshift/src/helpers/normalizeCompiledVersion.ts diff --git a/fe-app-podkop/src/helpers/onMount.ts b/fe-app-netshift/src/helpers/onMount.ts similarity index 100% rename from fe-app-podkop/src/helpers/onMount.ts rename to fe-app-netshift/src/helpers/onMount.ts diff --git a/fe-app-podkop/src/helpers/parseQueryString.ts b/fe-app-netshift/src/helpers/parseQueryString.ts similarity index 100% rename from fe-app-podkop/src/helpers/parseQueryString.ts rename to fe-app-netshift/src/helpers/parseQueryString.ts diff --git a/fe-app-podkop/src/helpers/parseValueList.ts b/fe-app-netshift/src/helpers/parseValueList.ts similarity index 100% rename from fe-app-podkop/src/helpers/parseValueList.ts rename to fe-app-netshift/src/helpers/parseValueList.ts diff --git a/fe-app-podkop/src/helpers/preserveScrollForPage.ts b/fe-app-netshift/src/helpers/preserveScrollForPage.ts similarity index 100% rename from fe-app-podkop/src/helpers/preserveScrollForPage.ts rename to fe-app-netshift/src/helpers/preserveScrollForPage.ts diff --git a/fe-app-podkop/src/helpers/prettyBytes.ts b/fe-app-netshift/src/helpers/prettyBytes.ts similarity index 100% rename from fe-app-podkop/src/helpers/prettyBytes.ts rename to fe-app-netshift/src/helpers/prettyBytes.ts diff --git a/fe-app-podkop/src/helpers/removeVersionPrefix.ts b/fe-app-netshift/src/helpers/removeVersionPrefix.ts similarity index 100% rename from fe-app-podkop/src/helpers/removeVersionPrefix.ts rename to fe-app-netshift/src/helpers/removeVersionPrefix.ts diff --git a/fe-app-podkop/src/helpers/showToast.ts b/fe-app-netshift/src/helpers/showToast.ts similarity index 100% rename from fe-app-podkop/src/helpers/showToast.ts rename to fe-app-netshift/src/helpers/showToast.ts diff --git a/fe-app-podkop/src/helpers/splitProxyString.ts b/fe-app-netshift/src/helpers/splitProxyString.ts similarity index 100% rename from fe-app-podkop/src/helpers/splitProxyString.ts rename to fe-app-netshift/src/helpers/splitProxyString.ts diff --git a/fe-app-podkop/src/helpers/svgEl.ts b/fe-app-netshift/src/helpers/svgEl.ts similarity index 100% rename from fe-app-podkop/src/helpers/svgEl.ts rename to fe-app-netshift/src/helpers/svgEl.ts diff --git a/fe-app-podkop/src/helpers/tests/maskIp.test.js b/fe-app-netshift/src/helpers/tests/maskIp.test.js similarity index 100% rename from fe-app-podkop/src/helpers/tests/maskIp.test.js rename to fe-app-netshift/src/helpers/tests/maskIp.test.js diff --git a/fe-app-podkop/src/helpers/withTimeout.ts b/fe-app-netshift/src/helpers/withTimeout.ts similarity index 94% rename from fe-app-podkop/src/helpers/withTimeout.ts rename to fe-app-netshift/src/helpers/withTimeout.ts index e2207409..d9319af4 100644 --- a/fe-app-podkop/src/helpers/withTimeout.ts +++ b/fe-app-netshift/src/helpers/withTimeout.ts @@ -1,4 +1,4 @@ -import { logger } from '../podkop'; +import { logger } from '../netshift'; export async function withTimeout( promise: Promise, diff --git a/fe-app-podkop/src/icons/index.ts b/fe-app-netshift/src/icons/index.ts similarity index 100% rename from fe-app-podkop/src/icons/index.ts rename to fe-app-netshift/src/icons/index.ts diff --git a/fe-app-podkop/src/icons/renderBookOpenTextIcon24.ts b/fe-app-netshift/src/icons/renderBookOpenTextIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderBookOpenTextIcon24.ts rename to fe-app-netshift/src/icons/renderBookOpenTextIcon24.ts diff --git a/fe-app-podkop/src/icons/renderCheckIcon24.ts b/fe-app-netshift/src/icons/renderCheckIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderCheckIcon24.ts rename to fe-app-netshift/src/icons/renderCheckIcon24.ts diff --git a/fe-app-podkop/src/icons/renderCircleAlertIcon24.ts b/fe-app-netshift/src/icons/renderCircleAlertIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderCircleAlertIcon24.ts rename to fe-app-netshift/src/icons/renderCircleAlertIcon24.ts diff --git a/fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts b/fe-app-netshift/src/icons/renderCircleCheckBigIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderCircleCheckBigIcon24.ts rename to fe-app-netshift/src/icons/renderCircleCheckBigIcon24.ts diff --git a/fe-app-podkop/src/icons/renderCircleCheckIcon24.ts b/fe-app-netshift/src/icons/renderCircleCheckIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderCircleCheckIcon24.ts rename to fe-app-netshift/src/icons/renderCircleCheckIcon24.ts diff --git a/fe-app-podkop/src/icons/renderCirclePlayIcon24.ts b/fe-app-netshift/src/icons/renderCirclePlayIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderCirclePlayIcon24.ts rename to fe-app-netshift/src/icons/renderCirclePlayIcon24.ts diff --git a/fe-app-podkop/src/icons/renderCircleSlashIcon24.ts b/fe-app-netshift/src/icons/renderCircleSlashIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderCircleSlashIcon24.ts rename to fe-app-netshift/src/icons/renderCircleSlashIcon24.ts diff --git a/fe-app-podkop/src/icons/renderCircleStopIcon24.ts b/fe-app-netshift/src/icons/renderCircleStopIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderCircleStopIcon24.ts rename to fe-app-netshift/src/icons/renderCircleStopIcon24.ts diff --git a/fe-app-podkop/src/icons/renderCircleXIcon24.ts b/fe-app-netshift/src/icons/renderCircleXIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderCircleXIcon24.ts rename to fe-app-netshift/src/icons/renderCircleXIcon24.ts diff --git a/fe-app-podkop/src/icons/renderCogIcon24.ts b/fe-app-netshift/src/icons/renderCogIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderCogIcon24.ts rename to fe-app-netshift/src/icons/renderCogIcon24.ts diff --git a/fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts b/fe-app-netshift/src/icons/renderLoaderCircleIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderLoaderCircleIcon24.ts rename to fe-app-netshift/src/icons/renderLoaderCircleIcon24.ts diff --git a/fe-app-podkop/src/icons/renderPauseIcon24.ts b/fe-app-netshift/src/icons/renderPauseIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderPauseIcon24.ts rename to fe-app-netshift/src/icons/renderPauseIcon24.ts diff --git a/fe-app-podkop/src/icons/renderPlayIcon24.ts b/fe-app-netshift/src/icons/renderPlayIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderPlayIcon24.ts rename to fe-app-netshift/src/icons/renderPlayIcon24.ts diff --git a/fe-app-podkop/src/icons/renderRotateCcwIcon24.ts b/fe-app-netshift/src/icons/renderRotateCcwIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderRotateCcwIcon24.ts rename to fe-app-netshift/src/icons/renderRotateCcwIcon24.ts diff --git a/fe-app-podkop/src/icons/renderSearchIcon24.ts b/fe-app-netshift/src/icons/renderSearchIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderSearchIcon24.ts rename to fe-app-netshift/src/icons/renderSearchIcon24.ts diff --git a/fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts b/fe-app-netshift/src/icons/renderSquareChartGanttIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderSquareChartGanttIcon24.ts rename to fe-app-netshift/src/icons/renderSquareChartGanttIcon24.ts diff --git a/fe-app-podkop/src/icons/renderTriangleAlertIcon24.ts b/fe-app-netshift/src/icons/renderTriangleAlertIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderTriangleAlertIcon24.ts rename to fe-app-netshift/src/icons/renderTriangleAlertIcon24.ts diff --git a/fe-app-podkop/src/icons/renderXIcon24.ts b/fe-app-netshift/src/icons/renderXIcon24.ts similarity index 100% rename from fe-app-podkop/src/icons/renderXIcon24.ts rename to fe-app-netshift/src/icons/renderXIcon24.ts diff --git a/fe-app-podkop/src/luci.d.ts b/fe-app-netshift/src/luci.d.ts similarity index 100% rename from fe-app-podkop/src/luci.d.ts rename to fe-app-netshift/src/luci.d.ts diff --git a/fe-app-podkop/src/main.ts b/fe-app-netshift/src/main.ts similarity index 90% rename from fe-app-podkop/src/main.ts rename to fe-app-netshift/src/main.ts index 497e60ef..3df7b463 100644 --- a/fe-app-podkop/src/main.ts +++ b/fe-app-netshift/src/main.ts @@ -9,5 +9,5 @@ if (typeof structuredClone !== 'function') export * from './validators'; export * from './helpers'; -export * from './podkop'; +export * from './netshift'; export * from './constants'; diff --git a/fe-app-podkop/src/podkop/api.ts b/fe-app-netshift/src/netshift/api.ts similarity index 100% rename from fe-app-podkop/src/podkop/api.ts rename to fe-app-netshift/src/netshift/api.ts diff --git a/fe-app-netshift/src/netshift/fetchers/fetchServicesInfo.ts b/fe-app-netshift/src/netshift/fetchers/fetchServicesInfo.ts new file mode 100644 index 00000000..f2886309 --- /dev/null +++ b/fe-app-netshift/src/netshift/fetchers/fetchServicesInfo.ts @@ -0,0 +1,32 @@ +import { NetShiftShellMethods } from '../methods'; +import { store } from '../services'; + +export async function fetchServicesInfo() { + const [netshift, singbox] = await Promise.all([ + NetShiftShellMethods.getStatus(), + NetShiftShellMethods.getSingBoxStatus(), + ]); + + if (!netshift.success || !singbox.success) { + store.set({ + servicesInfoWidget: { + loading: false, + failed: true, + data: { singbox: 0, netshift: 0 }, + }, + }); + } + + if (netshift.success && singbox.success) { + store.set({ + servicesInfoWidget: { + loading: false, + failed: false, + data: { + singbox: singbox.data.running, + netshift: netshift.data.enabled, + }, + }, + }); + } +} diff --git a/fe-app-podkop/src/podkop/fetchers/index.ts b/fe-app-netshift/src/netshift/fetchers/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/fetchers/index.ts rename to fe-app-netshift/src/netshift/fetchers/index.ts diff --git a/fe-app-podkop/src/podkop/index.ts b/fe-app-netshift/src/netshift/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/index.ts rename to fe-app-netshift/src/netshift/index.ts diff --git a/fe-app-podkop/src/podkop/methods/custom/getClashApiSecret.ts b/fe-app-netshift/src/netshift/methods/custom/getClashApiSecret.ts similarity index 100% rename from fe-app-podkop/src/podkop/methods/custom/getClashApiSecret.ts rename to fe-app-netshift/src/netshift/methods/custom/getClashApiSecret.ts diff --git a/fe-app-netshift/src/netshift/methods/custom/getConfigSections.ts b/fe-app-netshift/src/netshift/methods/custom/getConfigSections.ts new file mode 100644 index 00000000..1f40d04f --- /dev/null +++ b/fe-app-netshift/src/netshift/methods/custom/getConfigSections.ts @@ -0,0 +1,5 @@ +import { NetShift } from '../../types'; + +export async function getConfigSections(): Promise { + return uci.load('netshift').then(() => uci.sections('netshift')); +} diff --git a/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts b/fe-app-netshift/src/netshift/methods/custom/getDashboardSections.ts similarity index 97% rename from fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts rename to fe-app-netshift/src/netshift/methods/custom/getDashboardSections.ts index fedb0723..9248568f 100644 --- a/fe-app-podkop/src/podkop/methods/custom/getDashboardSections.ts +++ b/fe-app-netshift/src/netshift/methods/custom/getDashboardSections.ts @@ -1,16 +1,16 @@ import { getConfigSections } from './getConfigSections'; -import { Podkop } from '../../types'; +import { NetShift } from '../../types'; import { getProxyUrlName, splitProxyString } from '../../../helpers'; -import { PodkopShellMethods } from '../shell'; +import { NetShiftShellMethods } from '../shell'; interface IGetDashboardSectionsResponse { success: boolean; - data: Podkop.OutboundGroup[]; + data: NetShift.OutboundGroup[]; } export async function getDashboardSections(): Promise { const configSections = await getConfigSections(); - const clashProxies = await PodkopShellMethods.getClashApiProxies(); + const clashProxies = await NetShiftShellMethods.getClashApiProxies(); if (!clashProxies.success) { return { diff --git a/fe-app-podkop/src/podkop/methods/custom/index.ts b/fe-app-netshift/src/netshift/methods/custom/index.ts similarity index 86% rename from fe-app-podkop/src/podkop/methods/custom/index.ts rename to fe-app-netshift/src/netshift/methods/custom/index.ts index 8aba225c..13a8f0d3 100644 --- a/fe-app-podkop/src/podkop/methods/custom/index.ts +++ b/fe-app-netshift/src/netshift/methods/custom/index.ts @@ -2,7 +2,7 @@ import { getConfigSections } from './getConfigSections'; import { getDashboardSections } from './getDashboardSections'; import { getClashApiSecret } from './getClashApiSecret'; -export const CustomPodkopMethods = { +export const CustomNetShiftMethods = { getConfigSections, getDashboardSections, getClashApiSecret, diff --git a/fe-app-podkop/src/podkop/methods/fakeip/getFakeIpCheck.ts b/fe-app-netshift/src/netshift/methods/fakeip/getFakeIpCheck.ts similarity index 100% rename from fe-app-podkop/src/podkop/methods/fakeip/getFakeIpCheck.ts rename to fe-app-netshift/src/netshift/methods/fakeip/getFakeIpCheck.ts diff --git a/fe-app-podkop/src/podkop/methods/fakeip/getIpCheck.ts b/fe-app-netshift/src/netshift/methods/fakeip/getIpCheck.ts similarity index 100% rename from fe-app-podkop/src/podkop/methods/fakeip/getIpCheck.ts rename to fe-app-netshift/src/netshift/methods/fakeip/getIpCheck.ts diff --git a/fe-app-podkop/src/podkop/methods/fakeip/index.ts b/fe-app-netshift/src/netshift/methods/fakeip/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/methods/fakeip/index.ts rename to fe-app-netshift/src/netshift/methods/fakeip/index.ts diff --git a/fe-app-podkop/src/podkop/methods/index.ts b/fe-app-netshift/src/netshift/methods/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/methods/index.ts rename to fe-app-netshift/src/netshift/methods/index.ts diff --git a/fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts b/fe-app-netshift/src/netshift/methods/shell/callBaseMethod.ts similarity index 77% rename from fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts rename to fe-app-netshift/src/netshift/methods/shell/callBaseMethod.ts index 519cd369..f278a3de 100644 --- a/fe-app-podkop/src/podkop/methods/shell/callBaseMethod.ts +++ b/fe-app-netshift/src/netshift/methods/shell/callBaseMethod.ts @@ -1,11 +1,11 @@ import { executeShellCommand } from '../../../helpers'; -import { Podkop } from '../../types'; +import { NetShift } from '../../types'; export async function callBaseMethod( - method: Podkop.AvailableMethods, + method: NetShift.AvailableMethods, args: string[] = [], - command: string = '/usr/bin/podkop', -): Promise> { + command: string = '/usr/bin/netshift', +): Promise> { const response = await executeShellCommand({ command, args: [method as string, ...args], diff --git a/fe-app-netshift/src/netshift/methods/shell/index.ts b/fe-app-netshift/src/netshift/methods/shell/index.ts new file mode 100644 index 00000000..428a42c5 --- /dev/null +++ b/fe-app-netshift/src/netshift/methods/shell/index.ts @@ -0,0 +1,129 @@ +import { callBaseMethod } from './callBaseMethod'; +import { ClashAPI, NetShift } from '../../types'; +import { executeShellCommand } from '../../../helpers'; + +interface SingBoxComponentActionResult { + success: boolean; + version?: string; + message?: string; +} + +export const NetShiftShellMethods = { + checkDNSAvailable: async () => + callBaseMethod( + NetShift.AvailableMethods.CHECK_DNS_AVAILABLE, + ), + checkFakeIP: async () => + callBaseMethod( + NetShift.AvailableMethods.CHECK_FAKEIP, + ), + checkNftRules: async () => + callBaseMethod( + NetShift.AvailableMethods.CHECK_NFT_RULES, + ), + getStatus: async () => + callBaseMethod(NetShift.AvailableMethods.GET_STATUS), + checkSingBox: async () => + callBaseMethod( + NetShift.AvailableMethods.CHECK_SING_BOX, + ), + getSingBoxStatus: async () => + callBaseMethod( + NetShift.AvailableMethods.GET_SING_BOX_STATUS, + ), + getClashApiProxies: async () => + callBaseMethod(NetShift.AvailableMethods.CLASH_API, [ + NetShift.AvailableClashAPIMethods.GET_PROXIES, + ]), + getClashApiProxyLatency: async (tag: string) => + callBaseMethod( + NetShift.AvailableMethods.CLASH_API, + [NetShift.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, '5000'], + ), + getClashApiGroupLatency: async (tag: string) => + callBaseMethod( + NetShift.AvailableMethods.CLASH_API, + [NetShift.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, '10000'], + ), + setClashApiGroupProxy: async (group: string, proxy: string) => + callBaseMethod(NetShift.AvailableMethods.CLASH_API, [ + NetShift.AvailableClashAPIMethods.SET_GROUP_PROXY, + group, + proxy, + ]), + restart: async () => + callBaseMethod( + NetShift.AvailableMethods.RESTART, + [], + '/etc/init.d/netshift', + ), + start: async () => + callBaseMethod( + NetShift.AvailableMethods.START, + [], + '/etc/init.d/netshift', + ), + stop: async () => + callBaseMethod( + NetShift.AvailableMethods.STOP, + [], + '/etc/init.d/netshift', + ), + enable: async () => + callBaseMethod( + NetShift.AvailableMethods.ENABLE, + [], + '/etc/init.d/netshift', + ), + disable: async () => + callBaseMethod( + NetShift.AvailableMethods.DISABLE, + [], + '/etc/init.d/netshift', + ), + globalCheck: async () => + callBaseMethod(NetShift.AvailableMethods.GLOBAL_CHECK), + showSingBoxConfig: async () => + callBaseMethod(NetShift.AvailableMethods.SHOW_SING_BOX_CONFIG), + checkLogs: async () => + callBaseMethod(NetShift.AvailableMethods.CHECK_LOGS), + getSystemInfo: async () => + callBaseMethod( + NetShift.AvailableMethods.GET_SYSTEM_INFO, + ), + subscriptionUpdate: async () => + callBaseMethod(NetShift.AvailableMethods.SUBSCRIPTION_UPDATE), + singBoxComponentAction: async ( + action: 'install_extended' | 'install_stable' | 'check_update', + ): Promise => { + const response = await executeShellCommand({ + command: '/usr/bin/netshift', + args: ['component_action', 'sing_box', action], + timeout: 600000, + }); + + if (response.stdout) { + try { + const parsed = JSON.parse( + response.stdout, + ) as SingBoxComponentActionResult; + + return { + success: Boolean(parsed.success), + version: parsed.version, + message: parsed.message, + }; + } catch (_e) { + return { + success: false, + message: response.stdout, + }; + } + } + + return { + success: false, + message: response.stderr || '', + }; + }, +}; diff --git a/fe-app-podkop/src/podkop/services/core.service.ts b/fe-app-netshift/src/netshift/services/core.service.ts similarity index 71% rename from fe-app-podkop/src/podkop/services/core.service.ts rename to fe-app-netshift/src/netshift/services/core.service.ts index 79b63dbb..e401565c 100644 --- a/fe-app-podkop/src/podkop/services/core.service.ts +++ b/fe-app-netshift/src/netshift/services/core.service.ts @@ -1,8 +1,8 @@ import { TabServiceInstance } from './tab.service'; import { store } from './store.service'; import { logger } from './logger.service'; -import { PodkopLogWatcher } from './podkopLogWatcher.service'; -import { PodkopShellMethods } from '../methods'; +import { NetShiftLogWatcher } from './netshiftLogWatcher.service'; +import { NetShiftShellMethods } from '../methods'; export function coreService() { TabServiceInstance.onChange((activeId, tabs) => { @@ -15,11 +15,11 @@ export function coreService() { }); }); - const watcher = PodkopLogWatcher.getInstance(); + const watcher = NetShiftLogWatcher.getInstance(); watcher.init( async () => { - const logs = await PodkopShellMethods.checkLogs(); + const logs = await NetShiftShellMethods.checkLogs(); if (logs.success) { return logs.data as string; @@ -34,7 +34,7 @@ export function coreService() { line.toLowerCase().includes('[error]') || line.toLowerCase().includes('[fatal]') ) { - ui.addNotification('Podkop Error', E('div', {}, line), 'error'); + ui.addNotification('NetShift Error', E('div', {}, line), 'error'); } }, }, diff --git a/fe-app-podkop/src/podkop/services/index.ts b/fe-app-netshift/src/netshift/services/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/services/index.ts rename to fe-app-netshift/src/netshift/services/index.ts diff --git a/fe-app-podkop/src/podkop/services/logger.service.ts b/fe-app-netshift/src/netshift/services/logger.service.ts similarity index 100% rename from fe-app-podkop/src/podkop/services/logger.service.ts rename to fe-app-netshift/src/netshift/services/logger.service.ts diff --git a/fe-app-podkop/src/podkop/services/podkopLogWatcher.service.ts b/fe-app-netshift/src/netshift/services/netshiftLogWatcher.service.ts similarity index 68% rename from fe-app-podkop/src/podkop/services/podkopLogWatcher.service.ts rename to fe-app-netshift/src/netshift/services/netshiftLogWatcher.service.ts index e09b288e..560af7cc 100644 --- a/fe-app-podkop/src/podkop/services/podkopLogWatcher.service.ts +++ b/fe-app-netshift/src/netshift/services/netshiftLogWatcher.service.ts @@ -2,13 +2,13 @@ import { logger } from './logger.service'; export type LogFetcher = () => Promise | string; -export interface PodkopLogWatcherOptions { +export interface NetShiftLogWatcherOptions { intervalMs?: number; onNewLog?: (line: string) => void; } -export class PodkopLogWatcher { - private static instance: PodkopLogWatcher; +export class NetShiftLogWatcher { + private static instance: NetShiftLogWatcher; private fetcher?: LogFetcher; private onNewLog?: (line: string) => void; private intervalMs = 5000; @@ -26,31 +26,31 @@ export class PodkopLogWatcher { } } - static getInstance(): PodkopLogWatcher { - if (!PodkopLogWatcher.instance) { - PodkopLogWatcher.instance = new PodkopLogWatcher(); + static getInstance(): NetShiftLogWatcher { + if (!NetShiftLogWatcher.instance) { + NetShiftLogWatcher.instance = new NetShiftLogWatcher(); } - return PodkopLogWatcher.instance; + return NetShiftLogWatcher.instance; } - init(fetcher: LogFetcher, options?: PodkopLogWatcherOptions): void { + init(fetcher: LogFetcher, options?: NetShiftLogWatcherOptions): void { this.fetcher = fetcher; this.onNewLog = options?.onNewLog; this.intervalMs = options?.intervalMs ?? 5000; logger.info( - '[PodkopLogWatcher]', + '[NetShiftLogWatcher]', `initialized (interval: ${this.intervalMs}ms)`, ); } async checkOnce(): Promise { if (!this.fetcher) { - logger.warn('[PodkopLogWatcher]', 'fetcher not found'); + logger.warn('[NetShiftLogWatcher]', 'fetcher not found'); return; } if (this.paused) { - logger.debug('[PodkopLogWatcher]', 'skipped check — tab not visible'); + logger.debug('[NetShiftLogWatcher]', 'skipped check — tab not visible'); return; } @@ -70,21 +70,21 @@ export class PodkopLogWatcher { this.lastLines = new Set(arr.slice(-500)); } } catch (err) { - logger.error('[PodkopLogWatcher]', 'failed to read logs:', err); + logger.error('[NetShiftLogWatcher]', 'failed to read logs:', err); } } start(): void { if (this.running) return; if (!this.fetcher) { - logger.warn('[PodkopLogWatcher]', 'attempted to start without fetcher'); + logger.warn('[NetShiftLogWatcher]', 'attempted to start without fetcher'); return; } this.running = true; this.timer = setInterval(() => this.checkOnce(), this.intervalMs); logger.info( - '[PodkopLogWatcher]', + '[NetShiftLogWatcher]', `started (interval: ${this.intervalMs}ms)`, ); } @@ -93,24 +93,24 @@ export class PodkopLogWatcher { if (!this.running) return; this.running = false; if (this.timer) clearInterval(this.timer); - logger.info('[PodkopLogWatcher]', 'stopped'); + logger.info('[NetShiftLogWatcher]', 'stopped'); } pause(): void { if (!this.running || this.paused) return; this.paused = true; - logger.info('[PodkopLogWatcher]', 'paused (tab not visible)'); + logger.info('[NetShiftLogWatcher]', 'paused (tab not visible)'); } resume(): void { if (!this.running || !this.paused) return; this.paused = false; - logger.info('[PodkopLogWatcher]', 'resumed (tab active)'); + logger.info('[NetShiftLogWatcher]', 'resumed (tab active)'); this.checkOnce(); // сразу проверить, не появились ли новые логи } reset(): void { this.lastLines.clear(); - logger.info('[PodkopLogWatcher]', 'log history reset'); + logger.info('[NetShiftLogWatcher]', 'log history reset'); } } diff --git a/fe-app-podkop/src/podkop/services/socket.service.ts b/fe-app-netshift/src/netshift/services/socket.service.ts similarity index 100% rename from fe-app-podkop/src/podkop/services/socket.service.ts rename to fe-app-netshift/src/netshift/services/socket.service.ts diff --git a/fe-app-podkop/src/podkop/services/store.service.ts b/fe-app-netshift/src/netshift/services/store.service.ts similarity index 95% rename from fe-app-podkop/src/podkop/services/store.service.ts rename to fe-app-netshift/src/netshift/services/store.service.ts index ca9db829..255d61e3 100644 --- a/fe-app-podkop/src/podkop/services/store.service.ts +++ b/fe-app-netshift/src/netshift/services/store.service.ts @@ -1,4 +1,4 @@ -import { Podkop } from '../types'; +import { NetShift } from '../types'; import { initialDiagnosticStore } from '../tabs/diagnostic/diagnostic.store'; function jsonStableStringify(obj: T): string { @@ -159,12 +159,12 @@ export interface StoreType { servicesInfoWidget: { loading: boolean; failed: boolean; - data: { singbox: number; podkop: number }; + data: { singbox: number; netshift: number }; }; sectionsWidget: { loading: boolean; failed: boolean; - data: Podkop.OutboundGroup[]; + data: NetShift.OutboundGroup[]; latencyFetching: boolean; }; diagnosticsRunAction: { @@ -184,8 +184,8 @@ export interface StoreType { }; diagnosticsSystemInfo: { loading: boolean; - podkop_version: string; - podkop_latest_version: string; + netshift_version: string; + netshift_latest_version: string; luci_app_version: string; sing_box_version: string; openwrt_version: string; @@ -217,7 +217,7 @@ const initialStore: StoreType = { servicesInfoWidget: { loading: true, failed: false, - data: { singbox: 0, podkop: 0 }, + data: { singbox: 0, netshift: 0 }, }, sectionsWidget: { loading: true, diff --git a/fe-app-podkop/src/podkop/services/tab.service.ts b/fe-app-netshift/src/netshift/services/tab.service.ts similarity index 100% rename from fe-app-podkop/src/podkop/services/tab.service.ts rename to fe-app-netshift/src/netshift/services/tab.service.ts diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/index.ts b/fe-app-netshift/src/netshift/tabs/dashboard/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/dashboard/index.ts rename to fe-app-netshift/src/netshift/tabs/dashboard/index.ts diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/initController.ts b/fe-app-netshift/src/netshift/tabs/dashboard/initController.ts similarity index 95% rename from fe-app-podkop/src/podkop/tabs/dashboard/initController.ts rename to fe-app-netshift/src/netshift/tabs/dashboard/initController.ts index 2b8c9487..f30c414d 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/initController.ts +++ b/fe-app-netshift/src/netshift/tabs/dashboard/initController.ts @@ -4,7 +4,7 @@ import { preserveScrollForPage, } from '../../../helpers'; import { prettyBytes } from '../../../helpers/prettyBytes'; -import { CustomPodkopMethods, PodkopShellMethods } from '../../methods'; +import { CustomNetShiftMethods, NetShiftShellMethods } from '../../methods'; import { logger, socket, store, StoreType } from '../../services'; import { renderSections, renderWidget } from './partials'; import { fetchServicesInfo } from '../../fetchers'; @@ -22,7 +22,7 @@ async function fetchDashboardSections() { }, }); - const { data, success } = await CustomPodkopMethods.getDashboardSections(); + const { data, success } = await CustomNetShiftMethods.getDashboardSections(); if (!success) { logger.error('[DASHBOARD]', 'fetchDashboardSections: failed to fetch'); @@ -122,7 +122,7 @@ async function connectToClashSockets() { // Handlers async function handleChooseOutbound(selector: string, tag: string) { - await PodkopShellMethods.setClashApiGroupProxy(selector, tag); + await NetShiftShellMethods.setClashApiGroupProxy(selector, tag); await fetchDashboardSections(); } @@ -134,7 +134,7 @@ async function handleTestGroupLatency(tag: string) { }, }); - await PodkopShellMethods.getClashApiGroupLatency(tag); + await NetShiftShellMethods.getClashApiGroupLatency(tag); await fetchDashboardSections(); store.set({ @@ -153,7 +153,7 @@ async function handleTestProxyLatency(tag: string) { }, }); - await PodkopShellMethods.getClashApiProxyLatency(tag); + await NetShiftShellMethods.getClashApiProxyLatency(tag); await fetchDashboardSections(); store.set({ @@ -340,12 +340,12 @@ async function renderServicesInfoWidget() { title: _('Services info'), items: [ { - key: _('Podkop'), - value: servicesInfoWidget.data.podkop + key: _('NetShift'), + value: servicesInfoWidget.data.netshift ? _('✔ Enabled') : _('✘ Disabled'), attributes: { - class: servicesInfoWidget.data.podkop + class: servicesInfoWidget.data.netshift ? 'pdk_dashboard-page__widgets-section__item__row--success' : 'pdk_dashboard-page__widgets-section__item__row--error', }, diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/partials/index.ts b/fe-app-netshift/src/netshift/tabs/dashboard/partials/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/dashboard/partials/index.ts rename to fe-app-netshift/src/netshift/tabs/dashboard/partials/index.ts diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/partials/renderSections.ts b/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderSections.ts similarity index 96% rename from fe-app-podkop/src/podkop/tabs/dashboard/partials/renderSections.ts rename to fe-app-netshift/src/netshift/tabs/dashboard/partials/renderSections.ts index bf78e7e6..eeb90871 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/partials/renderSections.ts +++ b/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderSections.ts @@ -1,9 +1,9 @@ -import { Podkop } from '../../../types'; +import { NetShift } from '../../../types'; interface IRenderSectionsProps { loading: boolean; failed: boolean; - section: Podkop.OutboundGroup; + section: NetShift.OutboundGroup; onTestLatency: (tag: string) => void; onChooseOutbound: (selector: string, tag: string) => void; latencyFetching: boolean; @@ -44,7 +44,7 @@ export function renderDefaultState({ } } - function renderOutbound(outbound: Podkop.Outbound) { + function renderOutbound(outbound: NetShift.Outbound) { function getLatencyClass() { if (!outbound.latency) { return 'pdk_dashboard-page__outbound-grid__item__latency--empty'; diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/partials/renderWidget.ts b/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderWidget.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/dashboard/partials/renderWidget.ts rename to fe-app-netshift/src/netshift/tabs/dashboard/partials/renderWidget.ts diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/render.ts b/fe-app-netshift/src/netshift/tabs/dashboard/render.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/dashboard/render.ts rename to fe-app-netshift/src/netshift/tabs/dashboard/render.ts diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/styles.ts b/fe-app-netshift/src/netshift/tabs/dashboard/styles.ts similarity index 97% rename from fe-app-podkop/src/podkop/tabs/dashboard/styles.ts rename to fe-app-netshift/src/netshift/tabs/dashboard/styles.ts index 32066e5b..4b26b71e 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/styles.ts +++ b/fe-app-netshift/src/netshift/tabs/dashboard/styles.ts @@ -1,10 +1,10 @@ // language=CSS export const styles = ` -#cbi-podkop-dashboard-_mount_node > div { +#cbi-netshift-dashboard-_mount_node > div { width: 100%; } -#cbi-podkop-dashboard > h3 { +#cbi-netshift-dashboard > h3 { display: none; } diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/contstants.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/contstants.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/checks/contstants.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/checks/contstants.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runDnsCheck.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runDnsCheck.ts similarity index 94% rename from fe-app-podkop/src/podkop/tabs/diagnostic/checks/runDnsCheck.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/checks/runDnsCheck.ts index 4a13b4ec..116e4692 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runDnsCheck.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runDnsCheck.ts @@ -1,6 +1,6 @@ import { insertIf } from '../../../../helpers'; import { DIAGNOSTICS_CHECKS_MAP } from './contstants'; -import { PodkopShellMethods } from '../../../methods'; +import { NetShiftShellMethods } from '../../../methods'; import { IDiagnosticsChecksItem } from '../../../services'; import { updateCheckStore } from './updateCheckStore'; import { getMeta } from '../helpers/getMeta'; @@ -17,7 +17,7 @@ export async function runDnsCheck() { items: [], }); - const dnsChecks = await PodkopShellMethods.checkDNSAvailable(); + const dnsChecks = await NetShiftShellMethods.checkDNSAvailable(); if (!dnsChecks.success) { updateCheckStore({ diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts similarity index 93% rename from fe-app-podkop/src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts index ebfc7298..f77ff6f2 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts @@ -1,6 +1,6 @@ import { insertIf } from '../../../../helpers'; import { DIAGNOSTICS_CHECKS_MAP } from './contstants'; -import { PodkopShellMethods, RemoteFakeIPMethods } from '../../../methods'; +import { NetShiftShellMethods, RemoteFakeIPMethods } from '../../../methods'; import { IDiagnosticsChecksItem } from '../../../services'; import { updateCheckStore } from './updateCheckStore'; import { getMeta } from '../helpers/getMeta'; @@ -17,7 +17,7 @@ export async function runFakeIPCheck() { items: [], }); - const routerFakeIPResponse = await PodkopShellMethods.checkFakeIP(); + const routerFakeIPResponse = await NetShiftShellMethods.checkFakeIP(); const checkFakeIPResponse = await RemoteFakeIPMethods.getFakeIpCheck(); const checkIPResponse = await RemoteFakeIPMethods.getIpCheck(); diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runNftCheck.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runNftCheck.ts similarity index 95% rename from fe-app-podkop/src/podkop/tabs/diagnostic/checks/runNftCheck.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/checks/runNftCheck.ts index a28a378e..65bc065f 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runNftCheck.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runNftCheck.ts @@ -1,5 +1,5 @@ import { DIAGNOSTICS_CHECKS_MAP } from './contstants'; -import { RemoteFakeIPMethods, PodkopShellMethods } from '../../../methods'; +import { RemoteFakeIPMethods, NetShiftShellMethods } from '../../../methods'; import { updateCheckStore } from './updateCheckStore'; import { getMeta } from '../helpers/getMeta'; @@ -18,7 +18,7 @@ export async function runNftCheck() { await RemoteFakeIPMethods.getFakeIpCheck(); await RemoteFakeIPMethods.getIpCheck(); - const nftablesChecks = await PodkopShellMethods.checkNftRules(); + const nftablesChecks = await NetShiftShellMethods.checkNftRules(); if (!nftablesChecks.success) { updateCheckStore({ diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts similarity index 92% rename from fe-app-podkop/src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts index 35c85ae2..67fb9731 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts @@ -1,5 +1,5 @@ import { DIAGNOSTICS_CHECKS_MAP } from './contstants'; -import { PodkopShellMethods } from '../../../methods'; +import { NetShiftShellMethods } from '../../../methods'; import { updateCheckStore } from './updateCheckStore'; import { getMeta } from '../helpers/getMeta'; import { getDashboardSections } from '../../../methods/custom/getDashboardSections'; @@ -36,9 +36,8 @@ export async function runSectionsCheck() { sections.data.map(async (section) => { async function getLatency() { if (section.withTagSelect) { - const latencyGroup = await PodkopShellMethods.getClashApiGroupLatency( - section.code, - ); + const latencyGroup = + await NetShiftShellMethods.getClashApiGroupLatency(section.code); const selectedOutbound = section.outbounds.find( (item) => item.selected, @@ -82,7 +81,7 @@ export async function runSectionsCheck() { }; } - const latencyProxy = await PodkopShellMethods.getClashApiProxyLatency( + const latencyProxy = await NetShiftShellMethods.getClashApiProxyLatency( section.code, ); diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts similarity index 95% rename from fe-app-podkop/src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts index b13f0bc0..3d5f9bb2 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts @@ -1,5 +1,5 @@ import { DIAGNOSTICS_CHECKS_MAP } from './contstants'; -import { PodkopShellMethods } from '../../../methods'; +import { NetShiftShellMethods } from '../../../methods'; import { updateCheckStore } from './updateCheckStore'; import { getMeta } from '../helpers/getMeta'; @@ -15,7 +15,7 @@ export async function runSingBoxCheck() { items: [], }); - const singBoxChecks = await PodkopShellMethods.checkSingBox(); + const singBoxChecks = await NetShiftShellMethods.checkSingBox(); if (!singBoxChecks.success) { updateCheckStore({ diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/checks/updateCheckStore.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/updateCheckStore.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/checks/updateCheckStore.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/checks/updateCheckStore.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts similarity index 97% rename from fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts index 261f1cdf..9000f4da 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/diagnostic.store.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts @@ -13,8 +13,8 @@ export const initialDiagnosticStore: Pick< > = { diagnosticsSystemInfo: { loading: true, - podkop_version: 'loading', - podkop_latest_version: 'loading', + netshift_version: 'loading', + netshift_latest_version: 'loading', luci_app_version: 'loading', sing_box_version: 'loading', openwrt_version: 'loading', diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getCheckTitle.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/helpers/getCheckTitle.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getCheckTitle.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/helpers/getCheckTitle.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getMeta.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/helpers/getMeta.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getMeta.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/helpers/getMeta.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts similarity index 71% rename from fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts index 32b0688b..6f3b6761 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts @@ -7,32 +7,32 @@ function isUnknownVersion(version?: string | null): boolean { return version === 'unknown' || version === _('unknown'); } -export function getPodkopVersionRow( +export function getNetshiftVersionRow( diagnosticsSystemInfo: StoreType['diagnosticsSystemInfo'], ): IRenderSystemInfoRow { const loading = diagnosticsSystemInfo.loading; - const unknown = isUnknownVersion(diagnosticsSystemInfo.podkop_version); + const unknown = isUnknownVersion(diagnosticsSystemInfo.netshift_version); const hasActualVersion = - Boolean(diagnosticsSystemInfo.podkop_latest_version) && - !isUnknownVersion(diagnosticsSystemInfo.podkop_latest_version); + Boolean(diagnosticsSystemInfo.netshift_latest_version) && + !isUnknownVersion(diagnosticsSystemInfo.netshift_latest_version); const version = normalizeCompiledVersion( - diagnosticsSystemInfo.podkop_version, + diagnosticsSystemInfo.netshift_version, ); const isDevVersion = version === 'dev'; if (loading || unknown || !hasActualVersion || isDevVersion) { return { - key: 'Podkop', + key: 'NetShift', value: version, }; } if ( removeVersionPrefix(version) !== - removeVersionPrefix(diagnosticsSystemInfo.podkop_latest_version) + removeVersionPrefix(diagnosticsSystemInfo.netshift_latest_version) ) { return { - key: 'Podkop', + key: 'NetShift', value: version, tag: { label: _('Outdated'), @@ -42,7 +42,7 @@ export function getPodkopVersionRow( } return { - key: 'Podkop', + key: 'NetShift', value: version, tag: { label: _('Latest'), diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/index.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/index.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/index.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts similarity index 93% rename from fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts index 3e90e9f9..fd515d18 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/initController.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts @@ -11,18 +11,18 @@ import { renderRunAction, renderSystemInfo, } from './partials'; -import { PodkopShellMethods } from '../../methods'; +import { NetShiftShellMethods } from '../../methods'; import { fetchServicesInfo } from '../../fetchers'; import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVersion'; import { renderModal } from '../../../partials'; -import { PODKOP_LUCI_APP_VERSION } from '../../../constants'; +import { NETSHIFT_LUCI_APP_VERSION } from '../../../constants'; import { showToast } from '../../../helpers/showToast'; import { renderWikiDisclaimer } from './partials/renderWikiDisclaimer'; import { runSectionsCheck } from './checks/runSectionsCheck'; -import { getPodkopVersionRow } from './helpers/getPodkopVersionRow'; +import { getNetshiftVersionRow } from './helpers/getNetshiftVersionRow'; async function fetchSystemInfo() { - const systemInfo = await PodkopShellMethods.getSystemInfo(); + const systemInfo = await NetShiftShellMethods.getSystemInfo(); if (systemInfo.success) { store.set({ @@ -36,8 +36,8 @@ async function fetchSystemInfo() { store.set({ diagnosticsSystemInfo: { loading: false, - podkop_version: _('unknown'), - podkop_latest_version: _('unknown'), + netshift_version: _('unknown'), + netshift_latest_version: _('unknown'), luci_app_version: _('unknown'), sing_box_version: _('unknown'), openwrt_version: _('unknown'), @@ -90,7 +90,7 @@ async function handleRestart() { }); try { - await PodkopShellMethods.restart(); + await NetShiftShellMethods.restart(); } catch (e) { logger.error('[DIAGNOSTIC]', 'handleRestart - e', e); } finally { @@ -117,7 +117,7 @@ async function handleStop() { }); try { - await PodkopShellMethods.stop(); + await NetShiftShellMethods.stop(); } catch (e) { logger.error('[DIAGNOSTIC]', 'handleStop - e', e); } finally { @@ -142,7 +142,7 @@ async function handleStart() { }); try { - await PodkopShellMethods.start(); + await NetShiftShellMethods.start(); } catch (e) { logger.error('[DIAGNOSTIC]', 'handleStart - e', e); } finally { @@ -169,7 +169,7 @@ async function handleEnable() { }); try { - await PodkopShellMethods.enable(); + await NetShiftShellMethods.enable(); } catch (e) { logger.error('[DIAGNOSTIC]', 'handleEnable - e', e); } finally { @@ -193,7 +193,7 @@ async function handleDisable() { }); try { - await PodkopShellMethods.disable(); + await NetShiftShellMethods.disable(); } catch (e) { logger.error('[DIAGNOSTIC]', 'handleDisable - e', e); } finally { @@ -217,7 +217,7 @@ async function handleShowGlobalCheck() { }); try { - const globalCheck = await PodkopShellMethods.globalCheck(); + const globalCheck = await NetShiftShellMethods.globalCheck(); if (globalCheck.success) { ui.showModal( @@ -251,7 +251,7 @@ async function handleViewLogs() { }); try { - const viewLogs = await PodkopShellMethods.checkLogs(); + const viewLogs = await NetShiftShellMethods.checkLogs(); if (viewLogs.success) { ui.showModal( @@ -285,7 +285,7 @@ async function handleShowSingBoxConfig() { }); try { - const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig(); + const showSingBoxConfig = await NetShiftShellMethods.showSingBoxConfig(); if (showSingBoxConfig.success) { ui.showModal( @@ -328,7 +328,7 @@ async function handleInstallSingBox() { const isExtended = store.get().diagnosticsSystemInfo.sing_box_extended === 1; try { - const result = await PodkopShellMethods.singBoxComponentAction( + const result = await NetShiftShellMethods.singBoxComponentAction( isExtended ? 'install_stable' : 'install_extended', ); @@ -384,7 +384,7 @@ function renderDiagnosticAvailableActionsWidget() { const servicesInfoWidget = store.get().servicesInfoWidget; logger.debug('[DIAGNOSTIC]', 'renderDiagnosticAvailableActionsWidget'); - const podkopEnabled = Boolean(servicesInfoWidget.data.podkop); + const netshiftEnabled = Boolean(servicesInfoWidget.data.netshift); const singBoxRunning = Boolean(servicesInfoWidget.data.singbox); const atLeastOneServiceCommandLoading = servicesInfoWidget.loading || @@ -415,13 +415,13 @@ function renderDiagnosticAvailableActionsWidget() { }, enable: { loading: diagnosticsActions.enable.loading, - visible: !podkopEnabled, + visible: !netshiftEnabled, onClick: handleEnable, disabled: atLeastOneServiceCommandLoading, }, disable: { loading: diagnosticsActions.disable.loading, - visible: podkopEnabled, + visible: netshiftEnabled, onClick: handleDisable, disabled: atLeastOneServiceCommandLoading, }, @@ -467,10 +467,10 @@ function renderDiagnosticSystemInfoWidget() { const renderedSystemInfo = renderSystemInfo({ items: [ - getPodkopVersionRow(diagnosticsSystemInfo), + getNetshiftVersionRow(diagnosticsSystemInfo), { key: 'Luci App', - value: normalizeCompiledVersion(PODKOP_LUCI_APP_VERSION), + value: normalizeCompiledVersion(NETSHIFT_LUCI_APP_VERSION), }, { key: 'Sing-box', diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/index.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/partials/index.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/partials/index.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts similarity index 97% rename from fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts index 65ac61c8..4de4d208 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts @@ -50,7 +50,7 @@ export function renderAvailableActions({ classNames: ['cbi-button-apply'], onClick: restart.onClick, icon: renderRotateCcwIcon24, - text: _('Restart podkop'), + text: _('Restart NetShift'), loading: restart.loading, disabled: restart.disabled, }), @@ -60,7 +60,7 @@ export function renderAvailableActions({ classNames: ['cbi-button-remove'], onClick: stop.onClick, icon: renderCircleStopIcon24, - text: _('Stop podkop'), + text: _('Stop NetShift'), loading: stop.loading, disabled: stop.disabled, }), @@ -70,7 +70,7 @@ export function renderAvailableActions({ classNames: ['cbi-button-save'], onClick: start.onClick, icon: renderCirclePlayIcon24, - text: _('Start podkop'), + text: _('Start NetShift'), loading: start.loading, disabled: start.disabled, }), diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderCheckSection.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderCheckSection.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderCheckSection.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderCheckSection.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderRunAction.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderRunAction.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderRunAction.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderRunAction.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts similarity index 94% rename from fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts index 9511e80a..adb8801a 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts @@ -31,7 +31,7 @@ export function renderWikiDisclaimer(kind: 'default' | 'error' | 'warning') { text: _('Visit Wiki'), onClick: () => window.open( - 'https://podkop.net/docs/troubleshooting/?utm_source=podkop', + 'https://podkop.net/docs/troubleshooting/?utm_source=netshift', '_blank', 'noopener,noreferrer', ), diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/renderDiagnostic.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/renderDiagnostic.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/diagnostic/renderDiagnostic.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/renderDiagnostic.ts diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/styles.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/styles.ts similarity index 98% rename from fe-app-podkop/src/podkop/tabs/diagnostic/styles.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/styles.ts index def1d781..6d258aab 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/styles.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/styles.ts @@ -1,11 +1,11 @@ // language=CSS export const styles = ` -#cbi-podkop-diagnostic-_mount_node > div { +#cbi-netshift-diagnostic-_mount_node > div { width: 100%; } -#cbi-podkop-diagnostic > h3 { +#cbi-netshift-diagnostic > h3 { display: none; } diff --git a/fe-app-podkop/src/podkop/tabs/diagnostic/tests/getPodkopVersionRow.test.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/tests/getNetshiftVersionRow.test.ts similarity index 66% rename from fe-app-podkop/src/podkop/tabs/diagnostic/tests/getPodkopVersionRow.test.ts rename to fe-app-netshift/src/netshift/tabs/diagnostic/tests/getNetshiftVersionRow.test.ts index 88642f42..e8d93c89 100644 --- a/fe-app-podkop/src/podkop/tabs/diagnostic/tests/getPodkopVersionRow.test.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/tests/getNetshiftVersionRow.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { getPodkopVersionRow } from '../helpers/getPodkopVersionRow'; +import { getNetshiftVersionRow } from '../helpers/getNetshiftVersionRow'; import type { StoreType } from '../../../services/store.service'; function makeDiagnosticsSystemInfo( @@ -7,8 +7,8 @@ function makeDiagnosticsSystemInfo( ): StoreType['diagnosticsSystemInfo'] { return { loading: false, - podkop_version: '1.2.3', - podkop_latest_version: '1.2.3', + netshift_version: '1.2.3', + netshift_latest_version: '1.2.3', luci_app_version: '1.0.0', sing_box_version: '1.11.0', openwrt_version: 'OpenWrt 25.12', @@ -17,17 +17,17 @@ function makeDiagnosticsSystemInfo( }; } -describe('getPodkopVersionRow', () => { +describe('getNetshiftVersionRow', () => { it('returns Latest when versions differ only by leading v', () => { - const row = getPodkopVersionRow( + const row = getNetshiftVersionRow( makeDiagnosticsSystemInfo({ - podkop_version: 'v1.2.3', - podkop_latest_version: '1.2.3', + netshift_version: 'v1.2.3', + netshift_latest_version: '1.2.3', }), ); expect(row).toEqual({ - key: 'Podkop', + key: 'NetShift', value: 'v1.2.3', tag: { label: 'Latest', @@ -37,15 +37,15 @@ describe('getPodkopVersionRow', () => { }); it('returns Outdated when versions differ', () => { - const row = getPodkopVersionRow( + const row = getNetshiftVersionRow( makeDiagnosticsSystemInfo({ - podkop_version: '1.2.2', - podkop_latest_version: '1.2.3', + netshift_version: '1.2.2', + netshift_latest_version: '1.2.3', }), ); expect(row).toEqual({ - key: 'Podkop', + key: 'NetShift', value: '1.2.2', tag: { label: 'Outdated', @@ -55,14 +55,14 @@ describe('getPodkopVersionRow', () => { }); it('returns plain row without tag for dev build', () => { - const row = getPodkopVersionRow( + const row = getNetshiftVersionRow( makeDiagnosticsSystemInfo({ - podkop_version: 'COMPILED_VERSION', + netshift_version: 'COMPILED_VERSION', }), ); expect(row).toEqual({ - key: 'Podkop', + key: 'NetShift', value: 'dev', }); }); diff --git a/fe-app-podkop/src/podkop/tabs/index.ts b/fe-app-netshift/src/netshift/tabs/index.ts similarity index 100% rename from fe-app-podkop/src/podkop/tabs/index.ts rename to fe-app-netshift/src/netshift/tabs/index.ts diff --git a/fe-app-podkop/src/podkop/types.ts b/fe-app-netshift/src/netshift/types.ts similarity index 88% rename from fe-app-podkop/src/podkop/types.ts rename to fe-app-netshift/src/netshift/types.ts index f336ab6f..2994034c 100644 --- a/fe-app-podkop/src/podkop/types.ts +++ b/fe-app-netshift/src/netshift/types.ts @@ -20,30 +20,30 @@ export namespace ClashAPI { } // eslint-disable-next-line @typescript-eslint/no-namespace -export namespace Podkop { +export namespace NetShift { // Available commands: - // start Start podkop service - // stop Stop podkop service - // reload Reload podkop configuration - // restart Restart podkop service - // enable Enable podkop autostart - // disable Disable podkop autostart - // main Run main podkop process + // start Start netshift service + // stop Stop netshift service + // reload Reload netshift configuration + // restart Restart netshift service + // enable Enable netshift autostart + // disable Disable netshift autostart + // main Run main netshift process // list_update Update domain lists // check_proxy Check proxy connectivity // check_nft Check NFT rules // check_nft_rules Check NFT rules status // check_sing_box Check sing-box installation and status - // check_logs Show podkop logs from system journal + // check_logs Show netshift logs from system journal // check_sing_box_logs Show sing-box logs // check_fakeip Test FakeIP on router // clash_api Clash API interface for managing proxies and groups - // show_config Display current podkop configuration - // show_version Show podkop version + // show_config Display current netshift configuration + // show_version Show netshift version // show_sing_box_config Show sing-box configuration // show_sing_box_version Show sing-box version // show_system_info Show system information - // get_status Get podkop service status + // get_status Get netshift service status // get_sing_box_status Get sing-box service status // check_dns_available Check DNS server availability // global_check Run global system check @@ -212,8 +212,8 @@ export namespace Podkop { } export interface GetSystemInfo { - podkop_version: string; - podkop_latest_version: string; + netshift_version: string; + netshift_latest_version: string; luci_app_version: string; sing_box_version: string; openwrt_version: string; diff --git a/fe-app-podkop/src/partials/button/renderButton.ts b/fe-app-netshift/src/partials/button/renderButton.ts similarity index 100% rename from fe-app-podkop/src/partials/button/renderButton.ts rename to fe-app-netshift/src/partials/button/renderButton.ts diff --git a/fe-app-podkop/src/partials/button/styles.ts b/fe-app-netshift/src/partials/button/styles.ts similarity index 100% rename from fe-app-podkop/src/partials/button/styles.ts rename to fe-app-netshift/src/partials/button/styles.ts diff --git a/fe-app-podkop/src/partials/index.ts b/fe-app-netshift/src/partials/index.ts similarity index 100% rename from fe-app-podkop/src/partials/index.ts rename to fe-app-netshift/src/partials/index.ts diff --git a/fe-app-podkop/src/partials/modal/renderModal.ts b/fe-app-netshift/src/partials/modal/renderModal.ts similarity index 100% rename from fe-app-podkop/src/partials/modal/renderModal.ts rename to fe-app-netshift/src/partials/modal/renderModal.ts diff --git a/fe-app-podkop/src/partials/modal/styles.ts b/fe-app-netshift/src/partials/modal/styles.ts similarity index 100% rename from fe-app-podkop/src/partials/modal/styles.ts rename to fe-app-netshift/src/partials/modal/styles.ts diff --git a/fe-app-podkop/src/styles.ts b/fe-app-netshift/src/styles.ts similarity index 91% rename from fe-app-podkop/src/styles.ts rename to fe-app-netshift/src/styles.ts index e40000dd..b553b600 100644 --- a/fe-app-podkop/src/styles.ts +++ b/fe-app-netshift/src/styles.ts @@ -1,5 +1,5 @@ // language=CSS -import { DashboardTab, DiagnosticTab } from './podkop'; +import { DashboardTab, DiagnosticTab } from './netshift'; import { PartialStyles } from './partials'; export const GlobalStyles = ` @@ -9,17 +9,17 @@ ${PartialStyles} /* Hide extra H3 for settings tab */ -#cbi-podkop-settings > h3 { +#cbi-netshift-settings > h3 { display: none; } /* Hide extra H3 for sections tab */ -#cbi-podkop-section > h3:nth-child(1) { +#cbi-netshift-section > h3:nth-child(1) { display: none; } /* Vertical align for remove section action button */ -#cbi-podkop-section > .cbi-section-remove { +#cbi-netshift-section > .cbi-section-remove { margin-bottom: -32px; } diff --git a/fe-app-podkop/src/validators/bulkValidate.ts b/fe-app-netshift/src/validators/bulkValidate.ts similarity index 100% rename from fe-app-podkop/src/validators/bulkValidate.ts rename to fe-app-netshift/src/validators/bulkValidate.ts diff --git a/fe-app-podkop/src/validators/index.ts b/fe-app-netshift/src/validators/index.ts similarity index 100% rename from fe-app-podkop/src/validators/index.ts rename to fe-app-netshift/src/validators/index.ts diff --git a/fe-app-podkop/src/validators/tests/validateDns.test.js b/fe-app-netshift/src/validators/tests/validateDns.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateDns.test.js rename to fe-app-netshift/src/validators/tests/validateDns.test.js diff --git a/fe-app-podkop/src/validators/tests/validateDomain.test.js b/fe-app-netshift/src/validators/tests/validateDomain.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateDomain.test.js rename to fe-app-netshift/src/validators/tests/validateDomain.test.js diff --git a/fe-app-podkop/src/validators/tests/validateHysteriaUrl.test.js b/fe-app-netshift/src/validators/tests/validateHysteriaUrl.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateHysteriaUrl.test.js rename to fe-app-netshift/src/validators/tests/validateHysteriaUrl.test.js diff --git a/fe-app-podkop/src/validators/tests/validateIp.test.js b/fe-app-netshift/src/validators/tests/validateIp.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateIp.test.js rename to fe-app-netshift/src/validators/tests/validateIp.test.js diff --git a/fe-app-podkop/src/validators/tests/validatePath.test.js b/fe-app-netshift/src/validators/tests/validatePath.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validatePath.test.js rename to fe-app-netshift/src/validators/tests/validatePath.test.js diff --git a/fe-app-podkop/src/validators/tests/validateShadowsocksUrl.test.js b/fe-app-netshift/src/validators/tests/validateShadowsocksUrl.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateShadowsocksUrl.test.js rename to fe-app-netshift/src/validators/tests/validateShadowsocksUrl.test.js diff --git a/fe-app-podkop/src/validators/tests/validateSocksUrl.test.js b/fe-app-netshift/src/validators/tests/validateSocksUrl.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateSocksUrl.test.js rename to fe-app-netshift/src/validators/tests/validateSocksUrl.test.js diff --git a/fe-app-podkop/src/validators/tests/validateSubnet.test.js b/fe-app-netshift/src/validators/tests/validateSubnet.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateSubnet.test.js rename to fe-app-netshift/src/validators/tests/validateSubnet.test.js diff --git a/fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js b/fe-app-netshift/src/validators/tests/validateTrojanUrl.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js rename to fe-app-netshift/src/validators/tests/validateTrojanUrl.test.js diff --git a/fe-app-podkop/src/validators/tests/validateUrl.test.js b/fe-app-netshift/src/validators/tests/validateUrl.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateUrl.test.js rename to fe-app-netshift/src/validators/tests/validateUrl.test.js diff --git a/fe-app-podkop/src/validators/tests/validateVlessUrl.test.js b/fe-app-netshift/src/validators/tests/validateVlessUrl.test.js similarity index 100% rename from fe-app-podkop/src/validators/tests/validateVlessUrl.test.js rename to fe-app-netshift/src/validators/tests/validateVlessUrl.test.js diff --git a/fe-app-podkop/src/validators/types.ts b/fe-app-netshift/src/validators/types.ts similarity index 100% rename from fe-app-podkop/src/validators/types.ts rename to fe-app-netshift/src/validators/types.ts diff --git a/fe-app-podkop/src/validators/validateDns.ts b/fe-app-netshift/src/validators/validateDns.ts similarity index 100% rename from fe-app-podkop/src/validators/validateDns.ts rename to fe-app-netshift/src/validators/validateDns.ts diff --git a/fe-app-podkop/src/validators/validateDomain.ts b/fe-app-netshift/src/validators/validateDomain.ts similarity index 100% rename from fe-app-podkop/src/validators/validateDomain.ts rename to fe-app-netshift/src/validators/validateDomain.ts diff --git a/fe-app-podkop/src/validators/validateHysteriaUrl.ts b/fe-app-netshift/src/validators/validateHysteriaUrl.ts similarity index 100% rename from fe-app-podkop/src/validators/validateHysteriaUrl.ts rename to fe-app-netshift/src/validators/validateHysteriaUrl.ts diff --git a/fe-app-podkop/src/validators/validateIp.ts b/fe-app-netshift/src/validators/validateIp.ts similarity index 100% rename from fe-app-podkop/src/validators/validateIp.ts rename to fe-app-netshift/src/validators/validateIp.ts diff --git a/fe-app-podkop/src/validators/validateOutboundJson.ts b/fe-app-netshift/src/validators/validateOutboundJson.ts similarity index 100% rename from fe-app-podkop/src/validators/validateOutboundJson.ts rename to fe-app-netshift/src/validators/validateOutboundJson.ts diff --git a/fe-app-podkop/src/validators/validatePath.ts b/fe-app-netshift/src/validators/validatePath.ts similarity index 100% rename from fe-app-podkop/src/validators/validatePath.ts rename to fe-app-netshift/src/validators/validatePath.ts diff --git a/fe-app-podkop/src/validators/validateProxyUrl.ts b/fe-app-netshift/src/validators/validateProxyUrl.ts similarity index 100% rename from fe-app-podkop/src/validators/validateProxyUrl.ts rename to fe-app-netshift/src/validators/validateProxyUrl.ts diff --git a/fe-app-podkop/src/validators/validateShadowsocksUrl.ts b/fe-app-netshift/src/validators/validateShadowsocksUrl.ts similarity index 100% rename from fe-app-podkop/src/validators/validateShadowsocksUrl.ts rename to fe-app-netshift/src/validators/validateShadowsocksUrl.ts diff --git a/fe-app-podkop/src/validators/validateSocksUrl.ts b/fe-app-netshift/src/validators/validateSocksUrl.ts similarity index 100% rename from fe-app-podkop/src/validators/validateSocksUrl.ts rename to fe-app-netshift/src/validators/validateSocksUrl.ts diff --git a/fe-app-podkop/src/validators/validateSubnet.ts b/fe-app-netshift/src/validators/validateSubnet.ts similarity index 100% rename from fe-app-podkop/src/validators/validateSubnet.ts rename to fe-app-netshift/src/validators/validateSubnet.ts diff --git a/fe-app-podkop/src/validators/validateTrojanUrl.ts b/fe-app-netshift/src/validators/validateTrojanUrl.ts similarity index 100% rename from fe-app-podkop/src/validators/validateTrojanUrl.ts rename to fe-app-netshift/src/validators/validateTrojanUrl.ts diff --git a/fe-app-podkop/src/validators/validateUrl.ts b/fe-app-netshift/src/validators/validateUrl.ts similarity index 100% rename from fe-app-podkop/src/validators/validateUrl.ts rename to fe-app-netshift/src/validators/validateUrl.ts diff --git a/fe-app-podkop/src/validators/validateVlessUrl.ts b/fe-app-netshift/src/validators/validateVlessUrl.ts similarity index 100% rename from fe-app-podkop/src/validators/validateVlessUrl.ts rename to fe-app-netshift/src/validators/validateVlessUrl.ts diff --git a/fe-app-podkop/tests/setup/global-mocks.ts b/fe-app-netshift/tests/setup/global-mocks.ts similarity index 100% rename from fe-app-podkop/tests/setup/global-mocks.ts rename to fe-app-netshift/tests/setup/global-mocks.ts diff --git a/fe-app-podkop/tsconfig.json b/fe-app-netshift/tsconfig.json similarity index 100% rename from fe-app-podkop/tsconfig.json rename to fe-app-netshift/tsconfig.json diff --git a/fe-app-podkop/tsup.config.ts b/fe-app-netshift/tsup.config.ts similarity index 85% rename from fe-app-podkop/tsup.config.ts rename to fe-app-netshift/tsup.config.ts index 6caf4791..8f210667 100644 --- a/fe-app-podkop/tsup.config.ts +++ b/fe-app-netshift/tsup.config.ts @@ -5,7 +5,7 @@ import path from 'path'; export default defineConfig({ entry: ['src/main.ts'], format: ['esm'], // пусть tsup генерит export {...} - outDir: '../luci-app-podkop/htdocs/luci-static/resources/view/podkop', + outDir: '../luci-app-netshift/htdocs/luci-static/resources/view/netshift', outExtension: () => ({ js: '.js' }), dts: false, clean: false, @@ -18,7 +18,7 @@ export default defineConfig({ }, onSuccess: () => { const outDir = - '../luci-app-podkop/htdocs/luci-static/resources/view/podkop'; + '../luci-app-netshift/htdocs/luci-static/resources/view/netshift'; const file = path.join(outDir, 'main.js'); let code = fs.readFileSync(file, 'utf8'); diff --git a/fe-app-podkop/vitest.config.js b/fe-app-netshift/vitest.config.js similarity index 100% rename from fe-app-podkop/vitest.config.js rename to fe-app-netshift/vitest.config.js diff --git a/fe-app-podkop/watch-upload.js b/fe-app-netshift/watch-upload.js similarity index 87% rename from fe-app-podkop/watch-upload.js rename to fe-app-netshift/watch-upload.js index 0f578328..4b3ea844 100644 --- a/fe-app-podkop/watch-upload.js +++ b/fe-app-netshift/watch-upload.js @@ -18,19 +18,19 @@ const config = { const syncDirs = [ { - local: path.resolve(process.env.LOCAL_DIR_FE ?? '../luci-app-podkop/htdocs/luci-static/resources/view/podkop'), - remote: process.env.REMOTE_DIR_FE ?? '/www/luci-static/resources/view/podkop', + local: path.resolve(process.env.LOCAL_DIR_FE ?? '../luci-app-netshift/htdocs/luci-static/resources/view/netshift'), + remote: process.env.REMOTE_DIR_FE ?? '/www/luci-static/resources/view/netshift', }, { - local: path.resolve(process.env.LOCAL_DIR_BIN ?? '../podkop/files/usr/bin/'), + local: path.resolve(process.env.LOCAL_DIR_BIN ?? '../netshift/files/usr/bin/'), remote: process.env.REMOTE_DIR_BIN ?? '/usr/bin/', }, { - local: path.resolve(process.env.LOCAL_DIR_LIB ?? '../podkop/files/usr/lib/'), - remote: process.env.REMOTE_DIR_LIB ?? '/usr/lib/podkop/', + local: path.resolve(process.env.LOCAL_DIR_LIB ?? '../netshift/files/usr/lib/'), + remote: process.env.REMOTE_DIR_LIB ?? '/usr/lib/netshift/', }, { - local: path.resolve(process.env.LOCAL_DIR_INIT ?? '../podkop/files/etc/init.d/'), + local: path.resolve(process.env.LOCAL_DIR_INIT ?? '../netshift/files/etc/init.d/'), remote: process.env.REMOTE_DIR_INIT ?? '/etc/init.d/', } ]; diff --git a/fe-app-podkop/yarn.lock b/fe-app-netshift/yarn.lock similarity index 100% rename from fe-app-podkop/yarn.lock rename to fe-app-netshift/yarn.lock diff --git a/fe-app-podkop/.env.example b/fe-app-podkop/.env.example deleted file mode 100644 index b82d0b60..00000000 --- a/fe-app-podkop/.env.example +++ /dev/null @@ -1,16 +0,0 @@ -SFTP_HOST=192.168.160.129 -SFTP_PORT=22 -SFTP_USER=root -SFTP_PASS= - -# you can use key if needed -# SFTP_PRIVATE_KEY=~/.ssh/id_rsa - -LOCAL_DIR_FE=../luci-app-podkop/htdocs/luci-static/resources/view/podkop -REMOTE_DIR_FE=/www/luci-static/resources/view/podkop - -LOCAL_DIR_BIN=../podkop/files/usr/bin/ -REMOTE_DIR_BIN=/usr/bin/ - -LOCAL_DIR_LIB=../podkop/files/usr/lib/ -REMOTE_DIR_LIB=/usr/lib/podkop/ diff --git a/fe-app-podkop/locales/podkop.pot b/fe-app-podkop/locales/podkop.pot deleted file mode 100644 index a722424c..00000000 --- a/fe-app-podkop/locales/podkop.pot +++ /dev/null @@ -1,1183 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PODKOP package. -# yandexru45 , 2026. -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PODKOP\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-02 11:25+0300\n" -"PO-Revision-Date: 2026-06-02 11:25+0300\n" -"Last-Translator: yandexru45 \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: src\podkop\tabs\dashboard\initController.ts:345 -msgid "✔ Enabled" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:356 -msgid "✔ Running" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:346 -msgid "✘ Disabled" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:357 -msgid "✘ Stopped" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:127 -msgid "Группировать по странам" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:128 -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:307 -msgid "Active Connections" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:106 -msgid "Additional marking rules found" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:247 -msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:251 -msgid "Applicable for SOCKS and Shadowsocks proxy" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:496 -msgid "At least one valid domain must be specified. Comments-only content is not allowed." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:577 -msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:47 -msgid "Available actions" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:65 -msgid "Bootsrap DNS" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:45 -msgid "Bootstrap DNS server" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:58 -msgid "Browser is not using FakeIP" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:57 -msgid "Browser is using FakeIP correctly" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:348 -msgid "Cache File Path" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:362 -msgid "Cache file path cannot be empty" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:27 -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:28 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:27 -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:25 -msgid "Cannot receive checks result" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:15 -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:15 -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:13 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:15 -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:13 -msgid "Checking, please wait" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getCheckTitle.ts:2 -msgid "checks" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:26 -msgid "Checks failed" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:13 -msgid "Checks passed" -msgstr "" - -#: src\validators\validateSubnet.ts:33 -msgid "CIDR must be between 0 and 32" -msgstr "" - -#: src\partials\modal\renderModal.ts:26 -msgid "Close" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:351 -msgid "Community Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:335 -msgid "Config File Path" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:27 -msgid "Configuration for Podkop service" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:23 -msgid "Configuration Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:12 -msgid "Connection Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:26 -msgid "Connection URL" -msgstr "" - -#: src\partials\modal\renderModal.ts:20 -msgid "Copy" -msgstr "" - -#: src\podkop\tabs\dashboard\partials\renderWidget.ts:22 -msgid "Currently unavailable" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:80 -msgid "Dashboard" -msgstr "" - -#: src\podkop\tabs\dashboard\partials\renderSections.ts:19 -msgid "Dashboard currently unavailable" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:222 -msgid "Delay in milliseconds before reloading podkop after interface UP" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:229 -msgid "Delay value cannot be empty" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:82 -msgid "DHCP has DNS server" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:65 -msgid "Diagnostics" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:83 -msgid "Disable autostart" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:265 -msgid "Disable QUIC" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:266 -msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:442 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:522 -msgid "Disabled" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:77 -msgid "DNS on router" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:319 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:15 -msgid "DNS over HTTPS (DoH)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:320 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:16 -msgid "DNS over TLS (DoT)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:316 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:12 -msgid "DNS Protocol Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:68 -msgid "DNS Rewrite TTL" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:329 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:24 -msgid "DNS Server" -msgstr "" - -#: src\validators\validateDns.ts:7 -msgid "DNS server address cannot be empty" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:26 -msgid "Do not panic, everything can be fixed, just..." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:306 -msgid "Domain Resolver" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:326 -msgid "Dont Touch My DHCP!" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:241 -#: src\podkop\tabs\dashboard\initController.ts:275 -msgid "Downlink" -msgstr "" - -#: src\partials\modal\renderModal.ts:15 -msgid "Download" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:288 -msgid "Download Lists via Proxy/VPN" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:297 -msgid "Download Lists via specific proxy section" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:289 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:298 -msgid "Downloading all lists via specific Proxy/VPN" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:443 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:523 -msgid "Dynamic List" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:93 -msgid "Enable autostart" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:307 -msgid "Enable built-in DNS resolver for domains handled by this section" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:746 -msgid "Enable DNS resolve to get real IP when routing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:717 -msgid "Enable Mixed Proxy" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:126 -msgid "Enable Output Network Interface" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:718 -msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:237 -msgid "Enable YACD" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:246 -msgid "Enable YACD WAN Access" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:67 -msgid "Enter complete outbound configuration in JSON format" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:478 -msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:452 -msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:532 -msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:90 -msgid "Enter the subscription URL to fetch proxy configurations from your provider" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:187 -msgid "Every 1 minute" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:119 -msgid "Every 12 hours" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:117 -msgid "Every 3 hours" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:188 -msgid "Every 3 minutes" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:115 -msgid "Every 30 minutes" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:186 -msgid "Every 30 seconds" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:189 -msgid "Every 5 minutes" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:118 -msgid "Every 6 hours" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:120 -msgid "Every day" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:116 -msgid "Every hour" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:402 -msgid "Exclude NTP" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:403 -msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" -msgstr "" - -#: src\helpers\copyToClipboard.ts:12 -msgid "Failed to copy!" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:229 -#: src\podkop\tabs\diagnostic\initController.ts:233 -#: src\podkop\tabs\diagnostic\initController.ts:263 -#: src\podkop\tabs\diagnostic\initController.ts:267 -#: src\podkop\tabs\diagnostic\initController.ts:304 -#: src\podkop\tabs\diagnostic\initController.ts:308 -#: src\podkop\tabs\diagnostic\initController.ts:342 -#: src\podkop\tabs\diagnostic\initController.ts:346 -msgid "Failed to execute!" -msgstr "" - -#: src\podkop\methods\custom\getDashboardSections.ts:150 -#: src\podkop\methods\custom\getDashboardSections.ts:181 -#: src\podkop\methods\custom\getDashboardSections.ts:218 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:59 -msgid "Fastest" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:690 -msgid "Fully Routed IPs" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:102 -msgid "Get global check" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:224 -msgid "Global check" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:113 -msgid "How often to automatically update the subscription" -msgstr "" - -#: src\podkop\api.ts:27 -msgid "HTTP error" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:129 -msgid "Install extended" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:129 -msgid "Install stable" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:189 -msgid "Interface Monitoring" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:221 -msgid "Interface Monitoring Delay" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:190 -msgid "Interface monitoring for Bad WAN" -msgstr "" - -#: src\validators\validateDns.ts:23 -msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" -msgstr "" - -#: src\validators\validateDomain.ts:18 -#: src\validators\validateDomain.ts:27 -msgid "Invalid domain address" -msgstr "" - -#: src\validators\validateSubnet.ts:11 -msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:90 -msgid "Invalid HY2 URL: insecure must be 0 or 1" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:77 -msgid "Invalid HY2 URL: invalid port number" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:30 -msgid "Invalid HY2 URL: missing credentials/server" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:47 -msgid "Invalid HY2 URL: missing host" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:41 -msgid "Invalid HY2 URL: missing host & port" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:36 -msgid "Invalid HY2 URL: missing password" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:50 -msgid "Invalid HY2 URL: missing port" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:18 -msgid "Invalid HY2 URL: must not contain spaces" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:12 -msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:108 -msgid "Invalid HY2 URL: obfs-password required when obfs is set" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:122 -msgid "Invalid HY2 URL: parsing failed" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:116 -msgid "Invalid HY2 URL: sni cannot be empty" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:98 -msgid "Invalid HY2 URL: unsupported obfs type" -msgstr "" - -#: src\validators\validateIp.ts:11 -msgid "Invalid IP address" -msgstr "" - -#: src\validators\validateOutboundJson.ts:9 -msgid "Invalid JSON format" -msgstr "" - -#: src\validators\validatePath.ts:22 -msgid "Invalid path format. Path must start with \"/\" and contain valid characters" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:85 -msgid "Invalid port number. Must be between 1 and 65535" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:37 -msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:27 -msgid "Invalid Shadowsocks URL: missing credentials" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:46 -msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:76 -msgid "Invalid Shadowsocks URL: missing port" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:67 -msgid "Invalid Shadowsocks URL: missing server" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:58 -msgid "Invalid Shadowsocks URL: missing server address" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:16 -msgid "Invalid Shadowsocks URL: must not contain spaces" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:8 -msgid "Invalid Shadowsocks URL: must start with ss://" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:91 -msgid "Invalid Shadowsocks URL: parsing failed" -msgstr "" - -#: src\validators\validateSocksUrl.ts:73 -msgid "Invalid SOCKS URL: invalid host format" -msgstr "" - -#: src\validators\validateSocksUrl.ts:63 -msgid "Invalid SOCKS URL: invalid port number" -msgstr "" - -#: src\validators\validateSocksUrl.ts:42 -msgid "Invalid SOCKS URL: missing host and port" -msgstr "" - -#: src\validators\validateSocksUrl.ts:51 -msgid "Invalid SOCKS URL: missing hostname or IP" -msgstr "" - -#: src\validators\validateSocksUrl.ts:56 -msgid "Invalid SOCKS URL: missing port" -msgstr "" - -#: src\validators\validateSocksUrl.ts:34 -msgid "Invalid SOCKS URL: missing username" -msgstr "" - -#: src\validators\validateSocksUrl.ts:19 -msgid "Invalid SOCKS URL: must not contain spaces" -msgstr "" - -#: src\validators\validateSocksUrl.ts:10 -msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" -msgstr "" - -#: src\validators\validateSocksUrl.ts:77 -msgid "Invalid SOCKS URL: parsing failed" -msgstr "" - -#: src\validators\validateTrojanUrl.ts:15 -msgid "Invalid Trojan URL: must not contain spaces" -msgstr "" - -#: src\validators\validateTrojanUrl.ts:8 -msgid "Invalid Trojan URL: must start with trojan://" -msgstr "" - -#: src\validators\validateTrojanUrl.ts:56 -msgid "Invalid Trojan URL: parsing failed" -msgstr "" - -#: src\validators\validateUrl.ts:8 -#: src\validators\validateUrl.ts:31 -msgid "Invalid URL format" -msgstr "" - -#: src\validators\validateVlessUrl.ts:110 -msgid "Invalid VLESS URL: parsing failed" -msgstr "" - -#: src\validators\validateSubnet.ts:18 -msgid "IP address 0.0.0.0 is not allowed" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:20 -msgid "Issues detected" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:48 -msgid "Latest" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:276 -msgid "List Update Frequency" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:598 -msgid "Local Domain Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:621 -msgid "Local Subnet Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:384 -msgid "Log Level" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:72 -msgid "Main DNS" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:311 -msgid "Memory Usage" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:730 -msgid "Mixed Proxy Port" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:198 -msgid "Monitored Interfaces" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:215 -msgid "Must be a number in the range of 50 - 1000" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:260 -msgid "Network Interface" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:105 -msgid "No other marking rules found" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderCheckSection.ts:189 -msgid "Not implement yet" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:75 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:81 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:100 -msgid "Not responding" -msgstr "" - -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:59 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:67 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:75 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:83 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:91 -msgid "Not running" -msgstr "" - -#: src\helpers\withTimeout.ts:7 -msgid "Operation timed out" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:30 -msgid "Outbound Config" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:66 -msgid "Outbound Configuration" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:38 -msgid "Outdated" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:135 -msgid "Output Network Interface" -msgstr "" - -#: src\validators\validatePath.ts:7 -msgid "Path cannot be empty" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:366 -msgid "Path must be absolute (start with /)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:375 -msgid "Path must contain at least one directory (like /tmp/cache.db)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:370 -msgid "Path must end with cache.db" -msgstr "" - -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:107 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:115 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:123 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:131 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:139 -msgid "Pending" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:343 -msgid "Podkop" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:26 -msgid "Podkop Settings" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:327 -msgid "Podkop will not modify your DHCP configuration" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:37 -msgid "Proxy Configuration URL" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:66 -msgid "Proxy traffic is not routed via FakeIP" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:65 -msgid "Proxy traffic is routed via FakeIP" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:385 -msgid "Regional options cannot be used together" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:644 -msgid "Remote Domain Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:667 -msgid "Remote Subnet Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:745 -msgid "Resolve real IP for routing" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:53 -msgid "Restart podkop" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:51 -msgid "Router DNS is not routed through sing-box" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:50 -msgid "Router DNS is routed through sing-box" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:413 -msgid "Routing Excluded IPs" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:79 -msgid "Rules mangle counters" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:74 -msgid "Rules mangle exist" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:89 -msgid "Rules mangle output counters" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:84 -msgid "Rules mangle output exist" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:99 -msgid "Rules proxy counters" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:94 -msgid "Rules proxy exist" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderRunAction.ts:15 -msgid "Run Diagnostic" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:404 -msgid "Russia inside restrictions" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:257 -msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:36 -msgid "Sections" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:352 -msgid "Select a predefined list for routing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:13 -msgid "Select between VPN and Proxy connection methods for traffic routing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:13 -msgid "Select DNS protocol to use" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:277 -msgid "Select how often the domain or subnet lists are updated automatically" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:24 -msgid "Select how to configure the proxy" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:261 -msgid "Select network interface for VPN connection" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:330 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:25 -msgid "Select or enter DNS server address" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:349 -msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:336 -msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:317 -msgid "Select the DNS protocol type for the domain resolver" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:440 -msgid "Select the list type for adding custom domains" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:520 -msgid "Select the list type for adding custom subnets" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:385 -msgid "Select the log level for sing-box" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:90 -msgid "Select the network interface from which the traffic will originate" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:136 -msgid "Select the network interface to which the traffic will originate" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:199 -msgid "Select the WAN interfaces to be monitored" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:27 -msgid "Selector" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:137 -msgid "Selector Proxy Links" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:340 -msgid "Services info" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:49 -msgid "Settings" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:292 -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:120 -msgid "Show sing-box config" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:354 -msgid "Sing-box" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:77 -msgid "Sing-box autostart disabled" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:337 -msgid "Sing-box core changed, version:" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:62 -msgid "Sing-box installed" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:87 -msgid "Sing-box listening ports" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:82 -msgid "Sing-box process running" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:72 -msgid "Sing-box service exist" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:67 -msgid "Sing-box version is compatible (newer than 1.12.4)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:89 -msgid "Source Network Interface" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:414 -msgid "Specify a local IP address to be excluded from routing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:691 -msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:645 -msgid "Specify remote URLs to download and use domain lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:668 -msgid "Specify remote URLs to download and use subnet lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:599 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:622 -msgid "Specify the path to the list file located on the router filesystem" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:73 -msgid "Start podkop" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:63 -msgid "Stop podkop" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:29 -msgid "Subscription" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:112 -msgid "Subscription Update Interval" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:89 -msgid "Subscription URL" -msgstr "" - -#: src\helpers\copyToClipboard.ts:10 -msgid "Successfully copied!" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:304 -msgid "System info" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderSystemInfo.ts:21 -msgid "System information" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:69 -msgid "Table exist" -msgstr "" - -#: src\podkop\tabs\dashboard\partials\renderSections.ts:108 -msgid "Test latency" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:444 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:524 -msgid "Text List" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:46 -msgid "The DNS server used to look up the IP address of an upstream DNS server" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:184 -msgid "The interval between connectivity tests" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:198 -msgid "The maximum difference in response times (ms) allowed when comparing servers" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:222 -msgid "The URL used to test server connectivity" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:69 -msgid "Time in seconds for DNS record caching (default: 60)" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:238 -msgid "Traffic" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:268 -msgid "Traffic Total" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:25 -msgid "Troubleshooting" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:80 -msgid "TTL must be a positive number" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:75 -msgid "TTL value cannot be empty" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:321 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:17 -msgid "UDP (Unprotected DNS)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:250 -msgid "UDP over TCP" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:39 -#: src\podkop\tabs\diagnostic\initController.ts:40 -#: src\podkop\tabs\diagnostic\initController.ts:41 -#: src\podkop\tabs\diagnostic\initController.ts:42 -#: src\podkop\tabs\diagnostic\initController.ts:43 -#: src\podkop\tabs\diagnostic\initController.ts:44 -#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:7 -msgid "unknown" -msgstr "" - -#: src\podkop\api.ts:40 -msgid "Unknown error" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:240 -#: src\podkop\tabs\dashboard\initController.ts:271 -msgid "Uplink" -msgstr "" - -#: src\validators\validateProxyUrl.ts:37 -msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" -msgstr "" - -#: src\validators\validateUrl.ts:17 -msgid "URL must use one of the following protocols:" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:28 -msgid "URLTest" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:183 -msgid "URLTest Check Interval" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:160 -msgid "URLTest Proxy Links" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:221 -msgid "URLTest Testing URL" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:197 -msgid "URLTest Tolerance" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:439 -msgid "User Domain List Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:451 -msgid "User Domains" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:477 -msgid "User Domains List" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:519 -msgid "User Subnet List Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:531 -msgid "User Subnets" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:557 -msgid "User Subnets List" -msgstr "" - -#: src\validators\validateDns.ts:14 -#: src\validators\validateDns.ts:18 -#: src\validators\validateDomain.ts:13 -#: src\validators\validateDomain.ts:30 -#: src\validators\validateHysteriaUrl.ts:120 -#: src\validators\validateIp.ts:8 -#: src\validators\validateOutboundJson.ts:7 -#: src\validators\validatePath.ts:16 -#: src\validators\validateShadowsocksUrl.ts:95 -#: src\validators\validateSocksUrl.ts:80 -#: src\validators\validateSubnet.ts:38 -#: src\validators\validateTrojanUrl.ts:59 -#: src\validators\validateUrl.ts:28 -#: src\validators\validateVlessUrl.ts:108 -msgid "Valid" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:510 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:589 -msgid "Validation errors:" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:258 -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:111 -msgid "View logs" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:31 -msgid "Visit Wiki" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:38 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:138 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:161 -msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:387 -msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:406 -msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:256 -msgid "YACD Secret Key" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:127 -msgid "You can select Output Network Interface, by default autodetect" -msgstr "" diff --git a/fe-app-podkop/src/podkop/fetchers/fetchServicesInfo.ts b/fe-app-podkop/src/podkop/fetchers/fetchServicesInfo.ts deleted file mode 100644 index 777ab94b..00000000 --- a/fe-app-podkop/src/podkop/fetchers/fetchServicesInfo.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { PodkopShellMethods } from '../methods'; -import { store } from '../services'; - -export async function fetchServicesInfo() { - const [podkop, singbox] = await Promise.all([ - PodkopShellMethods.getStatus(), - PodkopShellMethods.getSingBoxStatus(), - ]); - - if (!podkop.success || !singbox.success) { - store.set({ - servicesInfoWidget: { - loading: false, - failed: true, - data: { singbox: 0, podkop: 0 }, - }, - }); - } - - if (podkop.success && singbox.success) { - store.set({ - servicesInfoWidget: { - loading: false, - failed: false, - data: { singbox: singbox.data.running, podkop: podkop.data.enabled }, - }, - }); - } -} diff --git a/fe-app-podkop/src/podkop/methods/custom/getConfigSections.ts b/fe-app-podkop/src/podkop/methods/custom/getConfigSections.ts deleted file mode 100644 index bdb13c46..00000000 --- a/fe-app-podkop/src/podkop/methods/custom/getConfigSections.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Podkop } from '../../types'; - -export async function getConfigSections(): Promise { - return uci.load('podkop').then(() => uci.sections('podkop')); -} diff --git a/fe-app-podkop/src/podkop/methods/shell/index.ts b/fe-app-podkop/src/podkop/methods/shell/index.ts deleted file mode 100644 index 24038743..00000000 --- a/fe-app-podkop/src/podkop/methods/shell/index.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { callBaseMethod } from './callBaseMethod'; -import { ClashAPI, Podkop } from '../../types'; -import { executeShellCommand } from '../../../helpers'; - -interface SingBoxComponentActionResult { - success: boolean; - version?: string; - message?: string; -} - -export const PodkopShellMethods = { - checkDNSAvailable: async () => - callBaseMethod( - Podkop.AvailableMethods.CHECK_DNS_AVAILABLE, - ), - checkFakeIP: async () => - callBaseMethod( - Podkop.AvailableMethods.CHECK_FAKEIP, - ), - checkNftRules: async () => - callBaseMethod( - Podkop.AvailableMethods.CHECK_NFT_RULES, - ), - getStatus: async () => - callBaseMethod(Podkop.AvailableMethods.GET_STATUS), - checkSingBox: async () => - callBaseMethod( - Podkop.AvailableMethods.CHECK_SING_BOX, - ), - getSingBoxStatus: async () => - callBaseMethod( - Podkop.AvailableMethods.GET_SING_BOX_STATUS, - ), - getClashApiProxies: async () => - callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ - Podkop.AvailableClashAPIMethods.GET_PROXIES, - ]), - getClashApiProxyLatency: async (tag: string) => - callBaseMethod( - Podkop.AvailableMethods.CLASH_API, - [Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, '5000'], - ), - getClashApiGroupLatency: async (tag: string) => - callBaseMethod( - Podkop.AvailableMethods.CLASH_API, - [Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, '10000'], - ), - setClashApiGroupProxy: async (group: string, proxy: string) => - callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ - Podkop.AvailableClashAPIMethods.SET_GROUP_PROXY, - group, - proxy, - ]), - restart: async () => - callBaseMethod( - Podkop.AvailableMethods.RESTART, - [], - '/etc/init.d/podkop', - ), - start: async () => - callBaseMethod( - Podkop.AvailableMethods.START, - [], - '/etc/init.d/podkop', - ), - stop: async () => - callBaseMethod( - Podkop.AvailableMethods.STOP, - [], - '/etc/init.d/podkop', - ), - enable: async () => - callBaseMethod( - Podkop.AvailableMethods.ENABLE, - [], - '/etc/init.d/podkop', - ), - disable: async () => - callBaseMethod( - Podkop.AvailableMethods.DISABLE, - [], - '/etc/init.d/podkop', - ), - globalCheck: async () => - callBaseMethod(Podkop.AvailableMethods.GLOBAL_CHECK), - showSingBoxConfig: async () => - callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG), - checkLogs: async () => - callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS), - getSystemInfo: async () => - callBaseMethod( - Podkop.AvailableMethods.GET_SYSTEM_INFO, - ), - subscriptionUpdate: async () => - callBaseMethod(Podkop.AvailableMethods.SUBSCRIPTION_UPDATE), - singBoxComponentAction: async ( - action: 'install_extended' | 'install_stable' | 'check_update', - ): Promise => { - const response = await executeShellCommand({ - command: '/usr/bin/podkop', - args: ['component_action', 'sing_box', action], - timeout: 600000, - }); - - if (response.stdout) { - try { - const parsed = JSON.parse( - response.stdout, - ) as SingBoxComponentActionResult; - - return { - success: Boolean(parsed.success), - version: parsed.version, - message: parsed.message, - }; - } catch (_e) { - return { - success: false, - message: response.stdout, - }; - } - } - - return { - success: false, - message: response.stderr || '', - }; - }, -}; diff --git a/install.sh b/install.sh index 6fde5176..0580f410 100755 --- a/install.sh +++ b/install.sh @@ -2,7 +2,7 @@ # shellcheck shell=dash REPO="https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest" -DOWNLOAD_DIR="/tmp/podkop" +DOWNLOAD_DIR="/tmp/netshift" COUNT=3 # Cached flag to switch between ipk or apk package managers @@ -63,9 +63,9 @@ pkg_install() { update_config() { printf "\033[48;5;196m\033[1m╔══════════════════════════════════════════════════════════════════════╗\033[0m\n" - printf "\033[48;5;196m\033[1m║ ! Обнаружена старая версия podkop. ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Если продолжите обновление, вам потребуется настроить Podkop заново. ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Старая конфигурация будет сохранена в /etc/config/podkop-070 ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ ! Обнаружена старая версия NetShift. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Если продолжите обновление, вам потребуется настроить NetShift заново.║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Старая конфигурация будет сохранена в /etc/config/netshift-070 ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -73,9 +73,9 @@ update_config() { echo "" printf "\033[48;5;196m\033[1m╔══════════════════════════════════════════════════════════════════════╗\033[0m\n" - printf "\033[48;5;196m\033[1m║ ! Detected old podkop version. ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ If you continue the update, you will need to RECONFIGURE podkop. ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Your old configuration will be saved to /etc/config/podkop-070 ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ ! Detected old NetShift version. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ If you continue the update, you will need to RECONFIGURE NetShift. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Your old configuration will be saved to /etc/config/netshift-070 ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -87,9 +87,9 @@ update_config() { case $CONFIG_UPDATE in yes|y|Y) - mv /etc/config/podkop /etc/config/podkop-070 - wget -O /etc/config/podkop https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/podkop/files/etc/config/podkop - msg "Podkop config has been reset to default. Your old config saved in /etc/config/podkop-070" + mv /etc/config/netshift /etc/config/netshift-070 + wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/netshift/files/etc/config/netshift + msg "NetShift config has been reset to default. Your old config saved in /etc/config/netshift-070" break ;; *) @@ -100,6 +100,138 @@ update_config() { done } +# Detect whether an OLD podkop install is present on this router. +# Returns 0 (true) if any podkop artifact is found. +podkop_is_installed() { + if [ -f "/etc/config/podkop" ]; then + return 0 + fi + if command -v podkop >/dev/null 2>&1; then + return 0 + fi + if [ -x "/etc/init.d/podkop" ] || [ -f "/etc/init.d/podkop" ]; then + return 0 + fi + return 1 +} + +# Migrate an existing podkop (< 0.8.0) install to NetShift. +# podkop never reached 0.8.0, so any old podkop install triggers this. +# Every step is guarded by existence checks, POSIX, and idempotent so that +# re-running install.sh is safe. +migrate_from_podkop() { + local old_version + old_version=$(/usr/bin/podkop show_version 2>/dev/null) + + # 1. Bilingual banner (RU first, EN second) + confirmation prompt. + printf "\033[48;5;196m\033[1m╔══════════════════════════════════════════════════════════════════════╗\033[0m\n" + printf "\033[48;5;196m\033[1m║ ! Обнаружена установка podkop. Она будет перенесена в NetShift. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Ваша конфигурация будет перенесена автоматически. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Старая конфигурация сохранится в /etc/config/podkop.bak.pre-netshift║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Старый пакет podkop будет удалён, NetShift будет установлен. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n" + printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" + + echo "" + + printf "\033[48;5;196m\033[1m╔══════════════════════════════════════════════════════════════════════╗\033[0m\n" + printf "\033[48;5;196m\033[1m║ ! Detected a podkop install. It will be migrated to NetShift. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Your configuration will be carried over automatically. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Old config will be backed up to /etc/config/podkop.bak.pre-netshift ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ The old podkop package will be removed, NetShift installed. ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n" + printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" + + if [ -n "$old_version" ]; then + msg "Detected podkop version: $old_version" + fi + + msg "Continue migration to NetShift? (yes/no)" + + read -r -p '' MIGRATE_CONFIRM + case $MIGRATE_CONFIRM in + yes|y|Y) + ;; + *) + msg "Exit" + exit 1 + ;; + esac + + # 2. Stop the old service if running. The old 'stop' restores dnsmasq + # (podkop_server/noresolv/cachesize keys in /etc/config/dhcp), removes + # the old nft table PodkopTable and the '105 podkop' rt_tables line. + # Must run BEFORE removing the package for a clean teardown. + if [ -x "/etc/init.d/podkop" ]; then + msg "Stopping old podkop service..." + /etc/init.d/podkop stop 2>/dev/null || true + fi + + # 3. Disable old rc.d autostart (best-effort). + if [ -x "/etc/init.d/podkop" ]; then + msg "Disabling old podkop autostart..." + /etc/init.d/podkop disable 2>/dev/null || true + fi + + # 4. Migrate config (copy, not move — keep a backup). Schema is compatible. + if [ -f "/etc/config/podkop" ]; then + if [ ! -f "/etc/config/netshift" ]; then + msg "Migrating config /etc/config/podkop -> /etc/config/netshift..." + cp /etc/config/podkop /etc/config/netshift 2>/dev/null || true + else + msg "/etc/config/netshift already exists, keeping it." + fi + if [ ! -f "/etc/config/podkop.bak.pre-netshift" ]; then + cp /etc/config/podkop /etc/config/podkop.bak.pre-netshift 2>/dev/null || true + fi + fi + + # 5. Migrate state dir (preserves subscription cache). Best-effort. + if [ -d "/etc/podkop" ] && [ ! -d "/etc/netshift" ]; then + msg "Migrating state dir /etc/podkop -> /etc/netshift..." + cp -r /etc/podkop /etc/netshift 2>/dev/null || true + fi + + # 6. Clean leftover OLD persistent system state that opkg/apk remove won't. + # rt_tables: remove old '105 podkop' line (NetShift adds '105 netshift' + # itself on start). + if [ -f "/etc/iproute2/rt_tables" ] && grep -q "105 podkop" /etc/iproute2/rt_tables 2>/dev/null; then + msg "Removing old '105 podkop' rt_tables entry..." + sed -i "/105 podkop/d" /etc/iproute2/rt_tables 2>/dev/null || true + fi + # Old cron lines: strip entries that call the old binary (NetShift re-adds + # its own on start). + if crontab -l >/dev/null 2>&1; then + if crontab -l 2>/dev/null | grep -q "/usr/bin/podkop"; then + msg "Removing old podkop cron entries..." + crontab -l 2>/dev/null | grep -v "/usr/bin/podkop" | crontab - 2>/dev/null || true + fi + fi + # NOTE: nft table PodkopTable + dnsmasq keys are cleaned by the + # '/etc/init.d/podkop stop' above; we do NOT hand-edit /etc/config/dhcp. + + # 7. Remove OLD packages (after config/state migrated). + # Order: i18n, then luci-app, then backend. + if pkg_is_installed luci-i18n-podkop; then + msg "Removing old luci-i18n-podkop* packages..." + pkg_remove luci-i18n-podkop* + fi + if pkg_is_installed luci-app-podkop; then + msg "Removing old luci-app-podkop package..." + pkg_remove luci-app-podkop + fi + if pkg_is_installed "^podkop" || command -v podkop >/dev/null 2>&1; then + msg "Removing old podkop package..." + pkg_remove podkop + fi + + # 8. Done. + msg "Migration complete. NetShift will now be installed." + msg "Your old config is preserved at /etc/config/podkop.bak.pre-netshift" +} + main() { check_system sing_box @@ -108,10 +240,10 @@ main() { pkg_list_update || { echo "Packages list update failed"; exit 1; } - if [ -f "/etc/init.d/podkop" ]; then - msg "Podkop is already installed. Upgrading..." + if [ -f "/etc/init.d/netshift" ]; then + msg "NetShift is already installed. Upgrading..." else - msg "Installing podkop..." + msg "Installing NetShift..." fi if command -v curl >/dev/null 2>&1; then @@ -154,12 +286,12 @@ main() { done # Check if any files were downloaded - if ! ls "$DOWNLOAD_DIR"/*podkop* >/dev/null 2>&1; then + if ! ls "$DOWNLOAD_DIR"/*netshift* >/dev/null 2>&1; then msg "No packages were downloaded successfully" exit 1 fi - for pkg in podkop luci-app-podkop; do + for pkg in netshift luci-app-netshift; do file="" for f in "$DOWNLOAD_DIR"/"$pkg"*; do if [ -f "$f" ]; then @@ -175,16 +307,16 @@ main() { done ru="" - for f in "$DOWNLOAD_DIR"/luci-i18n-podkop-ru*; do + for f in "$DOWNLOAD_DIR"/luci-i18n-netshift-ru*; do if [ -f "$f" ]; then ru=$(basename "$f") break fi done if [ -n "$ru" ]; then - if pkg_is_installed luci-i18n-podkop-ru; then + if pkg_is_installed luci-i18n-netshift-ru; then msg "Upgrading Russian translation..." - pkg_remove luci-i18n-podkop* + pkg_remove luci-i18n-netshift* pkg_install "$DOWNLOAD_DIR/$ru" else msg "Русский язык интерфейса ставим? y/n (Install the Russian interface language?)" @@ -192,7 +324,7 @@ main() { read -r -p '' RUS case $RUS in y) - pkg_remove luci-i18n-podkop* + pkg_remove luci-i18n-netshift* pkg_install "$DOWNLOAD_DIR/$ru" break ;; @@ -207,7 +339,7 @@ main() { fi fi - find "$DOWNLOAD_DIR" -type f -name '*podkop*' -exec rm {} \; + find "$DOWNLOAD_DIR" -type f -name '*netshift*' -exec rm {} \; } check_system() { @@ -218,8 +350,8 @@ check_system() { # Check OpenWrt version openwrt_version=$(cat /etc/openwrt_release | grep DISTRIB_RELEASE | cut -d"'" -f2 | cut -d'.' -f1) if [ "$openwrt_version" = "23" ]; then - msg "OpenWrt 23.05 не поддерживается начиная с podkop 0.5.0" - msg "Для OpenWrt 23.05 используйте podkop версии 0.4.11 или устанавливайте зависимости и podkop вручную" + msg "OpenWrt 23.05 не поддерживается начиная с NetShift 0.8.0" + msg "Для OpenWrt 23.05 устанавливайте зависимости и NetShift вручную" msg "Подробности: https://podkop.net/docs/install/#%d1%83%d1%81%d1%82%d0%b0%d0%bd%d0%be%d0%b2%d0%ba%d0%b0-%d0%bd%d0%b0-2305" exit 1 fi @@ -240,10 +372,17 @@ check_system() { exit 1 fi - # Check version - if command -v podkop > /dev/null 2>&1; then + # Old podkop install detected -> migrate to NetShift before installing the + # new packages. podkop never reached 0.8.0, so ANY old podkop triggers this. + if podkop_is_installed; then + migrate_from_podkop + return + fi + + # Otherwise check existing NetShift version (just upgrading NetShift). + if command -v netshift > /dev/null 2>&1; then local version - version=$(/usr/bin/podkop show_version 2> /dev/null) + version=$(/usr/bin/netshift show_version 2> /dev/null) if [ -n "$version" ]; then version=$(echo "$version" | sed 's/^v//') local major @@ -253,18 +392,17 @@ check_system() { minor=$(echo "$version" | cut -d. -f2) patch=$(echo "$version" | cut -d. -f3) - # Compare version: must be >= 0.7.0 + # Compare version: must be >= 0.8.0 if [ "$major" -gt 0 ] || - [ "$major" -eq 0 ] && [ "$minor" -gt 7 ] || - [ "$major" -eq 0 ] && [ "$minor" -eq 7 ] && [ "$patch" -ge 0 ]; then - msg "Podkop version >= 0.7.0" - break + { [ "$major" -eq 0 ] && [ "$minor" -gt 8 ]; } || + { [ "$major" -eq 0 ] && [ "$minor" -eq 8 ] && [ "$patch" -ge 0 ]; }; then + msg "NetShift version >= 0.8.0" else - msg "Podkop version < 0.7.0" + msg "NetShift version < 0.8.0" update_config fi else - msg "Unknown podkop version" + msg "Unknown NetShift version" update_config fi fi @@ -302,7 +440,7 @@ sing_box() { if [ "$(printf '%s\n%s\n' "$sing_box_version" "$required_version" | sort -V | head -n 1)" != "$required_version" ]; then msg "sing-box version $sing_box_version is older than the required version $required_version." msg "Removing old version..." - service podkop stop + service netshift stop 2>/dev/null || service podkop stop 2>/dev/null || true pkg_remove sing-box fi } diff --git a/luci-app-podkop/Makefile b/luci-app-netshift/Makefile similarity index 69% rename from luci-app-podkop/Makefile rename to luci-app-netshift/Makefile index f6ae3a0d..712405e4 100644 --- a/luci-app-podkop/Makefile +++ b/luci-app-netshift/Makefile @@ -1,13 +1,13 @@ include $(TOPDIR)/rules.mk -PKG_NAME:=luci-app-podkop +PKG_NAME:=luci-app-netshift -PKG_VERSION := $(if $(PODKOP_VERSION),$(PODKOP_VERSION),0.$(shell date +%d%m%Y)) +PKG_VERSION := $(if $(NETSHIFT_VERSION),$(NETSHIFT_VERSION),0.$(shell date +%d%m%Y)) PKG_RELEASE:=1 -LUCI_TITLE:=LuCI podkop app -LUCI_DEPENDS:=+luci-base +podkop +LUCI_TITLE:=LuCI NetShift app +LUCI_DEPENDS:=+luci-base +netshift LUCI_PKGARCH:=all LUCI_LANG.ru:=Русский (Russian) LUCI_LANG.en:=English @@ -24,7 +24,7 @@ define Package/$(PKG_NAME)/install $(CP) $(PKG_BUILD_DIR)/htdocs/* $(1)$(HTDOCS)/ $(INSTALL_DIR) $(1)/ $(CP) $(PKG_BUILD_DIR)/root/* $(1)/ - sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' $(1)$(HTDOCS)/luci-static/resources/view/podkop/main.js || true + sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' $(1)$(HTDOCS)/luci-static/resources/view/netshift/main.js || true endef $(eval $(call BuildPackage,$(PKG_NAME))) \ No newline at end of file diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboard.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/dashboard.js similarity index 91% rename from luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboard.js rename to luci-app-netshift/htdocs/luci-static/resources/view/netshift/dashboard.js index 6fd97cfe..990d4fac 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/dashboard.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/dashboard.js @@ -4,7 +4,7 @@ "require ui"; "require uci"; "require fs"; -"require view.podkop.main as main"; +"require view.netshift.main as main"; function createDashboardContent(section) { const o = section.option(form.DummyValue, "_mount_node"); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnostic.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/diagnostic.js similarity index 91% rename from luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnostic.js rename to luci-app-netshift/htdocs/luci-static/resources/view/netshift/diagnostic.js index e6ac146d..02304a4b 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnostic.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/diagnostic.js @@ -4,7 +4,7 @@ "require ui"; "require uci"; "require fs"; -"require view.podkop.main as main"; +"require view.netshift.main as main"; function createDiagnosticContent(section) { const o = section.option(form.DummyValue, "_mount_node"); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js similarity index 93% rename from luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js rename to luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 11453b9d..7f9abdb0 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -570,13 +570,13 @@ function parseValueList(value) { return value.split(/\n/).map((line) => line.split("//")[0]).join(" ").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean); } -// src/podkop/methods/custom/getConfigSections.ts +// src/netshift/methods/custom/getConfigSections.ts async function getConfigSections() { - return uci.load("podkop").then(() => uci.sections("podkop")); + return uci.load("netshift").then(() => uci.sections("netshift")); } -// src/podkop/methods/shell/callBaseMethod.ts -async function callBaseMethod(method, args = [], command = "/usr/bin/podkop") { +// src/netshift/methods/shell/callBaseMethod.ts +async function callBaseMethod(method, args = [], command = "/usr/bin/netshift") { const response = await executeShellCommand({ command, args: [method, ...args], @@ -601,9 +601,9 @@ async function callBaseMethod(method, args = [], command = "/usr/bin/podkop") { }; } -// src/podkop/types.ts -var Podkop; -((Podkop2) => { +// src/netshift/types.ts +var NetShift; +((NetShift2) => { let AvailableMethods; ((AvailableMethods2) => { AvailableMethods2["CHECK_DNS_AVAILABLE"] = "check_dns_available"; @@ -623,85 +623,85 @@ var Podkop; AvailableMethods2["CHECK_LOGS"] = "check_logs"; AvailableMethods2["GET_SYSTEM_INFO"] = "get_system_info"; AvailableMethods2["SUBSCRIPTION_UPDATE"] = "subscription_update"; - })(AvailableMethods = Podkop2.AvailableMethods || (Podkop2.AvailableMethods = {})); + })(AvailableMethods = NetShift2.AvailableMethods || (NetShift2.AvailableMethods = {})); let AvailableClashAPIMethods; ((AvailableClashAPIMethods2) => { AvailableClashAPIMethods2["GET_PROXIES"] = "get_proxies"; AvailableClashAPIMethods2["GET_PROXY_LATENCY"] = "get_proxy_latency"; AvailableClashAPIMethods2["GET_GROUP_LATENCY"] = "get_group_latency"; AvailableClashAPIMethods2["SET_GROUP_PROXY"] = "set_group_proxy"; - })(AvailableClashAPIMethods = Podkop2.AvailableClashAPIMethods || (Podkop2.AvailableClashAPIMethods = {})); -})(Podkop || (Podkop = {})); + })(AvailableClashAPIMethods = NetShift2.AvailableClashAPIMethods || (NetShift2.AvailableClashAPIMethods = {})); +})(NetShift || (NetShift = {})); -// src/podkop/methods/shell/index.ts -var PodkopShellMethods = { +// src/netshift/methods/shell/index.ts +var NetShiftShellMethods = { checkDNSAvailable: async () => callBaseMethod( - Podkop.AvailableMethods.CHECK_DNS_AVAILABLE + NetShift.AvailableMethods.CHECK_DNS_AVAILABLE ), checkFakeIP: async () => callBaseMethod( - Podkop.AvailableMethods.CHECK_FAKEIP + NetShift.AvailableMethods.CHECK_FAKEIP ), checkNftRules: async () => callBaseMethod( - Podkop.AvailableMethods.CHECK_NFT_RULES + NetShift.AvailableMethods.CHECK_NFT_RULES ), - getStatus: async () => callBaseMethod(Podkop.AvailableMethods.GET_STATUS), + getStatus: async () => callBaseMethod(NetShift.AvailableMethods.GET_STATUS), checkSingBox: async () => callBaseMethod( - Podkop.AvailableMethods.CHECK_SING_BOX + NetShift.AvailableMethods.CHECK_SING_BOX ), getSingBoxStatus: async () => callBaseMethod( - Podkop.AvailableMethods.GET_SING_BOX_STATUS + NetShift.AvailableMethods.GET_SING_BOX_STATUS ), - getClashApiProxies: async () => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ - Podkop.AvailableClashAPIMethods.GET_PROXIES + getClashApiProxies: async () => callBaseMethod(NetShift.AvailableMethods.CLASH_API, [ + NetShift.AvailableClashAPIMethods.GET_PROXIES ]), getClashApiProxyLatency: async (tag) => callBaseMethod( - Podkop.AvailableMethods.CLASH_API, - [Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, "5000"] + NetShift.AvailableMethods.CLASH_API, + [NetShift.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, "5000"] ), getClashApiGroupLatency: async (tag) => callBaseMethod( - Podkop.AvailableMethods.CLASH_API, - [Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, "10000"] + NetShift.AvailableMethods.CLASH_API, + [NetShift.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, "10000"] ), - setClashApiGroupProxy: async (group, proxy) => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [ - Podkop.AvailableClashAPIMethods.SET_GROUP_PROXY, + setClashApiGroupProxy: async (group, proxy) => callBaseMethod(NetShift.AvailableMethods.CLASH_API, [ + NetShift.AvailableClashAPIMethods.SET_GROUP_PROXY, group, proxy ]), restart: async () => callBaseMethod( - Podkop.AvailableMethods.RESTART, + NetShift.AvailableMethods.RESTART, [], - "/etc/init.d/podkop" + "/etc/init.d/netshift" ), start: async () => callBaseMethod( - Podkop.AvailableMethods.START, + NetShift.AvailableMethods.START, [], - "/etc/init.d/podkop" + "/etc/init.d/netshift" ), stop: async () => callBaseMethod( - Podkop.AvailableMethods.STOP, + NetShift.AvailableMethods.STOP, [], - "/etc/init.d/podkop" + "/etc/init.d/netshift" ), enable: async () => callBaseMethod( - Podkop.AvailableMethods.ENABLE, + NetShift.AvailableMethods.ENABLE, [], - "/etc/init.d/podkop" + "/etc/init.d/netshift" ), disable: async () => callBaseMethod( - Podkop.AvailableMethods.DISABLE, + NetShift.AvailableMethods.DISABLE, [], - "/etc/init.d/podkop" + "/etc/init.d/netshift" ), - globalCheck: async () => callBaseMethod(Podkop.AvailableMethods.GLOBAL_CHECK), - showSingBoxConfig: async () => callBaseMethod(Podkop.AvailableMethods.SHOW_SING_BOX_CONFIG), - checkLogs: async () => callBaseMethod(Podkop.AvailableMethods.CHECK_LOGS), + globalCheck: async () => callBaseMethod(NetShift.AvailableMethods.GLOBAL_CHECK), + showSingBoxConfig: async () => callBaseMethod(NetShift.AvailableMethods.SHOW_SING_BOX_CONFIG), + checkLogs: async () => callBaseMethod(NetShift.AvailableMethods.CHECK_LOGS), getSystemInfo: async () => callBaseMethod( - Podkop.AvailableMethods.GET_SYSTEM_INFO + NetShift.AvailableMethods.GET_SYSTEM_INFO ), - subscriptionUpdate: async () => callBaseMethod(Podkop.AvailableMethods.SUBSCRIPTION_UPDATE), + subscriptionUpdate: async () => callBaseMethod(NetShift.AvailableMethods.SUBSCRIPTION_UPDATE), singBoxComponentAction: async (action) => { const response = await executeShellCommand({ - command: "/usr/bin/podkop", + command: "/usr/bin/netshift", args: ["component_action", "sing_box", action], timeout: 6e5 }); @@ -729,10 +729,10 @@ var PodkopShellMethods = { } }; -// src/podkop/methods/custom/getDashboardSections.ts +// src/netshift/methods/custom/getDashboardSections.ts async function getDashboardSections() { const configSections = await getConfigSections(); - const clashProxies = await PodkopShellMethods.getClashApiProxies(); + const clashProxies = await NetShiftShellMethods.getClashApiProxies(); if (!clashProxies.success) { return { success: false, @@ -943,15 +943,15 @@ async function getDashboardSections() { }; } -// src/podkop/methods/custom/getClashApiSecret.ts +// src/netshift/methods/custom/getClashApiSecret.ts async function getClashApiSecret() { const sections = await getConfigSections(); const settings = sections.find((section) => section[".type"] === "settings"); return settings?.yacd_secret_key || ""; } -// src/podkop/methods/custom/index.ts -var CustomPodkopMethods = { +// src/netshift/methods/custom/index.ts +var CustomNetShiftMethods = { getConfigSections, getDashboardSections, getClashApiSecret @@ -963,7 +963,7 @@ var STATUS_COLORS = { ERROR: "#f44336", WARNING: "#ff9800" }; -var PODKOP_LUCI_APP_VERSION = "__COMPILED_VERSION_VARIABLE__"; +var NETSHIFT_LUCI_APP_VERSION = "__COMPILED_VERSION_VARIABLE__"; var FAKEIP_CHECK_DOMAIN = "fakeip.podkop.fyi"; var IP_CHECK_DOMAIN = "ip.podkop.fyi"; var REGIONAL_OPTIONS = [ @@ -1078,7 +1078,7 @@ var COMMAND_SCHEDULING = { // Lowest priority }; -// src/podkop/api.ts +// src/netshift/api.ts async function createBaseApiRequest(fetchFn, options) { const wrappedFn = () => options?.timeoutMs && options?.operationName ? withTimeout( fetchFn(), @@ -1107,7 +1107,7 @@ async function createBaseApiRequest(fetchFn, options) { } } -// src/podkop/methods/fakeip/getFakeIpCheck.ts +// src/netshift/methods/fakeip/getFakeIpCheck.ts async function getFakeIpCheck() { return createBaseApiRequest( () => fetch(`https://${FAKEIP_CHECK_DOMAIN}/check`, { @@ -1121,7 +1121,7 @@ async function getFakeIpCheck() { ); } -// src/podkop/methods/fakeip/getIpCheck.ts +// src/netshift/methods/fakeip/getIpCheck.ts async function getIpCheck() { return createBaseApiRequest( () => fetch(`https://${IP_CHECK_DOMAIN}/check`, { @@ -1135,13 +1135,13 @@ async function getIpCheck() { ); } -// src/podkop/methods/fakeip/index.ts +// src/netshift/methods/fakeip/index.ts var RemoteFakeIPMethods = { getFakeIpCheck, getIpCheck }; -// src/podkop/services/tab.service.ts +// src/netshift/services/tab.service.ts var TabService = class _TabService { constructor() { this.observer = null; @@ -1208,12 +1208,12 @@ var TabService = class _TabService { }; var TabServiceInstance = TabService.getInstance(); -// src/podkop/tabs/diagnostic/helpers/getCheckTitle.ts +// src/netshift/tabs/diagnostic/helpers/getCheckTitle.ts function getCheckTitle(name) { return `${name} ${_("checks")}`; } -// src/podkop/tabs/diagnostic/checks/contstants.ts +// src/netshift/tabs/diagnostic/checks/contstants.ts var DIAGNOSTICS_CHECKS_MAP = { ["DNS" /* DNS */]: { order: 1, @@ -1242,12 +1242,12 @@ var DIAGNOSTICS_CHECKS_MAP = { } }; -// src/podkop/tabs/diagnostic/diagnostic.store.ts +// src/netshift/tabs/diagnostic/diagnostic.store.ts var initialDiagnosticStore = { diagnosticsSystemInfo: { loading: true, - podkop_version: "loading", - podkop_latest_version: "loading", + netshift_version: "loading", + netshift_latest_version: "loading", luci_app_version: "loading", sing_box_version: "loading", openwrt_version: "loading", @@ -1372,7 +1372,7 @@ var loadingDiagnosticsChecksStore = { ] }; -// src/podkop/services/store.service.ts +// src/netshift/services/store.service.ts function jsonStableStringify(obj) { return JSON.stringify(obj, (_2, value) => { if (value && typeof value === "object" && !Array.isArray(value)) { @@ -1485,7 +1485,7 @@ var initialStore = { servicesInfoWidget: { loading: true, failed: false, - data: { singbox: 0, podkop: 0 } + data: { singbox: 0, netshift: 0 } }, sectionsWidget: { loading: true, @@ -1510,7 +1510,7 @@ function downloadAsTxt(text, filename) { URL.revokeObjectURL(link.href); } -// src/podkop/services/logger.service.ts +// src/netshift/services/logger.service.ts var Logger = class { constructor() { this.logs = []; @@ -1565,8 +1565,8 @@ var Logger = class { }; var logger = new Logger(); -// src/podkop/services/podkopLogWatcher.service.ts -var PodkopLogWatcher = class _PodkopLogWatcher { +// src/netshift/services/netshiftLogWatcher.service.ts +var NetShiftLogWatcher = class _NetShiftLogWatcher { constructor() { this.intervalMs = 5e3; this.lastLines = /* @__PURE__ */ new Set(); @@ -1580,27 +1580,27 @@ var PodkopLogWatcher = class _PodkopLogWatcher { } } static getInstance() { - if (!_PodkopLogWatcher.instance) { - _PodkopLogWatcher.instance = new _PodkopLogWatcher(); + if (!_NetShiftLogWatcher.instance) { + _NetShiftLogWatcher.instance = new _NetShiftLogWatcher(); } - return _PodkopLogWatcher.instance; + return _NetShiftLogWatcher.instance; } init(fetcher, options) { this.fetcher = fetcher; this.onNewLog = options?.onNewLog; this.intervalMs = options?.intervalMs ?? 5e3; logger.info( - "[PodkopLogWatcher]", + "[NetShiftLogWatcher]", `initialized (interval: ${this.intervalMs}ms)` ); } async checkOnce() { if (!this.fetcher) { - logger.warn("[PodkopLogWatcher]", "fetcher not found"); + logger.warn("[NetShiftLogWatcher]", "fetcher not found"); return; } if (this.paused) { - logger.debug("[PodkopLogWatcher]", "skipped check \u2014 tab not visible"); + logger.debug("[NetShiftLogWatcher]", "skipped check \u2014 tab not visible"); return; } try { @@ -1617,19 +1617,19 @@ var PodkopLogWatcher = class _PodkopLogWatcher { this.lastLines = new Set(arr.slice(-500)); } } catch (err) { - logger.error("[PodkopLogWatcher]", "failed to read logs:", err); + logger.error("[NetShiftLogWatcher]", "failed to read logs:", err); } } start() { if (this.running) return; if (!this.fetcher) { - logger.warn("[PodkopLogWatcher]", "attempted to start without fetcher"); + logger.warn("[NetShiftLogWatcher]", "attempted to start without fetcher"); return; } this.running = true; this.timer = setInterval(() => this.checkOnce(), this.intervalMs); logger.info( - "[PodkopLogWatcher]", + "[NetShiftLogWatcher]", `started (interval: ${this.intervalMs}ms)` ); } @@ -1637,26 +1637,26 @@ var PodkopLogWatcher = class _PodkopLogWatcher { if (!this.running) return; this.running = false; if (this.timer) clearInterval(this.timer); - logger.info("[PodkopLogWatcher]", "stopped"); + logger.info("[NetShiftLogWatcher]", "stopped"); } pause() { if (!this.running || this.paused) return; this.paused = true; - logger.info("[PodkopLogWatcher]", "paused (tab not visible)"); + logger.info("[NetShiftLogWatcher]", "paused (tab not visible)"); } resume() { if (!this.running || !this.paused) return; this.paused = false; - logger.info("[PodkopLogWatcher]", "resumed (tab active)"); + logger.info("[NetShiftLogWatcher]", "resumed (tab active)"); this.checkOnce(); } reset() { this.lastLines.clear(); - logger.info("[PodkopLogWatcher]", "log history reset"); + logger.info("[NetShiftLogWatcher]", "log history reset"); } }; -// src/podkop/services/core.service.ts +// src/netshift/services/core.service.ts function coreService() { TabServiceInstance.onChange((activeId, tabs) => { logger.info("[TAB]", activeId); @@ -1667,10 +1667,10 @@ function coreService() { } }); }); - const watcher = PodkopLogWatcher.getInstance(); + const watcher = NetShiftLogWatcher.getInstance(); watcher.init( async () => { - const logs = await PodkopShellMethods.checkLogs(); + const logs = await NetShiftShellMethods.checkLogs(); if (logs.success) { return logs.data; } @@ -1680,7 +1680,7 @@ function coreService() { intervalMs: 3e3, onNewLog: (line) => { if (line.toLowerCase().includes("[error]") || line.toLowerCase().includes("[fatal]")) { - ui.addNotification("Podkop Error", E("div", {}, line), "error"); + ui.addNotification("NetShift Error", E("div", {}, line), "error"); } } } @@ -1688,7 +1688,7 @@ function coreService() { watcher.start(); } -// src/podkop/services/socket.service.ts +// src/netshift/services/socket.service.ts var SocketManager = class _SocketManager { constructor() { this.sockets = /* @__PURE__ */ new Map(); @@ -1827,7 +1827,7 @@ var SocketManager = class _SocketManager { }; var socket = SocketManager.getInstance(); -// src/podkop/tabs/dashboard/partials/renderSections.ts +// src/netshift/tabs/dashboard/partials/renderSections.ts function renderFailedState() { return E( "div", @@ -1931,7 +1931,7 @@ function renderSections(props) { return renderDefaultState(props); } -// src/podkop/tabs/dashboard/partials/renderWidget.ts +// src/netshift/tabs/dashboard/partials/renderWidget.ts function renderFailedState2() { return E( "div", @@ -1993,7 +1993,7 @@ function renderWidget(props) { return renderDefaultState2(props); } -// src/podkop/tabs/dashboard/render.ts +// src/netshift/tabs/dashboard/render.ts function render() { return E( "div", @@ -2061,33 +2061,36 @@ function prettyBytes(n) { return n + " " + unit; } -// src/podkop/fetchers/fetchServicesInfo.ts +// src/netshift/fetchers/fetchServicesInfo.ts async function fetchServicesInfo() { - const [podkop, singbox] = await Promise.all([ - PodkopShellMethods.getStatus(), - PodkopShellMethods.getSingBoxStatus() + const [netshift, singbox] = await Promise.all([ + NetShiftShellMethods.getStatus(), + NetShiftShellMethods.getSingBoxStatus() ]); - if (!podkop.success || !singbox.success) { + if (!netshift.success || !singbox.success) { store.set({ servicesInfoWidget: { loading: false, failed: true, - data: { singbox: 0, podkop: 0 } + data: { singbox: 0, netshift: 0 } } }); } - if (podkop.success && singbox.success) { + if (netshift.success && singbox.success) { store.set({ servicesInfoWidget: { loading: false, failed: false, - data: { singbox: singbox.data.running, podkop: podkop.data.enabled } + data: { + singbox: singbox.data.running, + netshift: netshift.data.enabled + } } }); } } -// src/podkop/tabs/dashboard/initController.ts +// src/netshift/tabs/dashboard/initController.ts async function fetchDashboardSections() { const prev = store.get().sectionsWidget; store.set({ @@ -2096,7 +2099,7 @@ async function fetchDashboardSections() { failed: false } }); - const { data, success } = await CustomPodkopMethods.getDashboardSections(); + const { data, success } = await CustomNetShiftMethods.getDashboardSections(); if (!success) { logger.error("[DASHBOARD]", "fetchDashboardSections: failed to fetch"); } @@ -2186,7 +2189,7 @@ async function connectToClashSockets() { ); } async function handleChooseOutbound(selector, tag) { - await PodkopShellMethods.setClashApiGroupProxy(selector, tag); + await NetShiftShellMethods.setClashApiGroupProxy(selector, tag); await fetchDashboardSections(); } async function handleTestGroupLatency(tag) { @@ -2196,7 +2199,7 @@ async function handleTestGroupLatency(tag) { latencyFetching: true } }); - await PodkopShellMethods.getClashApiGroupLatency(tag); + await NetShiftShellMethods.getClashApiGroupLatency(tag); await fetchDashboardSections(); store.set({ sectionsWidget: { @@ -2212,7 +2215,7 @@ async function handleTestProxyLatency(tag) { latencyFetching: true } }); - await PodkopShellMethods.getClashApiProxyLatency(tag); + await NetShiftShellMethods.getClashApiProxyLatency(tag); await fetchDashboardSections(); store.set({ sectionsWidget: { @@ -2369,10 +2372,10 @@ async function renderServicesInfoWidget() { title: _("Services info"), items: [ { - key: _("Podkop"), - value: servicesInfoWidget.data.podkop ? _("\u2714 Enabled") : _("\u2718 Disabled"), + key: _("NetShift"), + value: servicesInfoWidget.data.netshift ? _("\u2714 Enabled") : _("\u2718 Disabled"), attributes: { - class: servicesInfoWidget.data.podkop ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" + class: servicesInfoWidget.data.netshift ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" } }, { @@ -2457,13 +2460,13 @@ async function initController() { }); } -// src/podkop/tabs/dashboard/styles.ts +// src/netshift/tabs/dashboard/styles.ts var styles = ` -#cbi-podkop-dashboard-_mount_node > div { +#cbi-netshift-dashboard-_mount_node > div { width: 100%; } -#cbi-podkop-dashboard > h3 { +#cbi-netshift-dashboard > h3 { display: none; } @@ -2578,14 +2581,14 @@ var styles = ` `; -// src/podkop/tabs/dashboard/index.ts +// src/netshift/tabs/dashboard/index.ts var DashboardTab = { render, initController, styles }; -// src/podkop/tabs/diagnostic/renderDiagnostic.ts +// src/netshift/tabs/diagnostic/renderDiagnostic.ts function render2() { return E("div", { id: "diagnostic-status", class: "pdk_diagnostic-page" }, [ E("div", { class: "pdk_diagnostic-page__left-bar" }, [ @@ -2603,7 +2606,7 @@ function render2() { ]); } -// src/podkop/tabs/diagnostic/checks/updateCheckStore.ts +// src/netshift/tabs/diagnostic/checks/updateCheckStore.ts function updateCheckStore(check, minified) { const diagnosticsChecks = store.get().diagnosticsChecks; const other = diagnosticsChecks.filter((item) => item.code !== check.code); @@ -2617,7 +2620,7 @@ function updateCheckStore(check, minified) { }); } -// src/podkop/tabs/diagnostic/helpers/getMeta.ts +// src/netshift/tabs/diagnostic/helpers/getMeta.ts function getMeta({ allGood, atLeastOneGood }) { if (allGood) { return { @@ -2637,7 +2640,7 @@ function getMeta({ allGood, atLeastOneGood }) { }; } -// src/podkop/tabs/diagnostic/checks/runDnsCheck.ts +// src/netshift/tabs/diagnostic/checks/runDnsCheck.ts async function runDnsCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS; updateCheckStore({ @@ -2648,7 +2651,7 @@ async function runDnsCheck() { state: "loading", items: [] }); - const dnsChecks = await PodkopShellMethods.checkDNSAvailable(); + const dnsChecks = await NetShiftShellMethods.checkDNSAvailable(); if (!dnsChecks.success) { updateCheckStore({ order, @@ -2703,7 +2706,7 @@ async function runDnsCheck() { } } -// src/podkop/tabs/diagnostic/checks/runSingBoxCheck.ts +// src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts async function runSingBoxCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX; updateCheckStore({ @@ -2714,7 +2717,7 @@ async function runSingBoxCheck() { state: "loading", items: [] }); - const singBoxChecks = await PodkopShellMethods.checkSingBox(); + const singBoxChecks = await NetShiftShellMethods.checkSingBox(); if (!singBoxChecks.success) { updateCheckStore({ order, @@ -2774,7 +2777,7 @@ async function runSingBoxCheck() { } } -// src/podkop/tabs/diagnostic/checks/runNftCheck.ts +// src/netshift/tabs/diagnostic/checks/runNftCheck.ts async function runNftCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT; updateCheckStore({ @@ -2787,7 +2790,7 @@ async function runNftCheck() { }); await RemoteFakeIPMethods.getFakeIpCheck(); await RemoteFakeIPMethods.getIpCheck(); - const nftablesChecks = await PodkopShellMethods.checkNftRules(); + const nftablesChecks = await NetShiftShellMethods.checkNftRules(); if (!nftablesChecks.success) { updateCheckStore({ order, @@ -2857,7 +2860,7 @@ async function runNftCheck() { } } -// src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts +// src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts async function runFakeIPCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP; updateCheckStore({ @@ -2868,7 +2871,7 @@ async function runFakeIPCheck() { state: "loading", items: [] }); - const routerFakeIPResponse = await PodkopShellMethods.checkFakeIP(); + const routerFakeIPResponse = await NetShiftShellMethods.checkFakeIP(); const checkFakeIPResponse = await RemoteFakeIPMethods.getFakeIpCheck(); const checkIPResponse = await RemoteFakeIPMethods.getIpCheck(); const checks = { @@ -3597,7 +3600,7 @@ ${styles2} ${styles3} `; -// src/podkop/tabs/diagnostic/partials/renderAvailableActions.ts +// src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts function renderAvailableActions({ restart, start, @@ -3617,7 +3620,7 @@ function renderAvailableActions({ classNames: ["cbi-button-apply"], onClick: restart.onClick, icon: renderRotateCcwIcon24, - text: _("Restart podkop"), + text: _("Restart NetShift"), loading: restart.loading, disabled: restart.disabled }) @@ -3627,7 +3630,7 @@ function renderAvailableActions({ classNames: ["cbi-button-remove"], onClick: stop.onClick, icon: renderCircleStopIcon24, - text: _("Stop podkop"), + text: _("Stop NetShift"), loading: stop.loading, disabled: stop.disabled }) @@ -3637,7 +3640,7 @@ function renderAvailableActions({ classNames: ["cbi-button-save"], onClick: start.onClick, icon: renderCirclePlayIcon24, - text: _("Start podkop"), + text: _("Start NetShift"), loading: start.loading, disabled: start.disabled }) @@ -3701,7 +3704,7 @@ function renderAvailableActions({ ]); } -// src/podkop/tabs/diagnostic/partials/renderCheckSection.ts +// src/netshift/tabs/diagnostic/partials/renderCheckSection.ts function renderCheckSummary(items) { if (!items.length) { return E("div", {}, ""); @@ -3856,7 +3859,7 @@ function renderCheckSection(props) { return E("div", {}, _("Not implement yet")); } -// src/podkop/tabs/diagnostic/partials/renderRunAction.ts +// src/netshift/tabs/diagnostic/partials/renderRunAction.ts function renderRunAction({ loading, click @@ -3872,7 +3875,7 @@ function renderRunAction({ ]); } -// src/podkop/tabs/diagnostic/partials/renderSystemInfo.ts +// src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts function renderSystemInfo({ items }) { return E("div", { class: "pdk_diagnostic-page__right-bar__system-info" }, [ E( @@ -3913,7 +3916,7 @@ function normalizeCompiledVersion(version) { return version; } -// src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts +// src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts function renderWikiDisclaimer(kind) { const iconWrap = E("span", { class: "pdk_diagnostic-page__right-bar__wiki__icon" @@ -3940,7 +3943,7 @@ function renderWikiDisclaimer(kind) { classNames: ["cbi-button-save"], text: _("Visit Wiki"), onClick: () => window.open( - "https://podkop.net/docs/troubleshooting/?utm_source=podkop", + "https://podkop.net/docs/troubleshooting/?utm_source=netshift", "_blank", "noopener,noreferrer" ) @@ -3948,7 +3951,7 @@ function renderWikiDisclaimer(kind) { ]); } -// src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts +// src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts async function runSectionsCheck() { const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS; updateCheckStore({ @@ -3975,9 +3978,7 @@ async function runSectionsCheck() { sections.data.map(async (section) => { async function getLatency() { if (section.withTagSelect) { - const latencyGroup = await PodkopShellMethods.getClashApiGroupLatency( - section.code - ); + const latencyGroup = await NetShiftShellMethods.getClashApiGroupLatency(section.code); const selectedOutbound = section.outbounds.find( (item) => item.selected ); @@ -4008,7 +4009,7 @@ async function runSectionsCheck() { latency: _("Not responding") }; } - const latencyProxy = await PodkopShellMethods.getClashApiProxyLatency( + const latencyProxy = await NetShiftShellMethods.getClashApiProxyLatency( section.code ); const success2 = latencyProxy.success && !latencyProxy.data.message; @@ -4052,27 +4053,27 @@ function removeVersionPrefix(version) { return version.replace(/^v/, ""); } -// src/podkop/tabs/diagnostic/helpers/getPodkopVersionRow.ts +// src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts function isUnknownVersion(version) { return version === "unknown" || version === _("unknown"); } -function getPodkopVersionRow(diagnosticsSystemInfo) { +function getNetshiftVersionRow(diagnosticsSystemInfo) { const loading = diagnosticsSystemInfo.loading; - const unknown = isUnknownVersion(diagnosticsSystemInfo.podkop_version); - const hasActualVersion = Boolean(diagnosticsSystemInfo.podkop_latest_version) && !isUnknownVersion(diagnosticsSystemInfo.podkop_latest_version); + const unknown = isUnknownVersion(diagnosticsSystemInfo.netshift_version); + const hasActualVersion = Boolean(diagnosticsSystemInfo.netshift_latest_version) && !isUnknownVersion(diagnosticsSystemInfo.netshift_latest_version); const version = normalizeCompiledVersion( - diagnosticsSystemInfo.podkop_version + diagnosticsSystemInfo.netshift_version ); const isDevVersion = version === "dev"; if (loading || unknown || !hasActualVersion || isDevVersion) { return { - key: "Podkop", + key: "NetShift", value: version }; } - if (removeVersionPrefix(version) !== removeVersionPrefix(diagnosticsSystemInfo.podkop_latest_version)) { + if (removeVersionPrefix(version) !== removeVersionPrefix(diagnosticsSystemInfo.netshift_latest_version)) { return { - key: "Podkop", + key: "NetShift", value: version, tag: { label: _("Outdated"), @@ -4081,7 +4082,7 @@ function getPodkopVersionRow(diagnosticsSystemInfo) { }; } return { - key: "Podkop", + key: "NetShift", value: version, tag: { label: _("Latest"), @@ -4090,9 +4091,9 @@ function getPodkopVersionRow(diagnosticsSystemInfo) { }; } -// src/podkop/tabs/diagnostic/initController.ts +// src/netshift/tabs/diagnostic/initController.ts async function fetchSystemInfo() { - const systemInfo = await PodkopShellMethods.getSystemInfo(); + const systemInfo = await NetShiftShellMethods.getSystemInfo(); if (systemInfo.success) { store.set({ diagnosticsSystemInfo: { @@ -4105,8 +4106,8 @@ async function fetchSystemInfo() { store.set({ diagnosticsSystemInfo: { loading: false, - podkop_version: _("unknown"), - podkop_latest_version: _("unknown"), + netshift_version: _("unknown"), + netshift_latest_version: _("unknown"), luci_app_version: _("unknown"), sing_box_version: _("unknown"), openwrt_version: _("unknown"), @@ -4148,7 +4149,7 @@ async function handleRestart() { } }); try { - await PodkopShellMethods.restart(); + await NetShiftShellMethods.restart(); } catch (e) { logger.error("[DIAGNOSTIC]", "handleRestart - e", e); } finally { @@ -4173,7 +4174,7 @@ async function handleStop() { } }); try { - await PodkopShellMethods.stop(); + await NetShiftShellMethods.stop(); } catch (e) { logger.error("[DIAGNOSTIC]", "handleStop - e", e); } finally { @@ -4196,7 +4197,7 @@ async function handleStart() { } }); try { - await PodkopShellMethods.start(); + await NetShiftShellMethods.start(); } catch (e) { logger.error("[DIAGNOSTIC]", "handleStart - e", e); } finally { @@ -4221,7 +4222,7 @@ async function handleEnable() { } }); try { - await PodkopShellMethods.enable(); + await NetShiftShellMethods.enable(); } catch (e) { logger.error("[DIAGNOSTIC]", "handleEnable - e", e); } finally { @@ -4243,7 +4244,7 @@ async function handleDisable() { } }); try { - await PodkopShellMethods.disable(); + await NetShiftShellMethods.disable(); } catch (e) { logger.error("[DIAGNOSTIC]", "handleDisable - e", e); } finally { @@ -4265,7 +4266,7 @@ async function handleShowGlobalCheck() { } }); try { - const globalCheck = await PodkopShellMethods.globalCheck(); + const globalCheck = await NetShiftShellMethods.globalCheck(); if (globalCheck.success) { ui.showModal( _("Global check"), @@ -4296,7 +4297,7 @@ async function handleViewLogs() { } }); try { - const viewLogs = await PodkopShellMethods.checkLogs(); + const viewLogs = await NetShiftShellMethods.checkLogs(); if (viewLogs.success) { ui.showModal( _("View logs"), @@ -4327,7 +4328,7 @@ async function handleShowSingBoxConfig() { } }); try { - const showSingBoxConfig = await PodkopShellMethods.showSingBoxConfig(); + const showSingBoxConfig = await NetShiftShellMethods.showSingBoxConfig(); if (showSingBoxConfig.success) { ui.showModal( _("Show sing-box config"), @@ -4366,7 +4367,7 @@ async function handleInstallSingBox() { }); const isExtended = store.get().diagnosticsSystemInfo.sing_box_extended === 1; try { - const result = await PodkopShellMethods.singBoxComponentAction( + const result = await NetShiftShellMethods.singBoxComponentAction( isExtended ? "install_stable" : "install_extended" ); if (result.success) { @@ -4412,7 +4413,7 @@ function renderDiagnosticAvailableActionsWidget() { const diagnosticsActions = store.get().diagnosticsActions; const servicesInfoWidget = store.get().servicesInfoWidget; logger.debug("[DIAGNOSTIC]", "renderDiagnosticAvailableActionsWidget"); - const podkopEnabled = Boolean(servicesInfoWidget.data.podkop); + const netshiftEnabled = Boolean(servicesInfoWidget.data.netshift); const singBoxRunning = Boolean(servicesInfoWidget.data.singbox); const atLeastOneServiceCommandLoading = servicesInfoWidget.loading || diagnosticsActions.restart.loading || diagnosticsActions.start.loading || diagnosticsActions.stop.loading; const container = document.getElementById("pdk_diagnostic-page-actions"); @@ -4437,13 +4438,13 @@ function renderDiagnosticAvailableActionsWidget() { }, enable: { loading: diagnosticsActions.enable.loading, - visible: !podkopEnabled, + visible: !netshiftEnabled, onClick: handleEnable, disabled: atLeastOneServiceCommandLoading }, disable: { loading: diagnosticsActions.disable.loading, - visible: podkopEnabled, + visible: netshiftEnabled, onClick: handleDisable, disabled: atLeastOneServiceCommandLoading }, @@ -4483,10 +4484,10 @@ function renderDiagnosticSystemInfoWidget() { const container = document.getElementById("pdk_diagnostic-page-system-info"); const renderedSystemInfo = renderSystemInfo({ items: [ - getPodkopVersionRow(diagnosticsSystemInfo), + getNetshiftVersionRow(diagnosticsSystemInfo), { key: "Luci App", - value: normalizeCompiledVersion(PODKOP_LUCI_APP_VERSION) + value: normalizeCompiledVersion(NETSHIFT_LUCI_APP_VERSION) }, { key: "Sing-box", @@ -4594,14 +4595,14 @@ async function initController2() { }); } -// src/podkop/tabs/diagnostic/styles.ts +// src/netshift/tabs/diagnostic/styles.ts var styles4 = ` -#cbi-podkop-diagnostic-_mount_node > div { +#cbi-netshift-diagnostic-_mount_node > div { width: 100%; } -#cbi-podkop-diagnostic > h3 { +#cbi-netshift-diagnostic > h3 { display: none; } @@ -4785,7 +4786,7 @@ var styles4 = ` } `; -// src/podkop/tabs/diagnostic/index.ts +// src/netshift/tabs/diagnostic/index.ts var DiagnosticTab = { render: render2, initController: initController2, @@ -4800,17 +4801,17 @@ ${PartialStyles} /* Hide extra H3 for settings tab */ -#cbi-podkop-settings > h3 { +#cbi-netshift-settings > h3 { display: none; } /* Hide extra H3 for sections tab */ -#cbi-podkop-section > h3:nth-child(1) { +#cbi-netshift-section > h3:nth-child(1) { display: none; } /* Vertical align for remove section action button */ -#cbi-podkop-section > .cbi-section-remove { +#cbi-netshift-section > .cbi-section-remove { margin-bottom: -32px; } @@ -5048,7 +5049,7 @@ return baseclass.extend({ CACHE_TIMEOUT, COMMAND_SCHEDULING, COMMAND_TIMEOUT, - CustomPodkopMethods, + CustomNetShiftMethods, DIAGNOSTICS_INITIAL_DELAY, DIAGNOSTICS_UPDATE_INTERVAL, DNS_SERVER_OPTIONS, @@ -5060,8 +5061,8 @@ return baseclass.extend({ FETCH_TIMEOUT, IP_CHECK_DOMAIN, Logger, - PODKOP_LUCI_APP_VERSION, - PodkopShellMethods, + NETSHIFT_LUCI_APP_VERSION, + NetShiftShellMethods, REGIONAL_OPTIONS, RemoteFakeIPMethods, STATUS_COLORS, diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js similarity index 73% rename from luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js rename to luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js index f699e173..4893143d 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js @@ -3,34 +3,34 @@ "require form"; "require baseclass"; "require network"; -"require view.podkop.main as main"; +"require view.netshift.main as main"; // Settings content -"require view.podkop.settings as settings"; +"require view.netshift.settings as settings"; // Sections content -"require view.podkop.section as section"; +"require view.netshift.section as section"; // Dashboard content -"require view.podkop.dashboard as dashboard"; +"require view.netshift.dashboard as dashboard"; // Diagnostic content -"require view.podkop.diagnostic as diagnostic"; +"require view.netshift.diagnostic as diagnostic"; const EntryPoint = { async render() { main.injectGlobalStyles(); - const podkopMap = new form.Map( - "podkop", - _("Podkop Settings"), - _("Configuration for Podkop service"), + const netshiftMap = new form.Map( + "netshift", + _("NetShift Settings"), + _("Configuration for NetShift service"), ); // Enable tab views - podkopMap.tabbed = true; + netshiftMap.tabbed = true; // Sections tab - const sectionsSection = podkopMap.section( + const sectionsSection = netshiftMap.section( form.TypedSection, "section", _("Sections"), @@ -43,7 +43,7 @@ const EntryPoint = { section.createSectionContent(sectionsSection); // Settings tab - const settingsSection = podkopMap.section( + const settingsSection = netshiftMap.section( form.TypedSection, "settings", _("Settings"), @@ -59,7 +59,7 @@ const EntryPoint = { settings.createSettingsContent(settingsSection); // Diagnostic tab - const diagnosticSection = podkopMap.section( + const diagnosticSection = netshiftMap.section( form.TypedSection, "diagnostic", _("Diagnostics"), @@ -74,7 +74,7 @@ const EntryPoint = { diagnostic.createDiagnosticContent(diagnosticSection); // Dashboard tab - const dashboardSection = podkopMap.section( + const dashboardSection = netshiftMap.section( form.TypedSection, "dashboard", _("Dashboard"), @@ -91,7 +91,7 @@ const EntryPoint = { // Inject core service main.coreService(); - return podkopMap.render(); + return netshiftMap.render(); }, }; diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js similarity index 99% rename from luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js rename to luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index a3f052d3..e15eb010 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -3,7 +3,7 @@ "require baseclass"; "require ui"; "require tools.widgets as widgets"; -"require view.podkop.main as main"; +"require view.netshift.main as main"; function createSectionContent(section) { let o = section.option( diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js similarity index 97% rename from luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js rename to luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js index efe3c29f..8f1cc56f 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js @@ -3,7 +3,7 @@ "require uci"; "require baseclass"; "require tools.widgets as widgets"; -"require view.podkop.main as main"; +"require view.netshift.main as main"; function createSettingsContent(section) { let o = section.option( @@ -219,7 +219,7 @@ function createSettingsContent(section) { form.Value, "badwan_reload_delay", _("Interface Monitoring Delay"), - _("Delay in milliseconds before reloading podkop after interface UP"), + _("Delay in milliseconds before reloading NetShift after interface UP"), ); o.depends("enable_badwan_interface_monitoring", "1"); o.default = "2000"; @@ -301,10 +301,10 @@ function createSettingsContent(section) { o.rmempty = false; o.depends("download_lists_via_proxy", "1"); o.cfgvalue = function (section_id) { - return uci.get("podkop", section_id, "download_lists_via_proxy_section"); + return uci.get("netshift", section_id, "download_lists_via_proxy_section"); }; o.load = function () { - const sections = this.map?.data?.state?.values?.podkop ?? {}; + const sections = this.map?.data?.state?.values?.netshift ?? {}; this.keylist = []; this.vallist = []; @@ -324,7 +324,7 @@ function createSettingsContent(section) { form.Flag, "dont_touch_dhcp", _("Dont Touch My DHCP!"), - _("Podkop will not modify your DHCP configuration"), + _("NetShift will not modify your DHCP configuration"), ); o.default = "0"; o.rmempty = false; diff --git a/luci-app-podkop/msgmerge.sh b/luci-app-netshift/msgmerge.sh similarity index 89% rename from luci-app-podkop/msgmerge.sh rename to luci-app-netshift/msgmerge.sh index 06e57061..a4b4ddb8 100644 --- a/luci-app-podkop/msgmerge.sh +++ b/luci-app-netshift/msgmerge.sh @@ -2,7 +2,7 @@ set -euo pipefail PODIR="po" -POTFILE="$PODIR/templates/podkop.pot" +POTFILE="$PODIR/templates/netshift.pot" WIDTH=120 if [ $# -ne 1 ]; then @@ -11,7 +11,7 @@ if [ $# -ne 1 ]; then fi LANG="$1" -POFILE="$PODIR/$LANG/podkop.po" +POFILE="$PODIR/$LANG/netshift.po" if [ ! -f "$POTFILE" ]; then echo "Template $POTFILE not found. Run xgettext first." diff --git a/fe-app-podkop/locales/podkop.ru.po b/luci-app-netshift/po/ru/netshift.po similarity index 96% rename from fe-app-podkop/locales/podkop.ru.po rename to luci-app-netshift/po/ru/netshift.po index 8bd2c236..2b4db66d 100644 --- a/fe-app-podkop/locales/podkop.ru.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -1,14 +1,14 @@ -# RU translations for PODKOP package. -# Copyright (C) 2026 THE PODKOP'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PODKOP package. +# RU translations for NETSHIFT package. +# Copyright (C) 2026 THE NETSHIFT'S COPYRIGHT HOLDER +# This file is distributed under the same license as the NETSHIFT package. # yandexru45, 2026. # msgid "" msgstr "" -"Project-Id-Version: PODKOP\n" +"Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-02 14:25+0300\n" -"PO-Revision-Date: 2026-06-02 14:25+0300\n" +"POT-Creation-Date: 2026-06-02 17:15+0300\n" +"PO-Revision-Date: 2026-06-02 17:15+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -101,8 +101,8 @@ msgstr "Списки сообщества" msgid "Config File Path" msgstr "Путь к файлу конфигурации" -msgid "Configuration for Podkop service" -msgstr "Настройки сервиса Podkop" +msgid "Configuration for NetShift service" +msgstr "" msgid "Configuration Type" msgstr "Тип конфигурации" @@ -125,8 +125,8 @@ msgstr "Дашборд" msgid "Dashboard currently unavailable" msgstr "Дашборд сейчас недоступен" -msgid "Delay in milliseconds before reloading podkop after interface UP" -msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса" +msgid "Delay in milliseconds before reloading NetShift after interface UP" +msgstr "" msgid "Delay value cannot be empty" msgstr "Значение задержки не может быть пустым" @@ -476,6 +476,15 @@ msgstr "Наблюдаемые интерфейсы" msgid "Must be a number in the range of 50 - 1000" msgstr "Должно быть числом от 50 до 1000" +msgid "NetShift" +msgstr "" + +msgid "NetShift Settings" +msgstr "" + +msgid "NetShift will not modify your DHCP configuration" +msgstr "" + msgid "Network Interface" msgstr "Сетевой интерфейс" @@ -521,15 +530,6 @@ msgstr "Путь должен заканчиваться на cache.db" msgid "Pending" msgstr "Ожидает запуска" -msgid "Podkop" -msgstr "Podkop" - -msgid "Podkop Settings" -msgstr "Настройки podkop" - -msgid "Podkop will not modify your DHCP configuration" -msgstr "Podkop не будет изменять вашу конфигурацию DHCP." - msgid "Proxy Configuration URL" msgstr "URL конфигурации прокси" @@ -551,8 +551,8 @@ msgstr "Внешние списки подсетей" msgid "Resolve real IP for routing" msgstr "Разрешение реальных IP-адресов" -msgid "Restart podkop" -msgstr "Перезапустить Podkop" +msgid "Restart NetShift" +msgstr "" msgid "Router DNS is not routed through sing-box" msgstr "DNS роутера не проходит через sing-box" @@ -698,11 +698,11 @@ msgstr "Укажите URL-адреса для загрузки и исполь msgid "Specify the path to the list file located on the router filesystem" msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора." -msgid "Start podkop" -msgstr "Запустить podkop" +msgid "Start NetShift" +msgstr "" -msgid "Stop podkop" -msgstr "Остановить podkop" +msgid "Stop NetShift" +msgstr "" msgid "Subscription" msgstr "" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot new file mode 100644 index 00000000..6e8ed43a --- /dev/null +++ b/luci-app-netshift/po/templates/netshift.pot @@ -0,0 +1,1183 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the NETSHIFT package. +# yandexru45 , 2026. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: NETSHIFT\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-06-02 14:15+0300\n" +"PO-Revision-Date: 2026-06-02 14:15+0300\n" +"Last-Translator: yandexru45 \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src\netshift\tabs\dashboard\initController.ts:345 +msgid "✔ Enabled" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:356 +msgid "✔ Running" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:346 +msgid "✘ Disabled" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:357 +msgid "✘ Stopped" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:127 +msgid "Группировать по странам" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:128 +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:307 +msgid "Active Connections" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:106 +msgid "Additional marking rules found" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:247 +msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:251 +msgid "Applicable for SOCKS and Shadowsocks proxy" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:496 +msgid "At least one valid domain must be specified. Comments-only content is not allowed." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:577 +msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:47 +msgid "Available actions" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:65 +msgid "Bootsrap DNS" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:45 +msgid "Bootstrap DNS server" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:58 +msgid "Browser is not using FakeIP" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:57 +msgid "Browser is using FakeIP correctly" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:348 +msgid "Cache File Path" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:362 +msgid "Cache file path cannot be empty" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:27 +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:28 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:27 +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:25 +msgid "Cannot receive checks result" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:15 +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:15 +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:13 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:15 +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:13 +msgid "Checking, please wait" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getCheckTitle.ts:2 +msgid "checks" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:26 +msgid "Checks failed" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:13 +msgid "Checks passed" +msgstr "" + +#: src\validators\validateSubnet.ts:33 +msgid "CIDR must be between 0 and 32" +msgstr "" + +#: src\partials\modal\renderModal.ts:26 +msgid "Close" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:351 +msgid "Community Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:335 +msgid "Config File Path" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:27 +msgid "Configuration for NetShift service" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:23 +msgid "Configuration Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:12 +msgid "Connection Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:26 +msgid "Connection URL" +msgstr "" + +#: src\partials\modal\renderModal.ts:20 +msgid "Copy" +msgstr "" + +#: src\netshift\tabs\dashboard\partials\renderWidget.ts:22 +msgid "Currently unavailable" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:80 +msgid "Dashboard" +msgstr "" + +#: src\netshift\tabs\dashboard\partials\renderSections.ts:19 +msgid "Dashboard currently unavailable" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:222 +msgid "Delay in milliseconds before reloading NetShift after interface UP" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:229 +msgid "Delay value cannot be empty" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:82 +msgid "DHCP has DNS server" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:65 +msgid "Diagnostics" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:83 +msgid "Disable autostart" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:265 +msgid "Disable QUIC" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:266 +msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:442 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:522 +msgid "Disabled" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:77 +msgid "DNS on router" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:319 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:15 +msgid "DNS over HTTPS (DoH)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:320 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:16 +msgid "DNS over TLS (DoT)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:316 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:12 +msgid "DNS Protocol Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:68 +msgid "DNS Rewrite TTL" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:329 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:24 +msgid "DNS Server" +msgstr "" + +#: src\validators\validateDns.ts:7 +msgid "DNS server address cannot be empty" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:26 +msgid "Do not panic, everything can be fixed, just..." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:306 +msgid "Domain Resolver" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:326 +msgid "Dont Touch My DHCP!" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:241 +#: src\netshift\tabs\dashboard\initController.ts:275 +msgid "Downlink" +msgstr "" + +#: src\partials\modal\renderModal.ts:15 +msgid "Download" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:288 +msgid "Download Lists via Proxy/VPN" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:297 +msgid "Download Lists via specific proxy section" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:289 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:298 +msgid "Downloading all lists via specific Proxy/VPN" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:443 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:523 +msgid "Dynamic List" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:93 +msgid "Enable autostart" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:307 +msgid "Enable built-in DNS resolver for domains handled by this section" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:746 +msgid "Enable DNS resolve to get real IP when routing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:717 +msgid "Enable Mixed Proxy" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:126 +msgid "Enable Output Network Interface" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:718 +msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:237 +msgid "Enable YACD" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:246 +msgid "Enable YACD WAN Access" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:67 +msgid "Enter complete outbound configuration in JSON format" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:478 +msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:452 +msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:532 +msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:90 +msgid "Enter the subscription URL to fetch proxy configurations from your provider" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:187 +msgid "Every 1 minute" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:119 +msgid "Every 12 hours" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:117 +msgid "Every 3 hours" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:188 +msgid "Every 3 minutes" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:115 +msgid "Every 30 minutes" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:186 +msgid "Every 30 seconds" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:189 +msgid "Every 5 minutes" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:118 +msgid "Every 6 hours" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:120 +msgid "Every day" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:116 +msgid "Every hour" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:402 +msgid "Exclude NTP" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:403 +msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" +msgstr "" + +#: src\helpers\copyToClipboard.ts:12 +msgid "Failed to copy!" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:229 +#: src\netshift\tabs\diagnostic\initController.ts:233 +#: src\netshift\tabs\diagnostic\initController.ts:263 +#: src\netshift\tabs\diagnostic\initController.ts:267 +#: src\netshift\tabs\diagnostic\initController.ts:304 +#: src\netshift\tabs\diagnostic\initController.ts:308 +#: src\netshift\tabs\diagnostic\initController.ts:342 +#: src\netshift\tabs\diagnostic\initController.ts:346 +msgid "Failed to execute!" +msgstr "" + +#: src\netshift\methods\custom\getDashboardSections.ts:150 +#: src\netshift\methods\custom\getDashboardSections.ts:181 +#: src\netshift\methods\custom\getDashboardSections.ts:218 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:58 +msgid "Fastest" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:690 +msgid "Fully Routed IPs" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:102 +msgid "Get global check" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:224 +msgid "Global check" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:113 +msgid "How often to automatically update the subscription" +msgstr "" + +#: src\netshift\api.ts:27 +msgid "HTTP error" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 +msgid "Install extended" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 +msgid "Install stable" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:189 +msgid "Interface Monitoring" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:221 +msgid "Interface Monitoring Delay" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:190 +msgid "Interface monitoring for Bad WAN" +msgstr "" + +#: src\validators\validateDns.ts:23 +msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" +msgstr "" + +#: src\validators\validateDomain.ts:18 +#: src\validators\validateDomain.ts:27 +msgid "Invalid domain address" +msgstr "" + +#: src\validators\validateSubnet.ts:11 +msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:90 +msgid "Invalid HY2 URL: insecure must be 0 or 1" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:77 +msgid "Invalid HY2 URL: invalid port number" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:30 +msgid "Invalid HY2 URL: missing credentials/server" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:47 +msgid "Invalid HY2 URL: missing host" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:41 +msgid "Invalid HY2 URL: missing host & port" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:36 +msgid "Invalid HY2 URL: missing password" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:50 +msgid "Invalid HY2 URL: missing port" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:18 +msgid "Invalid HY2 URL: must not contain spaces" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:12 +msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:108 +msgid "Invalid HY2 URL: obfs-password required when obfs is set" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:122 +msgid "Invalid HY2 URL: parsing failed" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:116 +msgid "Invalid HY2 URL: sni cannot be empty" +msgstr "" + +#: src\validators\validateHysteriaUrl.ts:98 +msgid "Invalid HY2 URL: unsupported obfs type" +msgstr "" + +#: src\validators\validateIp.ts:11 +msgid "Invalid IP address" +msgstr "" + +#: src\validators\validateOutboundJson.ts:9 +msgid "Invalid JSON format" +msgstr "" + +#: src\validators\validatePath.ts:22 +msgid "Invalid path format. Path must start with \"/\" and contain valid characters" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:85 +msgid "Invalid port number. Must be between 1 and 65535" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:37 +msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:27 +msgid "Invalid Shadowsocks URL: missing credentials" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:46 +msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:76 +msgid "Invalid Shadowsocks URL: missing port" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:67 +msgid "Invalid Shadowsocks URL: missing server" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:58 +msgid "Invalid Shadowsocks URL: missing server address" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:16 +msgid "Invalid Shadowsocks URL: must not contain spaces" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:8 +msgid "Invalid Shadowsocks URL: must start with ss://" +msgstr "" + +#: src\validators\validateShadowsocksUrl.ts:91 +msgid "Invalid Shadowsocks URL: parsing failed" +msgstr "" + +#: src\validators\validateSocksUrl.ts:73 +msgid "Invalid SOCKS URL: invalid host format" +msgstr "" + +#: src\validators\validateSocksUrl.ts:63 +msgid "Invalid SOCKS URL: invalid port number" +msgstr "" + +#: src\validators\validateSocksUrl.ts:42 +msgid "Invalid SOCKS URL: missing host and port" +msgstr "" + +#: src\validators\validateSocksUrl.ts:51 +msgid "Invalid SOCKS URL: missing hostname or IP" +msgstr "" + +#: src\validators\validateSocksUrl.ts:56 +msgid "Invalid SOCKS URL: missing port" +msgstr "" + +#: src\validators\validateSocksUrl.ts:34 +msgid "Invalid SOCKS URL: missing username" +msgstr "" + +#: src\validators\validateSocksUrl.ts:19 +msgid "Invalid SOCKS URL: must not contain spaces" +msgstr "" + +#: src\validators\validateSocksUrl.ts:10 +msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" +msgstr "" + +#: src\validators\validateSocksUrl.ts:77 +msgid "Invalid SOCKS URL: parsing failed" +msgstr "" + +#: src\validators\validateTrojanUrl.ts:15 +msgid "Invalid Trojan URL: must not contain spaces" +msgstr "" + +#: src\validators\validateTrojanUrl.ts:8 +msgid "Invalid Trojan URL: must start with trojan://" +msgstr "" + +#: src\validators\validateTrojanUrl.ts:56 +msgid "Invalid Trojan URL: parsing failed" +msgstr "" + +#: src\validators\validateUrl.ts:8 +#: src\validators\validateUrl.ts:31 +msgid "Invalid URL format" +msgstr "" + +#: src\validators\validateVlessUrl.ts:110 +msgid "Invalid VLESS URL: parsing failed" +msgstr "" + +#: src\validators\validateSubnet.ts:18 +msgid "IP address 0.0.0.0 is not allowed" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:20 +msgid "Issues detected" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:48 +msgid "Latest" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:276 +msgid "List Update Frequency" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:598 +msgid "Local Domain Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:621 +msgid "Local Subnet Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:384 +msgid "Log Level" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:72 +msgid "Main DNS" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:311 +msgid "Memory Usage" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:730 +msgid "Mixed Proxy Port" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:198 +msgid "Monitored Interfaces" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:215 +msgid "Must be a number in the range of 50 - 1000" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:343 +msgid "NetShift" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:26 +msgid "NetShift Settings" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:327 +msgid "NetShift will not modify your DHCP configuration" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:260 +msgid "Network Interface" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:105 +msgid "No other marking rules found" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderCheckSection.ts:189 +msgid "Not implement yet" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:74 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:80 +#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:99 +msgid "Not responding" +msgstr "" + +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:59 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:67 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:75 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:83 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:91 +msgid "Not running" +msgstr "" + +#: src\helpers\withTimeout.ts:7 +msgid "Operation timed out" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:30 +msgid "Outbound Config" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:66 +msgid "Outbound Configuration" +msgstr "" + +#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:38 +msgid "Outdated" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:135 +msgid "Output Network Interface" +msgstr "" + +#: src\validators\validatePath.ts:7 +msgid "Path cannot be empty" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:366 +msgid "Path must be absolute (start with /)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:375 +msgid "Path must contain at least one directory (like /tmp/cache.db)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:370 +msgid "Path must end with cache.db" +msgstr "" + +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:107 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:115 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:123 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:131 +#: src\netshift\tabs\diagnostic\diagnostic.store.ts:139 +msgid "Pending" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:37 +msgid "Proxy Configuration URL" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:66 +msgid "Proxy traffic is not routed via FakeIP" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:65 +msgid "Proxy traffic is routed via FakeIP" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:385 +msgid "Regional options cannot be used together" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:644 +msgid "Remote Domain Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:667 +msgid "Remote Subnet Lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:745 +msgid "Resolve real IP for routing" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:53 +msgid "Restart NetShift" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:51 +msgid "Router DNS is not routed through sing-box" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:50 +msgid "Router DNS is routed through sing-box" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:413 +msgid "Routing Excluded IPs" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:79 +msgid "Rules mangle counters" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:74 +msgid "Rules mangle exist" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:89 +msgid "Rules mangle output counters" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:84 +msgid "Rules mangle output exist" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:99 +msgid "Rules proxy counters" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:94 +msgid "Rules proxy exist" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderRunAction.ts:15 +msgid "Run Diagnostic" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:404 +msgid "Russia inside restrictions" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:257 +msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:36 +msgid "Sections" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:352 +msgid "Select a predefined list for routing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:13 +msgid "Select between VPN and Proxy connection methods for traffic routing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:13 +msgid "Select DNS protocol to use" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:277 +msgid "Select how often the domain or subnet lists are updated automatically" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:24 +msgid "Select how to configure the proxy" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:261 +msgid "Select network interface for VPN connection" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:330 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:25 +msgid "Select or enter DNS server address" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:349 +msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:336 +msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:317 +msgid "Select the DNS protocol type for the domain resolver" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:440 +msgid "Select the list type for adding custom domains" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:520 +msgid "Select the list type for adding custom subnets" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:385 +msgid "Select the log level for sing-box" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:90 +msgid "Select the network interface from which the traffic will originate" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:136 +msgid "Select the network interface to which the traffic will originate" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:199 +msgid "Select the WAN interfaces to be monitored" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:27 +msgid "Selector" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:137 +msgid "Selector Proxy Links" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:340 +msgid "Services info" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:49 +msgid "Settings" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:292 +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:120 +msgid "Show sing-box config" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:354 +msgid "Sing-box" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:77 +msgid "Sing-box autostart disabled" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:337 +msgid "Sing-box core changed, version:" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:62 +msgid "Sing-box installed" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:87 +msgid "Sing-box listening ports" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:82 +msgid "Sing-box process running" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:72 +msgid "Sing-box service exist" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:67 +msgid "Sing-box version is compatible (newer than 1.12.4)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:89 +msgid "Source Network Interface" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:414 +msgid "Specify a local IP address to be excluded from routing" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:691 +msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:645 +msgid "Specify remote URLs to download and use domain lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:668 +msgid "Specify remote URLs to download and use subnet lists" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:599 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:622 +msgid "Specify the path to the list file located on the router filesystem" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:73 +msgid "Start NetShift" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:63 +msgid "Stop NetShift" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:29 +msgid "Subscription" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:112 +msgid "Subscription Update Interval" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:89 +msgid "Subscription URL" +msgstr "" + +#: src\helpers\copyToClipboard.ts:10 +msgid "Successfully copied!" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:304 +msgid "System info" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderSystemInfo.ts:21 +msgid "System information" +msgstr "" + +#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:69 +msgid "Table exist" +msgstr "" + +#: src\netshift\tabs\dashboard\partials\renderSections.ts:108 +msgid "Test latency" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:444 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:524 +msgid "Text List" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:46 +msgid "The DNS server used to look up the IP address of an upstream DNS server" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:184 +msgid "The interval between connectivity tests" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:198 +msgid "The maximum difference in response times (ms) allowed when comparing servers" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:222 +msgid "The URL used to test server connectivity" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:69 +msgid "Time in seconds for DNS record caching (default: 60)" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:238 +msgid "Traffic" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:268 +msgid "Traffic Total" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:25 +msgid "Troubleshooting" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:80 +msgid "TTL must be a positive number" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:75 +msgid "TTL value cannot be empty" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:321 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:17 +msgid "UDP (Unprotected DNS)" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:250 +msgid "UDP over TCP" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:39 +#: src\netshift\tabs\diagnostic\initController.ts:40 +#: src\netshift\tabs\diagnostic\initController.ts:41 +#: src\netshift\tabs\diagnostic\initController.ts:42 +#: src\netshift\tabs\diagnostic\initController.ts:43 +#: src\netshift\tabs\diagnostic\initController.ts:44 +#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:7 +msgid "unknown" +msgstr "" + +#: src\netshift\api.ts:40 +msgid "Unknown error" +msgstr "" + +#: src\netshift\tabs\dashboard\initController.ts:240 +#: src\netshift\tabs\dashboard\initController.ts:271 +msgid "Uplink" +msgstr "" + +#: src\validators\validateProxyUrl.ts:37 +msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" +msgstr "" + +#: src\validators\validateUrl.ts:17 +msgid "URL must use one of the following protocols:" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:28 +msgid "URLTest" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:183 +msgid "URLTest Check Interval" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:160 +msgid "URLTest Proxy Links" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:221 +msgid "URLTest Testing URL" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:197 +msgid "URLTest Tolerance" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:439 +msgid "User Domain List Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:451 +msgid "User Domains" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:477 +msgid "User Domains List" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:519 +msgid "User Subnet List Type" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:531 +msgid "User Subnets" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:557 +msgid "User Subnets List" +msgstr "" + +#: src\validators\validateDns.ts:14 +#: src\validators\validateDns.ts:18 +#: src\validators\validateDomain.ts:13 +#: src\validators\validateDomain.ts:30 +#: src\validators\validateHysteriaUrl.ts:120 +#: src\validators\validateIp.ts:8 +#: src\validators\validateOutboundJson.ts:7 +#: src\validators\validatePath.ts:16 +#: src\validators\validateShadowsocksUrl.ts:95 +#: src\validators\validateSocksUrl.ts:80 +#: src\validators\validateSubnet.ts:38 +#: src\validators\validateTrojanUrl.ts:59 +#: src\validators\validateUrl.ts:28 +#: src\validators\validateVlessUrl.ts:108 +msgid "Valid" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:510 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:589 +msgid "Validation errors:" +msgstr "" + +#: src\netshift\tabs\diagnostic\initController.ts:258 +#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:111 +msgid "View logs" +msgstr "" + +#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:31 +msgid "Visit Wiki" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:38 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:138 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:161 +msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:387 +msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:406 +msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:256 +msgid "YACD Secret Key" +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:127 +msgid "You can select Output Network Interface, by default autodetect" +msgstr "" diff --git a/luci-app-podkop/root/etc/uci-defaults/50_luci-podkop b/luci-app-netshift/root/etc/uci-defaults/50_luci-netshift similarity index 66% rename from luci-app-podkop/root/etc/uci-defaults/50_luci-podkop rename to luci-app-netshift/root/etc/uci-defaults/50_luci-netshift index 519c3581..67f6c1e1 100644 --- a/luci-app-podkop/root/etc/uci-defaults/50_luci-podkop +++ b/luci-app-netshift/root/etc/uci-defaults/50_luci-netshift @@ -5,6 +5,6 @@ rm -f /tmp/luci-indexcache* [ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd reload -logger -t "podkop" "$timestamp uci-defaults script executed" +logger -t "netshift" "$timestamp uci-defaults script executed" exit 0 \ No newline at end of file diff --git a/luci-app-netshift/root/usr/share/luci/menu.d/luci-app-netshift.json b/luci-app-netshift/root/usr/share/luci/menu.d/luci-app-netshift.json new file mode 100644 index 00000000..455f866e --- /dev/null +++ b/luci-app-netshift/root/usr/share/luci/menu.d/luci-app-netshift.json @@ -0,0 +1,14 @@ +{ + "admin/services/netshift": { + "title": "NetShift", + "order": 42, + "action": { + "type": "view", + "path": "netshift/netshift" + }, + "depends": { + "acl": [ "luci-app-netshift" ], + "uci": { "netshift": true } + } + } +} diff --git a/luci-app-podkop/root/usr/share/rpcd/acl.d/luci-app-podkop.json b/luci-app-netshift/root/usr/share/rpcd/acl.d/luci-app-netshift.json similarity index 55% rename from luci-app-podkop/root/usr/share/rpcd/acl.d/luci-app-podkop.json rename to luci-app-netshift/root/usr/share/rpcd/acl.d/luci-app-netshift.json index 6d0eabcb..26c097e7 100644 --- a/luci-app-podkop/root/usr/share/rpcd/acl.d/luci-app-podkop.json +++ b/luci-app-netshift/root/usr/share/rpcd/acl.d/luci-app-netshift.json @@ -1,12 +1,12 @@ { - "luci-app-podkop": { - "description": "Grant UCI and RPC access to LuCI app podkop", + "luci-app-netshift": { + "description": "Grant UCI and RPC access to LuCI app NetShift", "read": { "file": { - "/etc/init.d/podkop": [ + "/etc/init.d/netshift": [ "exec" ], - "/usr/bin/podkop": [ + "/usr/bin/netshift": [ "exec" ] }, @@ -16,13 +16,13 @@ ] }, "uci": [ - "podkop" + "netshift" ] }, "write": { "uci": [ - "podkop" + "netshift" ] } } -} \ No newline at end of file +} diff --git a/luci-app-podkop/xgettext.sh b/luci-app-netshift/xgettext.sh similarity index 82% rename from luci-app-podkop/xgettext.sh rename to luci-app-netshift/xgettext.sh index 0db78fbd..29d9dfb6 100644 --- a/luci-app-podkop/xgettext.sh +++ b/luci-app-netshift/xgettext.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -SRC_DIR="htdocs/luci-static/resources/view/podkop" -OUT_POT="po/templates/podkop.pot" +SRC_DIR="htdocs/luci-static/resources/view/netshift" +OUT_POT="po/templates/netshift.pot" ENCODING="UTF-8" WIDTH=120 @@ -21,7 +21,7 @@ xgettext --language=JavaScript \ --from-code="$ENCODING" \ --output="$OUT_POT" \ --width="$WIDTH" \ - --package-name="PODKOP" \ + --package-name="NETSHIFT" \ "${FILES[@]}" echo "POT template generated: $OUT_POT" \ No newline at end of file diff --git a/luci-app-podkop/po/templates/podkop.pot b/luci-app-podkop/po/templates/podkop.pot deleted file mode 100644 index a722424c..00000000 --- a/luci-app-podkop/po/templates/podkop.pot +++ /dev/null @@ -1,1183 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PODKOP package. -# yandexru45 , 2026. -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PODKOP\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-02 11:25+0300\n" -"PO-Revision-Date: 2026-06-02 11:25+0300\n" -"Last-Translator: yandexru45 \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: src\podkop\tabs\dashboard\initController.ts:345 -msgid "✔ Enabled" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:356 -msgid "✔ Running" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:346 -msgid "✘ Disabled" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:357 -msgid "✘ Stopped" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:127 -msgid "Группировать по странам" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:128 -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:307 -msgid "Active Connections" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:106 -msgid "Additional marking rules found" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:247 -msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:251 -msgid "Applicable for SOCKS and Shadowsocks proxy" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:496 -msgid "At least one valid domain must be specified. Comments-only content is not allowed." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:577 -msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:47 -msgid "Available actions" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:65 -msgid "Bootsrap DNS" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:45 -msgid "Bootstrap DNS server" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:58 -msgid "Browser is not using FakeIP" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:57 -msgid "Browser is using FakeIP correctly" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:348 -msgid "Cache File Path" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:362 -msgid "Cache file path cannot be empty" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:27 -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:28 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:27 -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:25 -msgid "Cannot receive checks result" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:15 -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:15 -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:13 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:15 -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:13 -msgid "Checking, please wait" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getCheckTitle.ts:2 -msgid "checks" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:26 -msgid "Checks failed" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:13 -msgid "Checks passed" -msgstr "" - -#: src\validators\validateSubnet.ts:33 -msgid "CIDR must be between 0 and 32" -msgstr "" - -#: src\partials\modal\renderModal.ts:26 -msgid "Close" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:351 -msgid "Community Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:335 -msgid "Config File Path" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:27 -msgid "Configuration for Podkop service" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:23 -msgid "Configuration Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:12 -msgid "Connection Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:26 -msgid "Connection URL" -msgstr "" - -#: src\partials\modal\renderModal.ts:20 -msgid "Copy" -msgstr "" - -#: src\podkop\tabs\dashboard\partials\renderWidget.ts:22 -msgid "Currently unavailable" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:80 -msgid "Dashboard" -msgstr "" - -#: src\podkop\tabs\dashboard\partials\renderSections.ts:19 -msgid "Dashboard currently unavailable" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:222 -msgid "Delay in milliseconds before reloading podkop after interface UP" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:229 -msgid "Delay value cannot be empty" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:82 -msgid "DHCP has DNS server" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:65 -msgid "Diagnostics" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:83 -msgid "Disable autostart" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:265 -msgid "Disable QUIC" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:266 -msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:442 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:522 -msgid "Disabled" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:77 -msgid "DNS on router" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:319 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:15 -msgid "DNS over HTTPS (DoH)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:320 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:16 -msgid "DNS over TLS (DoT)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:316 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:12 -msgid "DNS Protocol Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:68 -msgid "DNS Rewrite TTL" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:329 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:24 -msgid "DNS Server" -msgstr "" - -#: src\validators\validateDns.ts:7 -msgid "DNS server address cannot be empty" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:26 -msgid "Do not panic, everything can be fixed, just..." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:306 -msgid "Domain Resolver" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:326 -msgid "Dont Touch My DHCP!" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:241 -#: src\podkop\tabs\dashboard\initController.ts:275 -msgid "Downlink" -msgstr "" - -#: src\partials\modal\renderModal.ts:15 -msgid "Download" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:288 -msgid "Download Lists via Proxy/VPN" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:297 -msgid "Download Lists via specific proxy section" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:289 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:298 -msgid "Downloading all lists via specific Proxy/VPN" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:443 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:523 -msgid "Dynamic List" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:93 -msgid "Enable autostart" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:307 -msgid "Enable built-in DNS resolver for domains handled by this section" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:746 -msgid "Enable DNS resolve to get real IP when routing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:717 -msgid "Enable Mixed Proxy" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:126 -msgid "Enable Output Network Interface" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:718 -msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:237 -msgid "Enable YACD" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:246 -msgid "Enable YACD WAN Access" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:67 -msgid "Enter complete outbound configuration in JSON format" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:478 -msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:452 -msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:532 -msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:90 -msgid "Enter the subscription URL to fetch proxy configurations from your provider" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:187 -msgid "Every 1 minute" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:119 -msgid "Every 12 hours" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:117 -msgid "Every 3 hours" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:188 -msgid "Every 3 minutes" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:115 -msgid "Every 30 minutes" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:186 -msgid "Every 30 seconds" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:189 -msgid "Every 5 minutes" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:118 -msgid "Every 6 hours" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:120 -msgid "Every day" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:116 -msgid "Every hour" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:402 -msgid "Exclude NTP" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:403 -msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" -msgstr "" - -#: src\helpers\copyToClipboard.ts:12 -msgid "Failed to copy!" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:229 -#: src\podkop\tabs\diagnostic\initController.ts:233 -#: src\podkop\tabs\diagnostic\initController.ts:263 -#: src\podkop\tabs\diagnostic\initController.ts:267 -#: src\podkop\tabs\diagnostic\initController.ts:304 -#: src\podkop\tabs\diagnostic\initController.ts:308 -#: src\podkop\tabs\diagnostic\initController.ts:342 -#: src\podkop\tabs\diagnostic\initController.ts:346 -msgid "Failed to execute!" -msgstr "" - -#: src\podkop\methods\custom\getDashboardSections.ts:150 -#: src\podkop\methods\custom\getDashboardSections.ts:181 -#: src\podkop\methods\custom\getDashboardSections.ts:218 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:59 -msgid "Fastest" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:690 -msgid "Fully Routed IPs" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:102 -msgid "Get global check" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:224 -msgid "Global check" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:113 -msgid "How often to automatically update the subscription" -msgstr "" - -#: src\podkop\api.ts:27 -msgid "HTTP error" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:129 -msgid "Install extended" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:129 -msgid "Install stable" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:189 -msgid "Interface Monitoring" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:221 -msgid "Interface Monitoring Delay" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:190 -msgid "Interface monitoring for Bad WAN" -msgstr "" - -#: src\validators\validateDns.ts:23 -msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" -msgstr "" - -#: src\validators\validateDomain.ts:18 -#: src\validators\validateDomain.ts:27 -msgid "Invalid domain address" -msgstr "" - -#: src\validators\validateSubnet.ts:11 -msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:90 -msgid "Invalid HY2 URL: insecure must be 0 or 1" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:77 -msgid "Invalid HY2 URL: invalid port number" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:30 -msgid "Invalid HY2 URL: missing credentials/server" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:47 -msgid "Invalid HY2 URL: missing host" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:41 -msgid "Invalid HY2 URL: missing host & port" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:36 -msgid "Invalid HY2 URL: missing password" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:50 -msgid "Invalid HY2 URL: missing port" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:18 -msgid "Invalid HY2 URL: must not contain spaces" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:12 -msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:108 -msgid "Invalid HY2 URL: obfs-password required when obfs is set" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:122 -msgid "Invalid HY2 URL: parsing failed" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:116 -msgid "Invalid HY2 URL: sni cannot be empty" -msgstr "" - -#: src\validators\validateHysteriaUrl.ts:98 -msgid "Invalid HY2 URL: unsupported obfs type" -msgstr "" - -#: src\validators\validateIp.ts:11 -msgid "Invalid IP address" -msgstr "" - -#: src\validators\validateOutboundJson.ts:9 -msgid "Invalid JSON format" -msgstr "" - -#: src\validators\validatePath.ts:22 -msgid "Invalid path format. Path must start with \"/\" and contain valid characters" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:85 -msgid "Invalid port number. Must be between 1 and 65535" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:37 -msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:27 -msgid "Invalid Shadowsocks URL: missing credentials" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:46 -msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:76 -msgid "Invalid Shadowsocks URL: missing port" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:67 -msgid "Invalid Shadowsocks URL: missing server" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:58 -msgid "Invalid Shadowsocks URL: missing server address" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:16 -msgid "Invalid Shadowsocks URL: must not contain spaces" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:8 -msgid "Invalid Shadowsocks URL: must start with ss://" -msgstr "" - -#: src\validators\validateShadowsocksUrl.ts:91 -msgid "Invalid Shadowsocks URL: parsing failed" -msgstr "" - -#: src\validators\validateSocksUrl.ts:73 -msgid "Invalid SOCKS URL: invalid host format" -msgstr "" - -#: src\validators\validateSocksUrl.ts:63 -msgid "Invalid SOCKS URL: invalid port number" -msgstr "" - -#: src\validators\validateSocksUrl.ts:42 -msgid "Invalid SOCKS URL: missing host and port" -msgstr "" - -#: src\validators\validateSocksUrl.ts:51 -msgid "Invalid SOCKS URL: missing hostname or IP" -msgstr "" - -#: src\validators\validateSocksUrl.ts:56 -msgid "Invalid SOCKS URL: missing port" -msgstr "" - -#: src\validators\validateSocksUrl.ts:34 -msgid "Invalid SOCKS URL: missing username" -msgstr "" - -#: src\validators\validateSocksUrl.ts:19 -msgid "Invalid SOCKS URL: must not contain spaces" -msgstr "" - -#: src\validators\validateSocksUrl.ts:10 -msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" -msgstr "" - -#: src\validators\validateSocksUrl.ts:77 -msgid "Invalid SOCKS URL: parsing failed" -msgstr "" - -#: src\validators\validateTrojanUrl.ts:15 -msgid "Invalid Trojan URL: must not contain spaces" -msgstr "" - -#: src\validators\validateTrojanUrl.ts:8 -msgid "Invalid Trojan URL: must start with trojan://" -msgstr "" - -#: src\validators\validateTrojanUrl.ts:56 -msgid "Invalid Trojan URL: parsing failed" -msgstr "" - -#: src\validators\validateUrl.ts:8 -#: src\validators\validateUrl.ts:31 -msgid "Invalid URL format" -msgstr "" - -#: src\validators\validateVlessUrl.ts:110 -msgid "Invalid VLESS URL: parsing failed" -msgstr "" - -#: src\validators\validateSubnet.ts:18 -msgid "IP address 0.0.0.0 is not allowed" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getMeta.ts:20 -msgid "Issues detected" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:48 -msgid "Latest" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:276 -msgid "List Update Frequency" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:598 -msgid "Local Domain Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:621 -msgid "Local Subnet Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:384 -msgid "Log Level" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runDnsCheck.ts:72 -msgid "Main DNS" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:311 -msgid "Memory Usage" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:730 -msgid "Mixed Proxy Port" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:198 -msgid "Monitored Interfaces" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:215 -msgid "Must be a number in the range of 50 - 1000" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:260 -msgid "Network Interface" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:105 -msgid "No other marking rules found" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderCheckSection.ts:189 -msgid "Not implement yet" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:75 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:81 -#: src\podkop\tabs\diagnostic\checks\runSectionsCheck.ts:100 -msgid "Not responding" -msgstr "" - -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:59 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:67 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:75 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:83 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:91 -msgid "Not running" -msgstr "" - -#: src\helpers\withTimeout.ts:7 -msgid "Operation timed out" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:30 -msgid "Outbound Config" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:66 -msgid "Outbound Configuration" -msgstr "" - -#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:38 -msgid "Outdated" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:135 -msgid "Output Network Interface" -msgstr "" - -#: src\validators\validatePath.ts:7 -msgid "Path cannot be empty" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:366 -msgid "Path must be absolute (start with /)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:375 -msgid "Path must contain at least one directory (like /tmp/cache.db)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:370 -msgid "Path must end with cache.db" -msgstr "" - -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:107 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:115 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:123 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:131 -#: src\podkop\tabs\diagnostic\diagnostic.store.ts:139 -msgid "Pending" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:343 -msgid "Podkop" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:26 -msgid "Podkop Settings" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:327 -msgid "Podkop will not modify your DHCP configuration" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:37 -msgid "Proxy Configuration URL" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:66 -msgid "Proxy traffic is not routed via FakeIP" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:65 -msgid "Proxy traffic is routed via FakeIP" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:385 -msgid "Regional options cannot be used together" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:644 -msgid "Remote Domain Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:667 -msgid "Remote Subnet Lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:745 -msgid "Resolve real IP for routing" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:53 -msgid "Restart podkop" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:51 -msgid "Router DNS is not routed through sing-box" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runFakeIPCheck.ts:50 -msgid "Router DNS is routed through sing-box" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:413 -msgid "Routing Excluded IPs" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:79 -msgid "Rules mangle counters" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:74 -msgid "Rules mangle exist" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:89 -msgid "Rules mangle output counters" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:84 -msgid "Rules mangle output exist" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:99 -msgid "Rules proxy counters" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:94 -msgid "Rules proxy exist" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderRunAction.ts:15 -msgid "Run Diagnostic" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:404 -msgid "Russia inside restrictions" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:257 -msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:36 -msgid "Sections" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:352 -msgid "Select a predefined list for routing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:13 -msgid "Select between VPN and Proxy connection methods for traffic routing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:13 -msgid "Select DNS protocol to use" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:277 -msgid "Select how often the domain or subnet lists are updated automatically" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:24 -msgid "Select how to configure the proxy" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:261 -msgid "Select network interface for VPN connection" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:330 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:25 -msgid "Select or enter DNS server address" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:349 -msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:336 -msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:317 -msgid "Select the DNS protocol type for the domain resolver" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:440 -msgid "Select the list type for adding custom domains" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:520 -msgid "Select the list type for adding custom subnets" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:385 -msgid "Select the log level for sing-box" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:90 -msgid "Select the network interface from which the traffic will originate" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:136 -msgid "Select the network interface to which the traffic will originate" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:199 -msgid "Select the WAN interfaces to be monitored" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:27 -msgid "Selector" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:137 -msgid "Selector Proxy Links" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:340 -msgid "Services info" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\podkop.js:49 -msgid "Settings" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:292 -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:120 -msgid "Show sing-box config" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:354 -msgid "Sing-box" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:77 -msgid "Sing-box autostart disabled" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:337 -msgid "Sing-box core changed, version:" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:62 -msgid "Sing-box installed" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:87 -msgid "Sing-box listening ports" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:82 -msgid "Sing-box process running" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:72 -msgid "Sing-box service exist" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runSingBoxCheck.ts:67 -msgid "Sing-box version is compatible (newer than 1.12.4)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:89 -msgid "Source Network Interface" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:414 -msgid "Specify a local IP address to be excluded from routing" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:691 -msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:645 -msgid "Specify remote URLs to download and use domain lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:668 -msgid "Specify remote URLs to download and use subnet lists" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:599 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:622 -msgid "Specify the path to the list file located on the router filesystem" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:73 -msgid "Start podkop" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:63 -msgid "Stop podkop" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:29 -msgid "Subscription" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:112 -msgid "Subscription Update Interval" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:89 -msgid "Subscription URL" -msgstr "" - -#: src\helpers\copyToClipboard.ts:10 -msgid "Successfully copied!" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:304 -msgid "System info" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderSystemInfo.ts:21 -msgid "System information" -msgstr "" - -#: src\podkop\tabs\diagnostic\checks\runNftCheck.ts:69 -msgid "Table exist" -msgstr "" - -#: src\podkop\tabs\dashboard\partials\renderSections.ts:108 -msgid "Test latency" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:444 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:524 -msgid "Text List" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:46 -msgid "The DNS server used to look up the IP address of an upstream DNS server" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:184 -msgid "The interval between connectivity tests" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:198 -msgid "The maximum difference in response times (ms) allowed when comparing servers" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:222 -msgid "The URL used to test server connectivity" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:69 -msgid "Time in seconds for DNS record caching (default: 60)" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:238 -msgid "Traffic" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:268 -msgid "Traffic Total" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:25 -msgid "Troubleshooting" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:80 -msgid "TTL must be a positive number" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:75 -msgid "TTL value cannot be empty" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:321 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:17 -msgid "UDP (Unprotected DNS)" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:250 -msgid "UDP over TCP" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:39 -#: src\podkop\tabs\diagnostic\initController.ts:40 -#: src\podkop\tabs\diagnostic\initController.ts:41 -#: src\podkop\tabs\diagnostic\initController.ts:42 -#: src\podkop\tabs\diagnostic\initController.ts:43 -#: src\podkop\tabs\diagnostic\initController.ts:44 -#: src\podkop\tabs\diagnostic\helpers\getPodkopVersionRow.ts:7 -msgid "unknown" -msgstr "" - -#: src\podkop\api.ts:40 -msgid "Unknown error" -msgstr "" - -#: src\podkop\tabs\dashboard\initController.ts:240 -#: src\podkop\tabs\dashboard\initController.ts:271 -msgid "Uplink" -msgstr "" - -#: src\validators\validateProxyUrl.ts:37 -msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" -msgstr "" - -#: src\validators\validateUrl.ts:17 -msgid "URL must use one of the following protocols:" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:28 -msgid "URLTest" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:183 -msgid "URLTest Check Interval" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:160 -msgid "URLTest Proxy Links" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:221 -msgid "URLTest Testing URL" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:197 -msgid "URLTest Tolerance" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:439 -msgid "User Domain List Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:451 -msgid "User Domains" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:477 -msgid "User Domains List" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:519 -msgid "User Subnet List Type" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:531 -msgid "User Subnets" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:557 -msgid "User Subnets List" -msgstr "" - -#: src\validators\validateDns.ts:14 -#: src\validators\validateDns.ts:18 -#: src\validators\validateDomain.ts:13 -#: src\validators\validateDomain.ts:30 -#: src\validators\validateHysteriaUrl.ts:120 -#: src\validators\validateIp.ts:8 -#: src\validators\validateOutboundJson.ts:7 -#: src\validators\validatePath.ts:16 -#: src\validators\validateShadowsocksUrl.ts:95 -#: src\validators\validateSocksUrl.ts:80 -#: src\validators\validateSubnet.ts:38 -#: src\validators\validateTrojanUrl.ts:59 -#: src\validators\validateUrl.ts:28 -#: src\validators\validateVlessUrl.ts:108 -msgid "Valid" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:510 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:589 -msgid "Validation errors:" -msgstr "" - -#: src\podkop\tabs\diagnostic\initController.ts:258 -#: src\podkop\tabs\diagnostic\partials\renderAvailableActions.ts:111 -msgid "View logs" -msgstr "" - -#: src\podkop\tabs\diagnostic\partials\renderWikiDisclaimer.ts:31 -msgid "Visit Wiki" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:38 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:138 -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:161 -msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:387 -msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\section.js:406 -msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:256 -msgid "YACD Secret Key" -msgstr "" - -#: ..\luci-app-podkop\htdocs\luci-static\resources\view\podkop\settings.js:127 -msgid "You can select Output Network Interface, by default autodetect" -msgstr "" diff --git a/luci-app-podkop/root/usr/share/luci/menu.d/luci-app-podkop.json b/luci-app-podkop/root/usr/share/luci/menu.d/luci-app-podkop.json deleted file mode 100644 index e7e6ffaf..00000000 --- a/luci-app-podkop/root/usr/share/luci/menu.d/luci-app-podkop.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "admin/services/podkop": { - "title": "Podkop", - "order": 42, - "action": { - "type": "view", - "path": "podkop/podkop" - }, - "depends": { - "acl": [ "luci-app-podkop" ], - "uci": { "podkop": true } - } - } -} \ No newline at end of file diff --git a/netshift/Makefile b/netshift/Makefile new file mode 100644 index 00000000..593c9135 --- /dev/null +++ b/netshift/Makefile @@ -0,0 +1,64 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=netshift + +PKG_VERSION := $(if $(NETSHIFT_VERSION),$(NETSHIFT_VERSION),0.$(shell date +%d%m%Y)) + +PKG_RELEASE:=1 + +PKG_MAINTAINER:=ITDog +PKG_LICENSE:=GPL-2.0-or-later + +include $(INCLUDE_DIR)/package.mk + +define Package/netshift + SECTION:=net + CATEGORY:=Network + DEPENDS:=+sing-box +curl +jq +kmod-nft-tproxy +coreutils-base64 +bind-dig + CONFLICTS:=https-dns-proxy nextdns luci-app-passwall luci-app-passwall2 + TITLE:=Domain routing app + URL:=https://podkop.net + PKGARCH:=all +endef + +define Package/netshift/description + Domain routing. Use of VLESS, Shadowsocks technologies +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/netshift/prerm +#!/bin/sh + +grep -q "105 netshift" /etc/iproute2/rt_tables && sed -i "/105 netshift/d" /etc/iproute2/rt_tables + +/etc/init.d/netshift stop + +exit 0 +endef + +define Package/netshift/conffiles +/etc/config/netshift +endef + +define Package/netshift/install + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/netshift $(1)/etc/init.d/netshift + + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/netshift $(1)/etc/config/netshift + + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/usr/bin/netshift $(1)/usr/bin/netshift + + $(INSTALL_DIR) $(1)/usr/lib/netshift + $(CP) ./files/usr/lib/* $(1)/usr/lib/netshift/ + + sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' $(1)/usr/lib/netshift/constants.sh +endef + +$(eval $(call BuildPackage,netshift)) diff --git a/podkop/files/etc/config/podkop b/netshift/files/etc/config/netshift similarity index 100% rename from podkop/files/etc/config/podkop rename to netshift/files/etc/config/netshift diff --git a/podkop/files/etc/init.d/podkop b/netshift/files/etc/init.d/netshift similarity index 88% rename from podkop/files/etc/init.d/podkop rename to netshift/files/etc/init.d/netshift index 73825efe..e4e055ac 100755 --- a/podkop/files/etc/init.d/podkop +++ b/netshift/files/etc/init.d/netshift @@ -9,13 +9,13 @@ NAME="$(basename ${script:-$initscript})" config_load "$NAME" start_service() { - echo "Start podkop" + echo "Start netshift" config_get enable_badwan_interface_monitoring "settings" "enable_badwan_interface_monitoring" config_get badwan_monitored_interfaces "settings" "badwan_monitored_interfaces" procd_open_instance - procd_set_param command /usr/bin/podkop start + procd_set_param command /usr/bin/netshift start [ "$enable_badwan_interface_monitoring" = "1" ] && [ -n "$badwan_monitored_interfaces" ] && procd_set_param netdev "$badwan_monitored_interfaces" procd_set_param stdout 1 @@ -24,11 +24,11 @@ start_service() { } stop_service() { - /usr/bin/podkop stop + /usr/bin/netshift stop } reload_service() { - /usr/bin/podkop reload > /dev/null 2>&1 + /usr/bin/netshift reload > /dev/null 2>&1 } service_triggers() { @@ -45,7 +45,7 @@ service_triggers() { if [ "$enable_badwan_interface_monitoring" = "1" ]; then for iface in $badwan_monitored_interfaces; do - procd_add_interface_trigger "interface.*.up" "$iface" /etc/init.d/podkop reload + procd_add_interface_trigger "interface.*.up" "$iface" /etc/init.d/netshift reload done fi procd_close_trigger diff --git a/podkop/files/usr/bin/podkop b/netshift/files/usr/bin/netshift similarity index 95% rename from podkop/files/usr/bin/podkop rename to netshift/files/usr/bin/netshift index 736a516f..363249fd 100755 --- a/podkop/files/usr/bin/podkop +++ b/netshift/files/usr/bin/netshift @@ -9,31 +9,31 @@ check_required_file() { fi } -PODKOP_LIB="/usr/lib/podkop" +NETSHIFT_LIB="/usr/lib/netshift" check_required_file /lib/functions.sh check_required_file /lib/config/uci.sh check_required_file /lib/functions/network.sh -check_required_file "$PODKOP_LIB/constants.sh" -check_required_file "$PODKOP_LIB/nft.sh" -check_required_file "$PODKOP_LIB/helpers.sh" -check_required_file "$PODKOP_LIB/sing_box_config_manager.sh" -check_required_file "$PODKOP_LIB/sing_box_config_facade.sh" -check_required_file "$PODKOP_LIB/logging.sh" -check_required_file "$PODKOP_LIB/rulesets.sh" -check_required_file "$PODKOP_LIB/updater.sh" +check_required_file "$NETSHIFT_LIB/constants.sh" +check_required_file "$NETSHIFT_LIB/nft.sh" +check_required_file "$NETSHIFT_LIB/helpers.sh" +check_required_file "$NETSHIFT_LIB/sing_box_config_manager.sh" +check_required_file "$NETSHIFT_LIB/sing_box_config_facade.sh" +check_required_file "$NETSHIFT_LIB/logging.sh" +check_required_file "$NETSHIFT_LIB/rulesets.sh" +check_required_file "$NETSHIFT_LIB/updater.sh" . /lib/functions.sh . /lib/config/uci.sh . /lib/functions/network.sh -. "$PODKOP_LIB/constants.sh" -. "$PODKOP_LIB/nft.sh" -. "$PODKOP_LIB/helpers.sh" -. "$PODKOP_LIB/sing_box_config_manager.sh" -. "$PODKOP_LIB/sing_box_config_facade.sh" -. "$PODKOP_LIB/logging.sh" -. "$PODKOP_LIB/rulesets.sh" -. "$PODKOP_LIB/updater.sh" +. "$NETSHIFT_LIB/constants.sh" +. "$NETSHIFT_LIB/nft.sh" +. "$NETSHIFT_LIB/helpers.sh" +. "$NETSHIFT_LIB/sing_box_config_manager.sh" +. "$NETSHIFT_LIB/sing_box_config_facade.sh" +. "$NETSHIFT_LIB/logging.sh" +. "$NETSHIFT_LIB/rulesets.sh" +. "$NETSHIFT_LIB/updater.sh" -config_load "$PODKOP_CONFIG" +config_load "$NETSHIFT_CONFIG" check_requirements() { log "Check Requirements" @@ -171,9 +171,9 @@ ensure_subscription_cache_dir() { local state_dir_created=0 cache_dir_created=0 local mkdir_errfile mkdir_rc - [ -d "$PODKOP_STATE_DIR" ] || state_dir_created=1 + [ -d "$NETSHIFT_STATE_DIR" ] || state_dir_created=1 [ -d "$SUBSCRIPTION_CACHE_FOLDER" ] || cache_dir_created=1 - mkdir_errfile="/tmp/podkop-subscription-cache-mkdir.$$" + mkdir_errfile="/tmp/netshift-subscription-cache-mkdir.$$" if mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" 2>"$mkdir_errfile"; then rm -f "$mkdir_errfile" else @@ -191,7 +191,7 @@ ensure_subscription_cache_dir() { fi if [ "$state_dir_created" -eq 1 ] || [ "$cache_dir_created" -eq 1 ]; then - chmod 700 "$PODKOP_STATE_DIR" 2>/dev/null + chmod 700 "$NETSHIFT_STATE_DIR" 2>/dev/null chmod 700 "$SUBSCRIPTION_CACHE_FOLDER" 2>/dev/null fi } @@ -498,7 +498,7 @@ prepare_subscription_caches_for_startup() { } stop_subscription_startup_retry_worker() { - local pidfile="/var/run/podkop_subscription_retry.pid" + local pidfile="/var/run/netshift_subscription_retry.pid" if [ -f "$pidfile" ]; then pid="$(cat "$pidfile" 2> /dev/null)" @@ -511,7 +511,7 @@ stop_subscription_startup_retry_worker() { } start_subscription_startup_retry_worker() { - local pidfile="/var/run/podkop_subscription_retry.pid" + local pidfile="/var/run/netshift_subscription_retry.pid" if [ -f "$pidfile" ]; then pid="$(cat "$pidfile" 2> /dev/null)" @@ -530,13 +530,13 @@ start_subscription_startup_retry_worker() { sleep 10 while true; do - config_load "$PODKOP_CONFIG" + config_load "$NETSHIFT_CONFIG" # Run in a child process: a successful subscription_update performs # its own restart, which stops this worker via the pidfile safely. # Keep diagnostics visible in syslog; subscription URLs are redacted # in the lower-level download helpers. - if /usr/bin/podkop subscription_update; then + if /usr/bin/netshift subscription_update; then log "Deferred subscription refresh succeeded; updated configuration is being applied" "info" rm -f "$pidfile" exit 0 @@ -552,7 +552,7 @@ start_subscription_startup_retry_worker() { } start_main() { - log "Starting podkop" + log "Starting netshift" check_requirements @@ -575,7 +575,7 @@ start_main() { prepare_subscription_caches_for_startup if [ "$subscription_startup_blocked" -ne 0 ]; then - log "Podkop startup continues with temporary blocked subscription outbounds until sources become reachable" "warn" + log "NetShift startup continues with temporary blocked subscription outbounds until sources become reachable" "warn" fi stop_subscription_startup_retry_worker @@ -596,21 +596,21 @@ start_main() { log "Nice" list_update & - echo $! > /var/run/podkop_list_update.pid + echo $! > /var/run/netshift_list_update.pid } stop_main() { - log "Stopping the podkop" + log "Stopping the netshift" stop_subscription_startup_retry_worker - if [ -f /var/run/podkop_list_update.pid ]; then - pid=$(cat /var/run/podkop_list_update.pid) + if [ -f /var/run/netshift_list_update.pid ]; then + pid=$(cat /var/run/netshift_list_update.pid) if kill -0 "$pid" 2> /dev/null; then kill "$pid" 2> /dev/null log "Stopped list_update" fi - rm -f /var/run/podkop_list_update.pid + rm -f /var/run/netshift_list_update.pid fi remove_cron_job @@ -623,7 +623,7 @@ stop_main() { fi log "Flush ip rule" - if ip rule list | grep -q "podkop"; then + if ip rule list | grep -q "netshift"; then ip rule del fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 fi @@ -653,8 +653,8 @@ start() { dnsmasq_configure fi - uci_set "podkop" "settings" "shutdown_correctly" 0 - uci commit "podkop" && config_load "$PODKOP_CONFIG" + uci_set "netshift" "settings" "shutdown_correctly" 0 + uci commit "netshift" && config_load "$NETSHIFT_CONFIG" } stop() { @@ -666,18 +666,18 @@ stop() { stop_main - uci_set "podkop" "settings" "shutdown_correctly" 1 - uci commit "podkop" && config_load "$PODKOP_CONFIG" + uci_set "netshift" "settings" "shutdown_correctly" 1 + uci commit "netshift" && config_load "$NETSHIFT_CONFIG" } reload() { - log "Podkop reload" + log "NetShift reload" stop start } restart() { - log "Podkop restart" + log "NetShift restart" stop start } @@ -820,7 +820,7 @@ dnsmasq_configure() { local shutdown_correctly config_get shutdown_correctly "settings" "shutdown_correctly" if [ "$shutdown_correctly" -eq 0 ]; then - log "Previous shutdown of podkop was not correct, reconfiguration of dnsmasq is not required" + log "Previous shutdown of netshift was not correct, reconfiguration of dnsmasq is not required" return 0 fi @@ -829,14 +829,14 @@ dnsmasq_configure() { if [ -n "$current_servers" ]; then for server in $(uci_get "dhcp" "@dnsmasq[0]" "server"); do if ! [ "$server" == "$SB_DNS_INBOUND_ADDRESS" ]; then - uci_add_list "dhcp" "@dnsmasq[0]" "podkop_server" "$server" + uci_add_list "dhcp" "@dnsmasq[0]" "netshift_server" "$server" fi done uci_remove "dhcp" "@dnsmasq[0]" "server" fi - backup_dnsmasq_config_option "noresolv" "podkop_noresolv" - backup_dnsmasq_config_option "cachesize" "podkop_cachesize" + backup_dnsmasq_config_option "noresolv" "netshift_noresolv" + backup_dnsmasq_config_option "cachesize" "netshift_cachesize" log "Configure dnsmasq for sing-box" uci_add_list "dhcp" "@dnsmasq[0]" "server" "$SB_DNS_INBOUND_ADDRESS" @@ -852,39 +852,39 @@ dnsmasq_restore() { local shutdown_correctly config_get shutdown_correctly "settings" "shutdown_correctly" if [ "$shutdown_correctly" -eq 1 ]; then - log "Previous shutdown of podkop was correct, reconfiguration of dnsmasq is not required" + log "Previous shutdown of netshift was correct, reconfiguration of dnsmasq is not required" return 0 fi local cachesize noresolv backup_servers resolvfile log "Restoring cachesize" "debug" - cachesize="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_cachesize")" + cachesize="$(uci_get "dhcp" "@dnsmasq[0]" "netshift_cachesize")" if [ -z "$cachesize" ]; then uci_remove "dhcp" "@dnsmasq[0]" "cachesize" uci_set "dhcp" "@dnsmasq[0]" "cachesize" 150 else uci_set "dhcp" "@dnsmasq[0]" "cachesize" "$cachesize" - uci_remove "dhcp" "@dnsmasq[0]" "podkop_cachesize" + uci_remove "dhcp" "@dnsmasq[0]" "netshift_cachesize" fi log "Restoring noresolv" "debug" - noresolv="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_noresolv")" + noresolv="$(uci_get "dhcp" "@dnsmasq[0]" "netshift_noresolv")" if [ -z "$noresolv" ]; then uci_set "dhcp" "@dnsmasq[0]" "noresolv" 0 else uci_set "dhcp" "@dnsmasq[0]" "noresolv" "$noresolv" - uci_remove "dhcp" "@dnsmasq[0]" "podkop_noresolv" + uci_remove "dhcp" "@dnsmasq[0]" "netshift_noresolv" fi log "Restoring DNS servers" "debug" uci_remove "dhcp" "@dnsmasq[0]" "server" resolvfile="/tmp/resolv.conf.d/resolv.conf.auto" - backup_servers="$(uci_get "dhcp" "@dnsmasq[0]" "podkop_server")" + backup_servers="$(uci_get "dhcp" "@dnsmasq[0]" "netshift_server")" if [ -n "$backup_servers" ]; then for server in $backup_servers; do uci_add_list "dhcp" "@dnsmasq[0]" "server" "$server" done - uci_remove "dhcp" "@dnsmasq[0]" "podkop_server" + uci_remove "dhcp" "@dnsmasq[0]" "netshift_server" elif file_exists "$resolvfile"; then log "Backup DNS servers not found, using default resolvfile" "debug" uci_set "dhcp" "@dnsmasq[0]" "resolvfile" "$resolvfile" @@ -911,19 +911,19 @@ add_cron_job() { case "$update_interval" in "1h") - cron_job="13 * * * * /usr/bin/podkop list_update" + cron_job="13 * * * * /usr/bin/netshift list_update" ;; "3h") - cron_job="13 */3 * * * /usr/bin/podkop list_update" + cron_job="13 */3 * * * /usr/bin/netshift list_update" ;; "12h") - cron_job="13 */12 * * * /usr/bin/podkop list_update" + cron_job="13 */12 * * * /usr/bin/netshift list_update" ;; "1d") - cron_job="13 9 * * * /usr/bin/podkop list_update" + cron_job="13 9 * * * /usr/bin/netshift list_update" ;; "3d") - cron_job="13 9 */3 * * /usr/bin/podkop list_update" + cron_job="13 9 */3 * * /usr/bin/netshift list_update" ;; *) log "Invalid update_interval value: $update_interval" @@ -944,7 +944,7 @@ add_cron_job() { } remove_cron_job() { - (crontab -l | grep -v "/usr/bin/podkop list_update" | grep -v "/usr/bin/podkop subscription_update") | crontab - + (crontab -l | grep -v "/usr/bin/netshift list_update" | grep -v "/usr/bin/netshift subscription_update") | crontab - log "The cron job removed" } @@ -966,22 +966,22 @@ add_subscription_cron_job() { case "$subscription_update_interval" in "30m") - cron_job="*/30 * * * * /usr/bin/podkop subscription_update" + cron_job="*/30 * * * * /usr/bin/netshift subscription_update" ;; "1h") - cron_job="17 * * * * /usr/bin/podkop subscription_update" + cron_job="17 * * * * /usr/bin/netshift subscription_update" ;; "3h") - cron_job="17 */3 * * * /usr/bin/podkop subscription_update" + cron_job="17 */3 * * * /usr/bin/netshift subscription_update" ;; "6h") - cron_job="17 */6 * * * /usr/bin/podkop subscription_update" + cron_job="17 */6 * * * /usr/bin/netshift subscription_update" ;; "12h") - cron_job="17 */12 * * * /usr/bin/podkop subscription_update" + cron_job="17 */12 * * * /usr/bin/netshift subscription_update" ;; "1d") - cron_job="17 9 * * * /usr/bin/podkop subscription_update" + cron_job="17 9 * * * /usr/bin/netshift subscription_update" ;; *) log "Invalid subscription_update_interval value: $subscription_update_interval" @@ -990,7 +990,7 @@ add_subscription_cron_job() { esac # Avoid duplicate subscription cron - (crontab -l | grep -v "/usr/bin/podkop subscription_update") | { + (crontab -l | grep -v "/usr/bin/netshift subscription_update") | { cat echo "$cron_job" } | crontab - @@ -1225,11 +1225,11 @@ subscription_update() { return 0 fi - echolog "рџ”„ Restarting podkop to apply updated subscriptions..." + echolog "рџ”„ Restarting netshift to apply updated subscriptions..." restart restart_rc=$? if [ "$restart_rc" -ne 0 ]; then - echolog "вќЊ Subscription was downloaded, but podkop restart failed" + echolog "вќЊ Subscription was downloaded, but netshift restart failed" return "$restart_rc" fi @@ -2520,13 +2520,13 @@ get_first_outbound_section() { get_sections_by_connection_type() { local connection_type="$1" - uci show podkop | grep "\.connection_type='$connection_type'" | cut -d'.' -f2 + uci show netshift | grep "\.connection_type='$connection_type'" | cut -d'.' -f2 } section_by_connection_type_exists() { local connection_type="$1" - if uci show podkop | grep -q "\.connection_type='$connection_type'"; then + if uci show netshift | grep -q "\.connection_type='$connection_type'"; then return 0 else return 1 @@ -2708,7 +2708,7 @@ check_nft() { if [ "$found_hetzner" -eq 1 ] || [ "$found_ovh" -eq 1 ]; then - local sets="podkop_subnets podkop_domains interfaces podkop_discord_subnets localv4" + local sets="netshift_subnets netshift_domains interfaces netshift_discord_subnets localv4" nolog "Sets statistics:" for set_name in $sets; do @@ -2747,21 +2747,21 @@ check_logs() { fi local logs - logs=$(logread | grep -E "podkop|sing-box") + logs=$(logread | grep -E "netshift|sing-box") if [ -z "$logs" ]; then nolog "Logs not found" return 1 fi - # Find the last occurrence of "Starting podkop" + # Find the last occurrence of "Starting netshift" local start_line - start_line=$(echo "$logs" | grep -n "podkop.*Starting podkop" | tail -n 1 | cut -d: -f1) + start_line=$(echo "$logs" | grep -n "netshift.*Starting netshift" | tail -n 1 | cut -d: -f1) if [ -n "$start_line" ]; then echo "$logs" | tail -n +"$start_line" else - nolog "No 'Starting podkop' message found, showing last 100 lines" + nolog "No 'Starting netshift' message found, showing last 100 lines" echo "$logs" | tail -n 100 fi } @@ -2803,7 +2803,7 @@ show_sing_box_config() { } show_config() { - if [ ! -f "$PODKOP_CONFIG" ]; then + if [ ! -f "$NETSHIFT_CONFIG" ]; then nolog "Configuration file not found" return 1 fi @@ -2817,14 +2817,14 @@ show_config() { -e "s@\\(option dns_server '[^/]*\\)/[^']*'@\\1/MASKED'@g" \ -e "s@\\(option domain_resolver_dns_server '[^/]*\\)/[^']*'@\\1/MASKED'@g" \ -e 's/\(option yacd_secret_key\).*/\1 '\''MASKED'\''/g' \ - "$PODKOP_CONFIG" > "$tmp_config" + "$NETSHIFT_CONFIG" > "$tmp_config" cat "$tmp_config" rm -f "$tmp_config" } show_version() { - echo "$PODKOP_VERSION" + echo "$NETSHIFT_VERSION" } show_sing_box_version() { @@ -2842,15 +2842,15 @@ show_system_info() { } get_system_info() { - local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model sing_box_extended + local netshift_version netshift_latest_version luci_app_version sing_box_version openwrt_version device_model sing_box_extended - podkop_version="$PODKOP_VERSION" + netshift_version="$NETSHIFT_VERSION" - podkop_latest_version=$(curl -m 3 -s https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest | grep '"tag_name":' | cut -d'"' -f4) - [ -z "$podkop_latest_version" ] && podkop_latest_version="unknown" + netshift_latest_version=$(curl -m 3 -s https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest | grep '"tag_name":' | cut -d'"' -f4) + [ -z "$netshift_latest_version" ] && netshift_latest_version="unknown" - if [ -f /www/luci-static/resources/view/podkop/main.js ]; then - luci_app_version=$(grep 'var PODKOP_LUCI_APP_VERSION' /www/luci-static/resources/view/podkop/main.js | cut -d'"' -f2) + if [ -f /www/luci-static/resources/view/netshift/main.js ]; then + luci_app_version=$(grep 'var NETSHIFT_LUCI_APP_VERSION' /www/luci-static/resources/view/netshift/main.js | cut -d'"' -f2) else luci_app_version="not installed" fi @@ -2881,7 +2881,7 @@ get_system_info() { device_model="unknown" fi - echo "{\"podkop_version\": \"$podkop_version\", \"podkop_latest_version\": \"$podkop_latest_version\", \"luci_app_version\": \"$luci_app_version\", \"sing_box_version\": \"$sing_box_version\", \"sing_box_extended\": $sing_box_extended, \"openwrt_version\": \"$openwrt_version\", \"device_model\": \"$device_model\"}" | jq . + echo "{\"netshift_version\": \"$netshift_version\", \"netshift_latest_version\": \"$netshift_latest_version\", \"luci_app_version\": \"$luci_app_version\", \"sing_box_version\": \"$sing_box_version\", \"sing_box_extended\": $sing_box_extended, \"openwrt_version\": \"$openwrt_version\", \"device_model\": \"$device_model\"}" | jq . } get_sing_box_status() { @@ -2932,7 +2932,7 @@ get_status() { local status="" # Check if service is enabled - if [ -x /etc/rc.d/S99podkop ]; then + if [ -x /etc/rc.d/S99netshift ]; then enabled=1 status="enabled" else @@ -3002,13 +3002,13 @@ check_dns_available() { # Check if /etc/config/dhcp has server 127.0.0.42 config_load dhcp - config_foreach check_dhcp_has_podkop_dns dnsmasq - config_load "$PODKOP_CONFIG" + config_foreach check_dhcp_has_netshift_dns dnsmasq + config_load "$NETSHIFT_CONFIG" echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$display_dns_server\",\"dns_status\":$dns_status,\"dns_on_router\":$dns_on_router,\"bootstrap_dns_server\":\"$bootstrap_dns_server\",\"bootstrap_dns_status\":$bootstrap_dns_status,\"dhcp_config_status\":$dhcp_config_status}" | jq . } -check_dhcp_has_podkop_dns() { +check_dhcp_has_netshift_dns() { local server_list cachesize noresolv server_found config_get server_list "$1" "server" config_get cachesize "$1" "cachesize" @@ -3040,7 +3040,7 @@ check_nft_rules() { local rules_proxy_counters=0 local rules_other_mark_exist=0 - # Generate traffic through PodkopTable + # Generate traffic through NetShiftTable curl -m 3 -s "https://$CHECK_PROXY_IP_DOMAIN/check" > /dev/null 2>&1 & local pid1=$! curl -m 3 -s "https://$FAKEIP_TEST_DOMAIN/check" > /dev/null 2>&1 & @@ -3050,7 +3050,7 @@ check_nft_rules() { wait $pid2 2> /dev/null sleep 1 - # Check if PodkopTable exists + # Check if NetShiftTable exists if nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then table_exist=1 @@ -3094,21 +3094,21 @@ check_nft_rules() { fi fi - # Check for other mark rules outside PodkopTable + # Check for other mark rules outside NetShiftTable nft list tables 2> /dev/null | while read -r _ family table_name; do [ -z "$table_name" ] && continue [ "$table_name" = "$NFT_TABLE_NAME" ] && continue if nft list table "$family" "$table_name" 2> /dev/null | grep -q "meta mark set"; then - touch /tmp/podkop_mark_check.$$ + touch /tmp/netshift_mark_check.$$ break fi done - if [ -f /tmp/podkop_mark_check.$$ ]; then + if [ -f /tmp/netshift_mark_check.$$ ]; then rules_other_mark_exist=1 - rm -f /tmp/podkop_mark_check.$$ + rm -f /tmp/netshift_mark_check.$$ fi echo "{\"table_exist\":$table_exist,\"rules_mangle_exist\":$rules_mangle_exist,\"rules_mangle_counters\":$rules_mangle_counters,\"rules_mangle_output_exist\":$rules_mangle_output_exist,\"rules_mangle_output_counters\":$rules_mangle_output_counters,\"rules_proxy_exist\":$rules_proxy_exist,\"rules_proxy_counters\":$rules_proxy_counters,\"rules_other_mark_exist\":$rules_other_mark_exist}" | jq . @@ -3329,8 +3329,8 @@ print_global() { } global_check() { - local PODKOP_LUCI_VERSION="Unknown" - [ -n "$1" ] && PODKOP_LUCI_VERSION="$1" + local NETSHIFT_LUCI_VERSION="Unknown" + [ -n "$1" ] && NETSHIFT_LUCI_VERSION="$1" print_global "рџ“Ў Global check run!" print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" @@ -3340,16 +3340,16 @@ global_check() { system_info_json=$(get_system_info) if [ -n "$system_info_json" ]; then - local podkop_version podkop_latest_version luci_app_version sing_box_version openwrt_version device_model sing_box_extended + local netshift_version netshift_latest_version luci_app_version sing_box_version openwrt_version device_model sing_box_extended - podkop_version=$(echo "$system_info_json" | jq -r '.podkop_version // "unknown"') - podkop_latest_version=$(echo "$system_info_json" | jq -r '.podkop_latest_version // "unknown"') + netshift_version=$(echo "$system_info_json" | jq -r '.netshift_version // "unknown"') + netshift_latest_version=$(echo "$system_info_json" | jq -r '.netshift_latest_version // "unknown"') luci_app_version=$(echo "$system_info_json" | jq -r '.luci_app_version // "unknown"') sing_box_version=$(echo "$system_info_json" | jq -r '.sing_box_version // "unknown"') openwrt_version=$(echo "$system_info_json" | jq -r '.openwrt_version // "unknown"') device_model=$(echo "$system_info_json" | jq -r '.device_model // "unknown"') - print_global "рџ•іпёЏ Podkop: $podkop_version (latest: $podkop_latest_version)" + print_global "рџ•іпёЏ NetShift: $netshift_version (latest: $netshift_latest_version)" print_global "рџ•іпёЏ LuCI App: $luci_app_version" print_global "📦 Sing-box: $sing_box_version" print_global "рџ›њ OpenWrt: $openwrt_version" @@ -3541,7 +3541,7 @@ global_check() { fi print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "рџ“„ Podkop config" + print_global "рџ“„ NetShift config" show_config # print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" @@ -3651,27 +3651,27 @@ show_help() { Usage: $0 COMMAND Available commands: - start Start podkop service - stop Stop podkop service - reload Reload podkop configuration - restart Restart podkop service - main Run main podkop process + start Start NetShift service + stop Stop NetShift service + reload Reload NetShift configuration + restart Restart NetShift service + main Run main NetShift process list_update Update domain lists subscription_update Update subscription proxies check_proxy Check proxy connectivity check_nft Check NFT rules check_nft_rules Check NFT rules status check_sing_box Check sing-box installation and status - check_logs Show podkop logs from system journal + check_logs Show NetShift logs from system journal check_sing_box_logs Show sing-box logs check_fakeip Test FakeIP on router clash_api Clash API interface for managing proxies and groups - show_config Display current podkop configuration - show_version Show podkop version + show_config Display current NetShift configuration + show_version Show NetShift version show_sing_box_config Show sing-box configuration show_sing_box_version Show sing-box version show_system_info Show system information - get_status Get podkop service status + get_status Get NetShift service status get_sing_box_status Get sing-box service status get_system_info Get system information in JSON format check_dns_available Check DNS server availability @@ -3762,8 +3762,8 @@ component_action) component_action "$2" "$3" ;; component_action_async) - "$0" component_action "$2" "$3" > "/tmp/podkop-component-$$.json" 2>&1 & - echo "{\"success\":true,\"job\":\"/tmp/podkop-component-$$.json\"}" + "$0" component_action "$2" "$3" > "/tmp/netshift-component-$$.json" 2>&1 & + echo "{\"success\":true,\"job\":\"/tmp/netshift-component-$$.json\"}" ;; *) show_help diff --git a/podkop/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh similarity index 89% rename from podkop/files/usr/lib/constants.sh rename to netshift/files/usr/lib/constants.sh index db40974e..fdc193f9 100644 --- a/podkop/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -1,9 +1,9 @@ # shellcheck disable=SC2034 -PODKOP_VERSION="__COMPILED_VERSION_VARIABLE__" +NETSHIFT_VERSION="__COMPILED_VERSION_VARIABLE__" ## Common -PODKOP_CONFIG="/etc/config/podkop" -PODKOP_STATE_DIR="/etc/podkop" +NETSHIFT_CONFIG="/etc/config/netshift" +NETSHIFT_STATE_DIR="/etc/netshift" RESOLV_CONF="/etc/resolv.conf" DNS_RESOLVERS="1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 9.9.9.9 9.9.9.11 94.140.14.14 94.140.15.15 208.67.220.220 208.67.222.222 77.88.8.1 77.88.8.8" CHECK_PROXY_IP_DOMAIN="ip.podkop.fyi" @@ -11,18 +11,18 @@ FAKEIP_TEST_DOMAIN="fakeip.podkop.fyi" TMP_SING_BOX_FOLDER="/tmp/sing-box" TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets" TMP_SUBSCRIPTION_FOLDER="$TMP_SING_BOX_FOLDER/subscriptions" -SUBSCRIPTION_CACHE_FOLDER="$PODKOP_STATE_DIR/subscriptions" +SUBSCRIPTION_CACHE_FOLDER="$NETSHIFT_STATE_DIR/subscriptions" TMP_SUBSCRIPTION_DOWNLOAD_FOLDER="$TMP_SING_BOX_FOLDER/subscription-downloads" CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker JQ_REQUIRED_VERSION="1.7.1" COREUTILS_BASE64_REQUIRED_VERSION="9.7" -RT_TABLE_NAME="podkop" +RT_TABLE_NAME="netshift" ## nft -NFT_TABLE_NAME="PodkopTable" +NFT_TABLE_NAME="NetShiftTable" NFT_LOCALV4_SET_NAME="localv4" -NFT_COMMON_SET_NAME="podkop_subnets" -NFT_DISCORD_SET_NAME="podkop_discord_subnets" +NFT_COMMON_SET_NAME="netshift_subnets" +NFT_DISCORD_SET_NAME="netshift_discord_subnets" NFT_INTERFACE_SET_NAME="interfaces" NFT_FAKEIP_MARK="0x00100000" NFT_OUTBOUND_MARK="0x00200000" diff --git a/podkop/files/usr/lib/helpers.jq b/netshift/files/usr/lib/helpers.jq similarity index 100% rename from podkop/files/usr/lib/helpers.jq rename to netshift/files/usr/lib/helpers.jq diff --git a/podkop/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh similarity index 99% rename from podkop/files/usr/lib/helpers.sh rename to netshift/files/usr/lib/helpers.sh index 7d1e8660..31d13dd7 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -449,7 +449,7 @@ log_wget_failure() { log "$operation failed [$attempt/$retries]: wget rc=$rc, mode=$mode, family=$family, timeout=${timeout}s, host=${host:-unknown}, url=$(redact_url_for_log "$url"), error_class=$err_class, error=\"$err\"" "warn" if echo "$err" | grep -qi 'Operation not permitted'; then - log "$operation got 'Operation not permitted'. On OpenWrt this can indicate firewall, routing, or IPv6 preference issues; podkop will retry with IPv4 when supported." "warn" + log "$operation got 'Operation not permitted'. On OpenWrt this can indicate firewall, routing, or IPv6 preference issues; netshift will retry with IPv4 when supported." "warn" fi } @@ -814,7 +814,7 @@ check_subscription_connectivity() { hwid="$(generate_hwid)" local attempt errfile rc family - errfile="/tmp/podkop-subscription-check.$$" + errfile="/tmp/netshift-subscription-check.$$" rm -f "$errfile" for attempt in $(seq 1 "$retries"); do family="any" diff --git a/podkop/files/usr/lib/logging.sh b/netshift/files/usr/lib/logging.sh similarity index 92% rename from podkop/files/usr/lib/logging.sh rename to netshift/files/usr/lib/logging.sh index 3a529ad5..247a02f3 100644 --- a/podkop/files/usr/lib/logging.sh +++ b/netshift/files/usr/lib/logging.sh @@ -10,7 +10,7 @@ log() { level="info" fi - logger -t "podkop" "[$level] $message" + logger -t "netshift" "[$level] $message" } nolog() { diff --git a/podkop/files/usr/lib/nft.sh b/netshift/files/usr/lib/nft.sh similarity index 100% rename from podkop/files/usr/lib/nft.sh rename to netshift/files/usr/lib/nft.sh diff --git a/podkop/files/usr/lib/rulesets.sh b/netshift/files/usr/lib/rulesets.sh similarity index 100% rename from podkop/files/usr/lib/rulesets.sh rename to netshift/files/usr/lib/rulesets.sh diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/netshift/files/usr/lib/sing_box_config_facade.sh similarity index 99% rename from podkop/files/usr/lib/sing_box_config_facade.sh rename to netshift/files/usr/lib/sing_box_config_facade.sh index a0884a7c..5d58aab5 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/netshift/files/usr/lib/sing_box_config_facade.sh @@ -1,6 +1,6 @@ -PODKOP_LIB="/usr/lib/podkop" -. "$PODKOP_LIB/helpers.sh" -. "$PODKOP_LIB/sing_box_config_manager.sh" +NETSHIFT_LIB="/usr/lib/netshift" +. "$NETSHIFT_LIB/helpers.sh" +. "$NETSHIFT_LIB/sing_box_config_manager.sh" sing_box_cf_add_dns_server() { local config="$1" diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/netshift/files/usr/lib/sing_box_config_manager.sh similarity index 99% rename from podkop/files/usr/lib/sing_box_config_manager.sh rename to netshift/files/usr/lib/sing_box_config_manager.sh index 635b4312..eefb7e1f 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/netshift/files/usr/lib/sing_box_config_manager.sh @@ -11,7 +11,7 @@ # # Usage: # Include this script in your ash script with: -# . /usr/lib/podkop/sing_box_config_manager.sh +# . /usr/lib/netshift/sing_box_config_manager.sh # # After that, sing_box_cm_* functions are available for generating # and modifying sing-box JSON configuration. @@ -287,7 +287,7 @@ sing_box_cm_patch_dns_route_rule() { --arg tag "$tag" \ --arg key "$key" \ --argjson value "$value" \ - 'import "helpers" as h {"search": "/usr/lib/podkop"}; + 'import "helpers" as h {"search": "/usr/lib/netshift"}; .dns.rules |= map( if .[$service_tag] == $tag then if has($key) then @@ -1222,7 +1222,7 @@ sing_box_cm_patch_route_rule() { --arg tag "$tag" \ --arg key "$key" \ --argjson value "$value" \ - 'import "helpers" as h {"search": "/usr/lib/podkop"}; + 'import "helpers" as h {"search": "/usr/lib/netshift"}; .route.rules |= map( if .[$service_tag] == $tag then if has($key) then @@ -1385,11 +1385,11 @@ sing_box_cm_add_inline_ruleset_rule() { value=$(_normalize_arg "$value") - echo "$config" | jq -L /usr/lib/podkop \ + echo "$config" | jq -L /usr/lib/netshift \ --arg tag "$tag" \ --arg key "$key" \ --argjson value "$value" \ - 'import "helpers" as h {"search": "/usr/lib/podkop"}; + 'import "helpers" as h {"search": "/usr/lib/netshift"}; .route.rule_set |= map( if .tag == $tag then if has($key) then diff --git a/podkop/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh similarity index 96% rename from podkop/files/usr/lib/updater.sh rename to netshift/files/usr/lib/updater.sh index fd8f6fbf..65717d04 100644 --- a/podkop/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -2,7 +2,7 @@ # Runtime updater for sing-box-extended and stock sing-box. # JSON parsing is done with jq (no ucode, no extra package deps). -# This file is sourced from /usr/bin/podkop, so log() is available. +# This file is sourced from /usr/bin/netshift, so log() is available. SB_EXT_ARCH_SUFFIX="" UPDATES_SING_BOX_EXTENDED_REPO="shtorm-7/sing-box-extended" @@ -145,11 +145,11 @@ updates_download_to_file() { return 1 } -# Restarts podkop if its init script is present (best-effort). -updates_restart_podkop() { - if [ -x /etc/init.d/podkop ]; then - updates_log "Restarting podkop after component change" - /etc/init.d/podkop restart >/dev/null 2>&1 || true +# Restarts netshift if its init script is present (best-effort). +updates_restart_netshift() { + if [ -x /etc/init.d/netshift ]; then + updates_log "Restarting netshift after component change" + /etc/init.d/netshift restart >/dev/null 2>&1 || true fi } @@ -188,7 +188,7 @@ updates_install_sing_box_extended() { return 1 fi - tmp_dir="$(mktemp -d /tmp/podkop-sbext.XXXXXX 2>/dev/null)" + tmp_dir="$(mktemp -d /tmp/netshift-sbext.XXXXXX 2>/dev/null)" if [ -z "$tmp_dir" ]; then updates_log "Failed to create temporary directory" "error" echo "{\"success\":false,\"message\":\"Failed to create temporary directory\"}" @@ -279,7 +279,7 @@ updates_install_sing_box_extended() { rm -f "$backup_binary" "$backup_cronet" rm -rf "$tmp_dir" - updates_restart_podkop + updates_restart_netshift updates_log "Installed sing-box-extended $new_version" echo "{\"success\":true,\"version\":\"$new_version\"}" return 0 @@ -310,7 +310,7 @@ updates_install_sing_box_stable() { return 1 fi - updates_restart_podkop + updates_restart_netshift new_version="$(get_sing_box_version)" updates_log "Stable sing-box installed: ${new_version:-unknown}" echo "{\"success\":true,\"version\":\"$new_version\"}" diff --git a/podkop/Makefile b/podkop/Makefile deleted file mode 100644 index 5cef0c1e..00000000 --- a/podkop/Makefile +++ /dev/null @@ -1,64 +0,0 @@ -include $(TOPDIR)/rules.mk - -PKG_NAME:=podkop - -PKG_VERSION := $(if $(PODKOP_VERSION),$(PODKOP_VERSION),0.$(shell date +%d%m%Y)) - -PKG_RELEASE:=1 - -PKG_MAINTAINER:=ITDog -PKG_LICENSE:=GPL-2.0-or-later - -include $(INCLUDE_DIR)/package.mk - -define Package/podkop - SECTION:=net - CATEGORY:=Network - DEPENDS:=+sing-box +curl +jq +kmod-nft-tproxy +coreutils-base64 +bind-dig - CONFLICTS:=https-dns-proxy nextdns luci-app-passwall luci-app-passwall2 - TITLE:=Domain routing app - URL:=https://podkop.net - PKGARCH:=all -endef - -define Package/podkop/description - Domain routing. Use of VLESS, Shadowsocks technologies -endef - -define Build/Configure -endef - -define Build/Compile -endef - -define Package/podkop/prerm -#!/bin/sh - -grep -q "105 podkop" /etc/iproute2/rt_tables && sed -i "/105 podkop/d" /etc/iproute2/rt_tables - -/etc/init.d/podkop stop - -exit 0 -endef - -define Package/podkop/conffiles -/etc/config/podkop -endef - -define Package/podkop/install - $(INSTALL_DIR) $(1)/etc/init.d - $(INSTALL_BIN) ./files/etc/init.d/podkop $(1)/etc/init.d/podkop - - $(INSTALL_DIR) $(1)/etc/config - $(INSTALL_CONF) ./files/etc/config/podkop $(1)/etc/config/podkop - - $(INSTALL_DIR) $(1)/usr/bin - $(INSTALL_BIN) ./files/usr/bin/podkop $(1)/usr/bin/podkop - - $(INSTALL_DIR) $(1)/usr/lib/podkop - $(CP) ./files/usr/lib/* $(1)/usr/lib/podkop/ - - sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' $(1)/usr/lib/podkop/constants.sh -endef - -$(eval $(call BuildPackage,podkop)) From 69c9a69acdbefc3032fbce0524f131c0182c42fb Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 17:37:17 +0300 Subject: [PATCH 21/75] docs: redesign README (centered header, badges, structured sections) Rework README in the style of well-presented project READMEs: - centered header with NETSHIFT ASCII banner + badges (release, license, OpenWrt, docs, DeepWiki) - one-line tagline + upstream attribution - checkbox Features list with sub-descriptions - collapsible
for pre-install requirements, migration (0.8.0 / 0.7.0), UCI example - Subscription headers as a table - Acknowledgments + License sections Content preserved: install one-liner, system requirements, subscription / HWID docs, extended/xhttp, migration notes, roadmap, upstream links. --- README.md | 231 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 146 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index db3976a9..4cb674a3 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,117 @@ +
+ +``` + ╔╗╔╔═╗╔╦╗╔═╗╦ ╦╦╔═╗╔╦╗ + ║║║║╣ ║ ╚═╗╠═╣║╠╣ ║ + ╝╚╝╚═╝ ╩ ╚═╝╩ ╩╩╚ ╩ + shift your traffic +``` + # NetShift -> **Форк с поддержкой Subscription URL + HWID и переключаемым ядром sing-box-extended (xhttp)** -> -> NetShift добавляет поддержку ссылок подписки (subscription URL) с кастомными заголовками (HWID, Device-OS, Device-Model) и автоматическим обновлением, а также переключение ядра на sing-box-extended с поддержкой клиентского транспорта xhttp. Основан на [itdoginfo/podkop](https://github.com/itdoginfo/podkop). +**Маршрутизация трафика для OpenWrt — нужное в туннель, остальное напрямую.** -Маршрутизация трафика для OpenWrt. +Открытое ПО на базе [sing-box](https://github.com/SagerNet/sing-box) · форк [itdoginfo/podkop](https://github.com/itdoginfo/podkop) с поддержкой Subscription URL, HWID и переключаемого ядра sing-box-extended. -Направляйте нужные ресурсы в туннель, а остальное — напрямую. Открытое программное обеспечение на базе [sing-box](https://github.com/SagerNet/sing-box). +[![Release](https://img.shields.io/github/v/release/yandexru45/podkop-evolution?style=flat-square)](https://github.com/yandexru45/podkop-evolution/releases) +[![License](https://img.shields.io/badge/license-GPL--2.0--or--later-blue?style=flat-square)](LICENSE) +[![OpenWrt](https://img.shields.io/badge/OpenWrt-24.10%2B-orange?style=flat-square)](https://openwrt.org/) +[![Docs](https://img.shields.io/badge/docs-podkop.net-informational?style=flat-square)](https://podkop.net/) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop) + +
> [!WARNING] > Проект находится в стадии бета-версии. Возможны ошибки, нестабильная работа и существенные изменения функциональности. --- -# Вещи, которые вам нужно знать перед установкой +## Возможности -### Обновления и конфигурация -- При обновлении **обязательно** [очищайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/). -- После обновления проверяйте конфигурацию — она может изменяться между версиями. -- При старте NetShift модифицируется конфигурация Dnsmasq. -- NetShift изменяет конфигурацию sing-box. Если вы используете собственную конфигурацию, заранее сохраните её. +- [x] **Маршрутизация по доменам и подсетям** — направляйте нужные ресурсы в туннель, остальное идёт напрямую
 VLESS · Shadowsocks · Trojan · Hysteria2 · готовые community-списки +- [x] **Subscription URL** — ссылки подписки от провайдера с автообновлением и автовыбором лучшего сервера
 кастомные заголовки HWID / Device-OS / Device-Model · URLTest · ручное переключение +- [x] **Переключаемое ядро sing-box** — стабильное ↔ sing-box-extended прямо из веб-интерфейса
 клиентский транспорт xhttp · установка/откат в один клик +- [x] **Веб-интерфейс LuCI** — дашборд, диагностика и настройки без правки конфигов
 статус серверов · проверка соединения · логи +- [x] **Автоматическая миграция** — обновление со старого podkop переносит конфиг без перенастройки -### Системные требования -- Требуется OpenWrt 24.10 или выше. -- Необходимо минимум 25 МБ свободного места на устройстве. Устройства с флеш-памятью 16 МБ не поддерживаются. +## Скриншоты -### Важные ограничения и особенности -- Если установлен Getdomains, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления) -- Dashboard доступен только при подключении по HTTP (из-за особенностей Clash API). При использовании HTTPS или домена работа может быть недоступна. - -### Поддержка и диагностика -- [Руководство по диагностике](https://podkop.net/docs/diagnostics/) -- Актуальные изменения публикуются в [Telegram-чате](https://t.me/itdogchat/81758/420321). Пожалуйста, ознакомьтесь с закрепленными сообщениями. -- При возникновении проблем оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате. +> Интерфейс доступен в LuCI: **Services → NetShift**. +> _(скриншоты будут добавлены)_ +## Установка -# Документация -https://podkop.net/ +Полная инструкция — в [документации](https://podkop.net/docs/install/). -# Установка NetShift -Полная информация в [документации](https://podkop.net/docs/install/) +Для установки и обновления достаточно одного скрипта: -Для установки и обновления достаточно выполнить один скрипт: -``` +```sh sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/install.sh) ``` -## Новое в NetShift: Подписки (Subscription) +> [!IMPORTANT] +> Перед установкой ознакомьтесь с разделом [**Перед установкой**](#перед-установкой) ниже — там системные требования и важные ограничения. + +## Перед установкой + +
+Системные требования + +- OpenWrt **24.10** или выше. +- Минимум **25 МБ** свободного места. Устройства с флеш-памятью 16 МБ не поддерживаются. + +
+ +
+Обновления и конфигурация + +- При обновлении **обязательно** [очищайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/). +- После обновления проверяйте конфигурацию — она может меняться между версиями. +- При старте NetShift модифицирует конфигурацию Dnsmasq. +- NetShift изменяет конфигурацию sing-box. Если используете собственную — заранее сохраните её. + +
+ +
+Ограничения и особенности + +- Если установлен **Getdomains**, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления). +- **Dashboard** работает только по HTTP (особенность Clash API). По HTTPS или через домен может быть недоступен. + +
+ +
+Поддержка и диагностика + +- [Руководство по диагностике](https://podkop.net/docs/diagnostics/) +- Актуальные изменения — в [Telegram-чате](https://t.me/itdogchat/81758/420321) (читайте закреплённые сообщения). +- При проблемах оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате. + +
-Добавлена поддержка subscription URL — ссылки подписки от провайдера прокси. При выборе типа конфигурации **Subscription** в LuCI: +## Subscription URL -- Введите URL подписки от вашего провайдера -- Выберите интервал автообновления (от 30 минут до 1 дня) -- Все серверы из подписки автоматически появятся в дашборде -- Автоматический выбор лучшего сервера по задержке (URLTest) -- Ручное переключение между серверами через дашборд +Поддержка ссылок подписки от провайдера прокси. При выборе типа конфигурации **Subscription** в LuCI: + +- Введите URL подписки от вашего провайдера. +- Выберите интервал автообновления (от 30 минут до 1 дня). +- Все серверы из подписки автоматически появятся в дашборде. +- Автовыбор лучшего сервера по задержке (URLTest) и ручное переключение. При скачивании подписки отправляются заголовки: -- `User-Agent: singbox/<версия>` -- `X-HWID` — уникальный идентификатор роутера -- `X-Device-OS: OpenWrt Linux` -- `X-Device-Model` — модель роутера -- `X-Ver-OS` — версия ядра -Пример конфигурации через UCI: -``` +| Заголовок | Значение | +|---|---| +| `User-Agent` | `singbox/<версия>` | +| `X-HWID` | уникальный идентификатор роутера | +| `X-Device-OS` | `OpenWrt Linux` | +| `X-Device-Model` | модель роутера | +| `X-Ver-OS` | версия ядра | + +
+Пример настройки через UCI + +```sh uci set netshift.my_sub=section uci set netshift.my_sub.connection_type='proxy' uci set netshift.my_sub.proxy_config_type='subscription' @@ -75,69 +122,83 @@ uci commit netshift ``` Ручное обновление подписки: -``` + +```sh /usr/bin/netshift subscription_update ``` -## Новое в NetShift: ядро sing-box-extended (xhttp) +
-NetShift позволяет переключать ядро между стабильным sing-box и сборкой -sing-box-extended прямо из вкладки **Diagnostics** в LuCI: +## Ядро sing-box-extended (xhttp) + +Переключение ядра между стабильным sing-box и сборкой **sing-box-extended** прямо из вкладки **Diagnostics** в LuCI: - **Install extended** — установить расширенное ядро sing-box-extended. -- **Install stable** — вернуться на стабильное ядро sing-box. +- **Install stable** — вернуться на стабильное ядро. -После установки расширенного ядра становится доступен клиентский транспорт -**xhttp**. Поддерживается только клиентский режим xhttp (не серверный). +После установки расширенного ядра становится доступен клиентский транспорт **xhttp**. Поддерживается только клиентский режим (не серверный). По умолчанию ставится стабильное ядро — extended включается по желанию. -## Изменения 0.8.0 — переименование в NetShift -Начиная с версии 0.8.0 проект переименован из `podkop` в **NetShift**. Пакет -теперь называется `netshift` (бинарь `/usr/bin/netshift`), а конфигурация -переехала на `/etc/config/netshift`. LuCI-приложение — `luci-app-netshift`. +## Миграция -При обновлении старый конфиг `/etc/config/podkop` автоматически мигрируется в -`/etc/config/netshift`, а резервная копия сохраняется в -`/etc/config/podkop.bak.pre-netshift`. +
+0.8.0 — переименование в NetShift -## Изменения 0.7.0 -Начиная с версии 0.7.0 изменена структура конфига `/etc/config/netshift` -(на тот момент — `/etc/config/podkop`). Старые значения несовместимы с новыми. -Нужно заново настроить NetShift. +С версии 0.8.0 проект переименован из `podkop` в **NetShift**: -Скрипт установки обнаружит старую версию и предупредит вас об этом. Если вы согласитесь, то он сделает автоматически написанное ниже. +- пакет — `netshift` (бинарь `/usr/bin/netshift`); +- конфигурация — `/etc/config/netshift`; +- LuCI-приложение — `luci-app-netshift`. -При обновлении вручную нужно: +При обновлении старый конфиг `/etc/config/podkop` **автоматически мигрируется** в `/etc/config/netshift`, резервная копия сохраняется в `/etc/config/podkop.bak.pre-netshift`. VPN продолжит работать без перенастройки. -0. Не ныть в issue и чатик. -1. Забэкапить старый конфиг: -``` +
+ +
+0.7.0 — несовместимый формат конфига + +С версии 0.7.0 изменена структура конфига (на тот момент — `/etc/config/podkop`). Старые значения несовместимы — нужно настроить заново. Скрипт установки обнаружит старую версию и предложит сделать это автоматически. + +Вручную: + +```sh +# 1. Забэкапить старый конфиг mv /etc/config/netshift /etc/config/netshift-070 -``` -2. Стянуть новый дефолтный конфиг: -``` +# 2. Стянуть новый дефолтный конфиг wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/netshift/files/etc/config/netshift +# 3. Настроить заново через LuCI или UCI ``` -3. Настроить заново ваш NetShift через Luci или UCI. -# ToDo +
+ +## Документация -> [!IMPORTANT] -> Pull Request принимаются только после согласования с авторами в Telegram-чате. На данный момент PR без предварительного обсуждения не рассматриваются. +Полная документация: **** -## Будущее -- [x] [Подписка](https://github.com/itdoginfo/podkop/issues/118) — **реализовано в NetShift!** +## Дорожная карта + +> [!IMPORTANT] +> Pull Request принимаются только после согласования с авторами в [Telegram-чате](https://t.me/itdogchat/81758/420321). PR без предварительного обсуждения не рассматриваются. + +- [x] [Подписка (Subscription URL)](https://github.com/itdoginfo/podkop/issues/118) — **реализовано** +- [x] Переключаемое ядро sing-box-extended + xhttp — **реализовано** - [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне. -- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. [Issue](https://github.com/itdoginfo/podkop/issues/111) -- [ ] Галочка, которая режет доступ к doh серверам. -- [ ] IPv6. Только после наполнения Wiki. +- [ ] Фоновый режим со слежением за состоянием sing-box и авто-restore dnsmasq при падении. [Issue](https://github.com/itdoginfo/podkop/issues/111) +- [ ] Опция, ограничивающая доступ к DoH-серверам. +- [ ] IPv6 (после наполнения Wiki). -## Тесты -- [ ] Unit тесты (BATS) -- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS) +**Тесты:** -> [!WARNING] -> Данное программное обеспечение предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий, включая гарантии коммерческой пригодности и соответствия определённой цели. -> -> Правообладатели и участники проекта не несут ответственности за любые прямые, косвенные, случайные, специальные или иные убытки, возникшие в результате использования программного обеспечения, включая потерю данных, прибыли или прерывание деятельности, даже если они были предупреждены о возможности таких последствий. +- [ ] Unit-тесты (BATS) +- [ ] Интеграционные тесты бэкенда (OpenWrt rootfs + BATS) -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop) +## Благодарности + +- [itdoginfo/podkop](https://github.com/itdoginfo/podkop) — исходный проект, форком которого является NetShift. +- [sing-box](https://github.com/SagerNet/sing-box) — движок маршрутизации. + +## Лицензия + +GPL-2.0-or-later — см. [LICENSE](LICENSE). + +> [!WARNING] +> Программное обеспечение предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий, включая гарантии коммерческой пригодности и соответствия определённой цели. Правообладатели и участники проекта не несут ответственности за любые убытки, возникшие в результате использования ПО. From 3b4554e61d6511a78be035b125c5a614150f417f Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 17:44:02 +0300 Subject: [PATCH 22/75] Revert "docs: redesign README (centered header, badges, structured sections)" This reverts commit 69c9a69acdbefc3032fbce0524f131c0182c42fb. --- README.md | 231 ++++++++++++++++++++---------------------------------- 1 file changed, 85 insertions(+), 146 deletions(-) diff --git a/README.md b/README.md index 4cb674a3..db3976a9 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,70 @@ -
- -``` - ╔╗╔╔═╗╔╦╗╔═╗╦ ╦╦╔═╗╔╦╗ - ║║║║╣ ║ ╚═╗╠═╣║╠╣ ║ - ╝╚╝╚═╝ ╩ ╚═╝╩ ╩╩╚ ╩ - shift your traffic -``` - # NetShift -**Маршрутизация трафика для OpenWrt — нужное в туннель, остальное напрямую.** +> **Форк с поддержкой Subscription URL + HWID и переключаемым ядром sing-box-extended (xhttp)** +> +> NetShift добавляет поддержку ссылок подписки (subscription URL) с кастомными заголовками (HWID, Device-OS, Device-Model) и автоматическим обновлением, а также переключение ядра на sing-box-extended с поддержкой клиентского транспорта xhttp. Основан на [itdoginfo/podkop](https://github.com/itdoginfo/podkop). -Открытое ПО на базе [sing-box](https://github.com/SagerNet/sing-box) · форк [itdoginfo/podkop](https://github.com/itdoginfo/podkop) с поддержкой Subscription URL, HWID и переключаемого ядра sing-box-extended. +Маршрутизация трафика для OpenWrt. -[![Release](https://img.shields.io/github/v/release/yandexru45/podkop-evolution?style=flat-square)](https://github.com/yandexru45/podkop-evolution/releases) -[![License](https://img.shields.io/badge/license-GPL--2.0--or--later-blue?style=flat-square)](LICENSE) -[![OpenWrt](https://img.shields.io/badge/OpenWrt-24.10%2B-orange?style=flat-square)](https://openwrt.org/) -[![Docs](https://img.shields.io/badge/docs-podkop.net-informational?style=flat-square)](https://podkop.net/) -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop) - -
+Направляйте нужные ресурсы в туннель, а остальное — напрямую. Открытое программное обеспечение на базе [sing-box](https://github.com/SagerNet/sing-box). > [!WARNING] > Проект находится в стадии бета-версии. Возможны ошибки, нестабильная работа и существенные изменения функциональности. --- -## Возможности - -- [x] **Маршрутизация по доменам и подсетям** — направляйте нужные ресурсы в туннель, остальное идёт напрямую
 VLESS · Shadowsocks · Trojan · Hysteria2 · готовые community-списки -- [x] **Subscription URL** — ссылки подписки от провайдера с автообновлением и автовыбором лучшего сервера
 кастомные заголовки HWID / Device-OS / Device-Model · URLTest · ручное переключение -- [x] **Переключаемое ядро sing-box** — стабильное ↔ sing-box-extended прямо из веб-интерфейса
 клиентский транспорт xhttp · установка/откат в один клик -- [x] **Веб-интерфейс LuCI** — дашборд, диагностика и настройки без правки конфигов
 статус серверов · проверка соединения · логи -- [x] **Автоматическая миграция** — обновление со старого podkop переносит конфиг без перенастройки - -## Скриншоты - -> Интерфейс доступен в LuCI: **Services → NetShift**. -> _(скриншоты будут добавлены)_ - -## Установка - -Полная инструкция — в [документации](https://podkop.net/docs/install/). - -Для установки и обновления достаточно одного скрипта: - -```sh -sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/install.sh) -``` - -> [!IMPORTANT] -> Перед установкой ознакомьтесь с разделом [**Перед установкой**](#перед-установкой) ниже — там системные требования и важные ограничения. - -## Перед установкой - -
-Системные требования - -- OpenWrt **24.10** или выше. -- Минимум **25 МБ** свободного места. Устройства с флеш-памятью 16 МБ не поддерживаются. - -
- -
-Обновления и конфигурация +# Вещи, которые вам нужно знать перед установкой +### Обновления и конфигурация - При обновлении **обязательно** [очищайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/). -- После обновления проверяйте конфигурацию — она может меняться между версиями. -- При старте NetShift модифицирует конфигурацию Dnsmasq. -- NetShift изменяет конфигурацию sing-box. Если используете собственную — заранее сохраните её. +- После обновления проверяйте конфигурацию — она может изменяться между версиями. +- При старте NetShift модифицируется конфигурация Dnsmasq. +- NetShift изменяет конфигурацию sing-box. Если вы используете собственную конфигурацию, заранее сохраните её. -
+### Системные требования +- Требуется OpenWrt 24.10 или выше. +- Необходимо минимум 25 МБ свободного места на устройстве. Устройства с флеш-памятью 16 МБ не поддерживаются. -
-Ограничения и особенности +### Важные ограничения и особенности +- Если установлен Getdomains, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления) +- Dashboard доступен только при подключении по HTTP (из-за особенностей Clash API). При использовании HTTPS или домена работа может быть недоступна. -- Если установлен **Getdomains**, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления). -- **Dashboard** работает только по HTTP (особенность Clash API). По HTTPS или через домен может быть недоступен. +### Поддержка и диагностика +- [Руководство по диагностике](https://podkop.net/docs/diagnostics/) +- Актуальные изменения публикуются в [Telegram-чате](https://t.me/itdogchat/81758/420321). Пожалуйста, ознакомьтесь с закрепленными сообщениями. +- При возникновении проблем оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате. -
-
-Поддержка и диагностика +# Документация +https://podkop.net/ -- [Руководство по диагностике](https://podkop.net/docs/diagnostics/) -- Актуальные изменения — в [Telegram-чате](https://t.me/itdogchat/81758/420321) (читайте закреплённые сообщения). -- При проблемах оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате. +# Установка NetShift +Полная информация в [документации](https://podkop.net/docs/install/) -
+Для установки и обновления достаточно выполнить один скрипт: +``` +sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/install.sh) +``` -## Subscription URL +## Новое в NetShift: Подписки (Subscription) -Поддержка ссылок подписки от провайдера прокси. При выборе типа конфигурации **Subscription** в LuCI: +Добавлена поддержка subscription URL — ссылки подписки от провайдера прокси. При выборе типа конфигурации **Subscription** в LuCI: -- Введите URL подписки от вашего провайдера. -- Выберите интервал автообновления (от 30 минут до 1 дня). -- Все серверы из подписки автоматически появятся в дашборде. -- Автовыбор лучшего сервера по задержке (URLTest) и ручное переключение. +- Введите URL подписки от вашего провайдера +- Выберите интервал автообновления (от 30 минут до 1 дня) +- Все серверы из подписки автоматически появятся в дашборде +- Автоматический выбор лучшего сервера по задержке (URLTest) +- Ручное переключение между серверами через дашборд При скачивании подписки отправляются заголовки: +- `User-Agent: singbox/<версия>` +- `X-HWID` — уникальный идентификатор роутера +- `X-Device-OS: OpenWrt Linux` +- `X-Device-Model` — модель роутера +- `X-Ver-OS` — версия ядра -| Заголовок | Значение | -|---|---| -| `User-Agent` | `singbox/<версия>` | -| `X-HWID` | уникальный идентификатор роутера | -| `X-Device-OS` | `OpenWrt Linux` | -| `X-Device-Model` | модель роутера | -| `X-Ver-OS` | версия ядра | - -
-Пример настройки через UCI - -```sh +Пример конфигурации через UCI: +``` uci set netshift.my_sub=section uci set netshift.my_sub.connection_type='proxy' uci set netshift.my_sub.proxy_config_type='subscription' @@ -122,83 +75,69 @@ uci commit netshift ``` Ручное обновление подписки: - -```sh +``` /usr/bin/netshift subscription_update ``` -
+## Новое в NetShift: ядро sing-box-extended (xhttp) -## Ядро sing-box-extended (xhttp) - -Переключение ядра между стабильным sing-box и сборкой **sing-box-extended** прямо из вкладки **Diagnostics** в LuCI: +NetShift позволяет переключать ядро между стабильным sing-box и сборкой +sing-box-extended прямо из вкладки **Diagnostics** в LuCI: - **Install extended** — установить расширенное ядро sing-box-extended. -- **Install stable** — вернуться на стабильное ядро. - -После установки расширенного ядра становится доступен клиентский транспорт **xhttp**. Поддерживается только клиентский режим (не серверный). По умолчанию ставится стабильное ядро — extended включается по желанию. - -## Миграция +- **Install stable** — вернуться на стабильное ядро sing-box. -
-0.8.0 — переименование в NetShift +После установки расширенного ядра становится доступен клиентский транспорт +**xhttp**. Поддерживается только клиентский режим xhttp (не серверный). -С версии 0.8.0 проект переименован из `podkop` в **NetShift**: +## Изменения 0.8.0 — переименование в NetShift +Начиная с версии 0.8.0 проект переименован из `podkop` в **NetShift**. Пакет +теперь называется `netshift` (бинарь `/usr/bin/netshift`), а конфигурация +переехала на `/etc/config/netshift`. LuCI-приложение — `luci-app-netshift`. -- пакет — `netshift` (бинарь `/usr/bin/netshift`); -- конфигурация — `/etc/config/netshift`; -- LuCI-приложение — `luci-app-netshift`. +При обновлении старый конфиг `/etc/config/podkop` автоматически мигрируется в +`/etc/config/netshift`, а резервная копия сохраняется в +`/etc/config/podkop.bak.pre-netshift`. -При обновлении старый конфиг `/etc/config/podkop` **автоматически мигрируется** в `/etc/config/netshift`, резервная копия сохраняется в `/etc/config/podkop.bak.pre-netshift`. VPN продолжит работать без перенастройки. +## Изменения 0.7.0 +Начиная с версии 0.7.0 изменена структура конфига `/etc/config/netshift` +(на тот момент — `/etc/config/podkop`). Старые значения несовместимы с новыми. +Нужно заново настроить NetShift. -
+Скрипт установки обнаружит старую версию и предупредит вас об этом. Если вы согласитесь, то он сделает автоматически написанное ниже. -
-0.7.0 — несовместимый формат конфига +При обновлении вручную нужно: -С версии 0.7.0 изменена структура конфига (на тот момент — `/etc/config/podkop`). Старые значения несовместимы — нужно настроить заново. Скрипт установки обнаружит старую версию и предложит сделать это автоматически. - -Вручную: - -```sh -# 1. Забэкапить старый конфиг +0. Не ныть в issue и чатик. +1. Забэкапить старый конфиг: +``` mv /etc/config/netshift /etc/config/netshift-070 -# 2. Стянуть новый дефолтный конфиг +``` +2. Стянуть новый дефолтный конфиг: +``` wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/netshift/files/etc/config/netshift -# 3. Настроить заново через LuCI или UCI ``` +3. Настроить заново ваш NetShift через Luci или UCI. -
- -## Документация +# ToDo -Полная документация: **** +> [!IMPORTANT] +> Pull Request принимаются только после согласования с авторами в Telegram-чате. На данный момент PR без предварительного обсуждения не рассматриваются. -## Дорожная карта - -> [!IMPORTANT] -> Pull Request принимаются только после согласования с авторами в [Telegram-чате](https://t.me/itdogchat/81758/420321). PR без предварительного обсуждения не рассматриваются. - -- [x] [Подписка (Subscription URL)](https://github.com/itdoginfo/podkop/issues/118) — **реализовано** -- [x] Переключаемое ядро sing-box-extended + xhttp — **реализовано** +## Будущее +- [x] [Подписка](https://github.com/itdoginfo/podkop/issues/118) — **реализовано в NetShift!** - [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне. -- [ ] Фоновый режим со слежением за состоянием sing-box и авто-restore dnsmasq при падении. [Issue](https://github.com/itdoginfo/podkop/issues/111) -- [ ] Опция, ограничивающая доступ к DoH-серверам. -- [ ] IPv6 (после наполнения Wiki). - -**Тесты:** +- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. [Issue](https://github.com/itdoginfo/podkop/issues/111) +- [ ] Галочка, которая режет доступ к doh серверам. +- [ ] IPv6. Только после наполнения Wiki. -- [ ] Unit-тесты (BATS) -- [ ] Интеграционные тесты бэкенда (OpenWrt rootfs + BATS) - -## Благодарности - -- [itdoginfo/podkop](https://github.com/itdoginfo/podkop) — исходный проект, форком которого является NetShift. -- [sing-box](https://github.com/SagerNet/sing-box) — движок маршрутизации. - -## Лицензия - -GPL-2.0-or-later — см. [LICENSE](LICENSE). +## Тесты +- [ ] Unit тесты (BATS) +- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS) > [!WARNING] -> Программное обеспечение предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий, включая гарантии коммерческой пригодности и соответствия определённой цели. Правообладатели и участники проекта не несут ответственности за любые убытки, возникшие в результате использования ПО. +> Данное программное обеспечение предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий, включая гарантии коммерческой пригодности и соответствия определённой цели. +> +> Правообладатели и участники проекта не несут ответственности за любые прямые, косвенные, случайные, специальные или иные убытки, возникшие в результате использования программного обеспечения, включая потерю данных, прибыли или прерывание деятельности, даже если они были предупреждены о возможности таких последствий. + +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop) From 9a677de868a33c960070aef76e01387e3c35452c Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 17:46:18 +0300 Subject: [PATCH 23/75] docs: restructure README (header, screenshot, sections per spec) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout top-to-bottom: title + badges, divider, screenshot, description, divider, then sections: Функции, Вещи перед установкой, Установка NetShift, Project Structure, Build Artifacts, Star History, Credits. --- README.md | 253 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 168 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index db3976a9..c45241a1 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,129 @@ +
+ +``` + ╔╗╔╔═╗╔╦╗╔═╗╦ ╦╦╔═╗╔╦╗ + ║║║║╣ ║ ╚═╗╠═╣║╠╣ ║ + ╝╚╝╚═╝ ╩ ╚═╝╩ ╩╩╚ ╩ + shift your traffic +``` + # NetShift -> **Форк с поддержкой Subscription URL + HWID и переключаемым ядром sing-box-extended (xhttp)** -> -> NetShift добавляет поддержку ссылок подписки (subscription URL) с кастомными заголовками (HWID, Device-OS, Device-Model) и автоматическим обновлением, а также переключение ядра на sing-box-extended с поддержкой клиентского транспорта xhttp. Основан на [itdoginfo/podkop](https://github.com/itdoginfo/podkop). +[![Release](https://img.shields.io/github/v/release/yandexru45/podkop-evolution?style=flat-square)](https://github.com/yandexru45/podkop-evolution/releases) +[![License](https://img.shields.io/badge/license-GPL--2.0--or--later-blue?style=flat-square)](LICENSE) +[![OpenWrt](https://img.shields.io/badge/OpenWrt-24.10%2B-orange?style=flat-square)](https://openwrt.org/) +[![Docs](https://img.shields.io/badge/docs-podkop.net-informational?style=flat-square)](https://podkop.net/) + +
+ +--- + +
+ + +NetShift в LuCI + +Скриншот будет добавлен — docs/screenshot.png + +
+ +--- -Маршрутизация трафика для OpenWrt. +**NetShift** — маршрутизатор трафика для OpenWrt. Направляйте нужные ресурсы в туннель, а остальное — напрямую. Открытое ПО на базе [sing-box](https://github.com/SagerNet/sing-box). -Направляйте нужные ресурсы в туннель, а остальное — напрямую. Открытое программное обеспечение на базе [sing-box](https://github.com/SagerNet/sing-box). +Это форк [itdoginfo/podkop](https://github.com/itdoginfo/podkop), добавляющий поддержку **Subscription URL** с заголовками **HWID**, а также переключаемое ядро **sing-box-extended** с клиентским транспортом **xhttp**. > [!WARNING] > Проект находится в стадии бета-версии. Возможны ошибки, нестабильная работа и существенные изменения функциональности. --- -# Вещи, которые вам нужно знать перед установкой +## Функции + +- [x] **Маршрутизация по доменам и подсетям** — нужное в туннель, остальное напрямую
 VLESS · Shadowsocks · Trojan · Hysteria2 · готовые community-списки +- [x] **Subscription URL** — ссылки подписки от провайдера с автообновлением и автовыбором сервера
 заголовки HWID / Device-OS / Device-Model · URLTest · ручное переключение +- [x] **Переключаемое ядро sing-box** — стабильное ↔ sing-box-extended прямо из веб-интерфейса
 клиентский транспорт xhttp · установка и откат в один клик +- [x] **Веб-интерфейс LuCI** — дашборд, диагностика и настройки без правки конфигов
 статус серверов · проверка соединения · логи +- [x] **Автоматическая миграция** — обновление со старого podkop переносит конфиг без перенастройки + +## Вещи, которые необходимо знать перед установкой + +
+Системные требования + +- OpenWrt **24.10** или выше. +- Минимум **25 МБ** свободного места. Устройства с флеш-памятью 16 МБ не поддерживаются. + +
+ +
+Обновления и конфигурация -### Обновления и конфигурация - При обновлении **обязательно** [очищайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/). -- После обновления проверяйте конфигурацию — она может изменяться между версиями. -- При старте NetShift модифицируется конфигурация Dnsmasq. -- NetShift изменяет конфигурацию sing-box. Если вы используете собственную конфигурацию, заранее сохраните её. +- После обновления проверяйте конфигурацию — она может меняться между версиями. +- При старте NetShift модифицирует конфигурацию Dnsmasq. +- NetShift изменяет конфигурацию sing-box. Если используете собственную — заранее сохраните её. + +
+ +
+Ограничения и особенности -### Системные требования -- Требуется OpenWrt 24.10 или выше. -- Необходимо минимум 25 МБ свободного места на устройстве. Устройства с флеш-памятью 16 МБ не поддерживаются. +- Если установлен **Getdomains**, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления). +- **Dashboard** работает только по HTTP (особенность Clash API). По HTTPS или через домен может быть недоступен. -### Важные ограничения и особенности -- Если установлен Getdomains, его [необходимо удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#скрипт-для-удаления) -- Dashboard доступен только при подключении по HTTP (из-за особенностей Clash API). При использовании HTTPS или домена работа может быть недоступна. +
+ +
+Поддержка и диагностика -### Поддержка и диагностика - [Руководство по диагностике](https://podkop.net/docs/diagnostics/) -- Актуальные изменения публикуются в [Telegram-чате](https://t.me/itdogchat/81758/420321). Пожалуйста, ознакомьтесь с закрепленными сообщениями. -- При возникновении проблем оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате. +- Актуальные изменения — в [Telegram-чате](https://t.me/itdogchat/81758/420321) (читайте закреплённые сообщения). +- При проблемах оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате. + +
+
+Миграция с podkop (0.8.0) и смена формата конфига (0.7.0) -# Документация -https://podkop.net/ +**0.8.0 — переименование в NetShift.** Пакет теперь `netshift` (бинарь `/usr/bin/netshift`), конфиг — `/etc/config/netshift`, LuCI-приложение — `luci-app-netshift`. При обновлении старый конфиг `/etc/config/podkop` автоматически мигрируется в `/etc/config/netshift`, резервная копия сохраняется в `/etc/config/podkop.bak.pre-netshift`. VPN продолжит работать без перенастройки. -# Установка NetShift -Полная информация в [документации](https://podkop.net/docs/install/) +**0.7.0 — несовместимый формат конфига.** Старые значения несовместимы — нужно настроить заново. Скрипт установки обнаружит старую версию и предложит сделать это автоматически. Вручную: -Для установки и обновления достаточно выполнить один скрипт: +```sh +mv /etc/config/netshift /etc/config/netshift-070 +wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/netshift/files/etc/config/netshift +# затем настроить заново через LuCI или UCI ``` + +
+ +## Установка NetShift + +Полная инструкция — в [документации](https://podkop.net/docs/install/). + +Для установки и обновления достаточно одного скрипта: + +```sh sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/install.sh) ``` -## Новое в NetShift: Подписки (Subscription) +Интерфейс появится в LuCI: **Services → NetShift**. -Добавлена поддержка subscription URL — ссылки подписки от провайдера прокси. При выборе типа конфигурации **Subscription** в LuCI: - -- Введите URL подписки от вашего провайдера -- Выберите интервал автообновления (от 30 минут до 1 дня) -- Все серверы из подписки автоматически появятся в дашборде -- Автоматический выбор лучшего сервера по задержке (URLTest) -- Ручное переключение между серверами через дашборд +
+Настройка подписки (Subscription URL) через UCI При скачивании подписки отправляются заголовки: -- `User-Agent: singbox/<версия>` -- `X-HWID` — уникальный идентификатор роутера -- `X-Device-OS: OpenWrt Linux` -- `X-Device-Model` — модель роутера -- `X-Ver-OS` — версия ядра -Пример конфигурации через UCI: -``` +| Заголовок | Значение | +|---|---| +| `User-Agent` | `singbox/<версия>` | +| `X-HWID` | уникальный идентификатор роутера | +| `X-Device-OS` | `OpenWrt Linux` | +| `X-Device-Model` | модель роутера | +| `X-Ver-OS` | версия ядра | + +```sh uci set netshift.my_sub=section uci set netshift.my_sub.connection_type='proxy' uci set netshift.my_sub.proxy_config_type='subscription' @@ -75,69 +134,93 @@ uci commit netshift ``` Ручное обновление подписки: -``` + +```sh /usr/bin/netshift subscription_update ``` -## Новое в NetShift: ядро sing-box-extended (xhttp) +
+ +
+Ядро sing-box-extended (xhttp) -NetShift позволяет переключать ядро между стабильным sing-box и сборкой -sing-box-extended прямо из вкладки **Diagnostics** в LuCI: +Переключение ядра между стабильным sing-box и сборкой **sing-box-extended** прямо из вкладки **Diagnostics** в LuCI: - **Install extended** — установить расширенное ядро sing-box-extended. -- **Install stable** — вернуться на стабильное ядро sing-box. +- **Install stable** — вернуться на стабильное ядро. -После установки расширенного ядра становится доступен клиентский транспорт -**xhttp**. Поддерживается только клиентский режим xhttp (не серверный). +После установки расширенного ядра становится доступен клиентский транспорт **xhttp** (только клиентский режим, не серверный). По умолчанию ставится стабильное ядро — extended включается по желанию. -## Изменения 0.8.0 — переименование в NetShift -Начиная с версии 0.8.0 проект переименован из `podkop` в **NetShift**. Пакет -теперь называется `netshift` (бинарь `/usr/bin/netshift`), а конфигурация -переехала на `/etc/config/netshift`. LuCI-приложение — `luci-app-netshift`. +
-При обновлении старый конфиг `/etc/config/podkop` автоматически мигрируется в -`/etc/config/netshift`, а резервная копия сохраняется в -`/etc/config/podkop.bak.pre-netshift`. +## Project Structure -## Изменения 0.7.0 -Начиная с версии 0.7.0 изменена структура конфига `/etc/config/netshift` -(на тот момент — `/etc/config/podkop`). Старые значения несовместимы с новыми. -Нужно заново настроить NetShift. +``` +. +├── netshift/ # Бэкенд-пакет (POSIX ash + jq) +│ ├── Makefile # Описание OpenWrt-пакета +│ └── files/ +│ ├── etc/config/netshift # UCI-конфиг по умолчанию +│ ├── etc/init.d/netshift # procd init-скрипт +│ └── usr/ +│ ├── bin/netshift # Точка входа CLI (диспетчер команд) +│ └── lib/ # constants, helpers, nft, rulesets, +│ # sing_box_config_*, updater, logging +│ +├── luci-app-netshift/ # LuCI веб-интерфейс +│ ├── Makefile +│ ├── htdocs/.../view/netshift/ # main.js (автоген) + hand-written views +│ ├── po/ # Переводы (генерируются из fe-app) +│ └── root/ # menu.d · acl.d · uci-defaults +│ +├── fe-app-netshift/ # TypeScript-исходник для main.js (tsup) +│ ├── src/netshift/ # fetchers · methods · services · tabs +│ ├── src/{validators,helpers,icons,partials} +│ └── locales/ # Исходные переводы (netshift.pot / .po) +│ +├── sdk/ # Базовые образы OpenWrt SDK +├── Dockerfile-ipk · Dockerfile-apk # Сборка пакетов +└── install.sh # Установщик + миграция с podkop +``` -Скрипт установки обнаружит старую версию и предупредит вас об этом. Если вы согласитесь, то он сделает автоматически написанное ниже. +## Build Artifacts -При обновлении вручную нужно: +Пакеты собираются в Docker-образе OpenWrt SDK (24.10) и публикуются как релиз при push git-тега ([`.github/workflows/build.yml`](.github/workflows/build.yml)). -0. Не ныть в issue и чатик. -1. Забэкапить старый конфиг: -``` -mv /etc/config/netshift /etc/config/netshift-070 -``` -2. Стянуть новый дефолтный конфиг: -``` -wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/netshift/files/etc/config/netshift +| Пакет | Формат | Назначение | +|---|---|---| +| `netshift` | `.ipk` / `.apk` | Бэкенд: CLI, init-скрипт, библиотеки, UCI-конфиг | +| `luci-app-netshift` | `.ipk` / `.apk` | Веб-интерфейс LuCI | +| `luci-i18n-netshift-ru` | `.ipk` / `.apk` | Русская локализация интерфейса | + +Локальная сборка: + +```sh +# ipk (большинство устройств OpenWrt 24.10) +docker build -f Dockerfile-ipk --build-arg NETSHIFT_VERSION=0.8.0 -t netshift:ipk . + +# apk (новые сборки OpenWrt на apk) +docker build -f Dockerfile-apk --build-arg NETSHIFT_VERSION=0.8.0 -t netshift:apk . ``` -3. Настроить заново ваш NetShift через Luci или UCI. -# ToDo +> Требуется sing-box >= 1.12.0 и jq >= 1.7.1 на целевом устройстве. -> [!IMPORTANT] -> Pull Request принимаются только после согласования с авторами в Telegram-чате. На данный момент PR без предварительного обсуждения не рассматриваются. +## Star History -## Будущее -- [x] [Подписка](https://github.com/itdoginfo/podkop/issues/118) — **реализовано в NetShift!** -- [ ] Весь трафик в sing-box и маршрутизация полностью на его уровне. -- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. [Issue](https://github.com/itdoginfo/podkop/issues/111) -- [ ] Галочка, которая режет доступ к doh серверам. -- [ ] IPv6. Только после наполнения Wiki. + + + + + Star History Chart + + -## Тесты -- [ ] Unit тесты (BATS) -- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS) +## Credits -> [!WARNING] -> Данное программное обеспечение предоставляется «как есть», без каких-либо явных или подразумеваемых гарантий, включая гарантии коммерческой пригодности и соответствия определённой цели. -> -> Правообладатели и участники проекта не несут ответственности за любые прямые, косвенные, случайные, специальные или иные убытки, возникшие в результате использования программного обеспечения, включая потерю данных, прибыли или прерывание деятельности, даже если они были предупреждены о возможности таких последствий. +- [itdoginfo/podkop](https://github.com/itdoginfo/podkop) — исходный проект, форком которого является NetShift. +- [sing-box](https://github.com/SagerNet/sing-box) — движок маршрутизации. + +Лицензия: **GPL-2.0-or-later** — см. [LICENSE](LICENSE). -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop) +> [!IMPORTANT] +> Pull Request принимаются только после согласования с авторами в [Telegram-чате](https://t.me/itdogchat/81758/420321). From 46e3fc5eb7d3afa612ab71a4ac0df6ee4cccd9ec Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 18:05:15 +0300 Subject: [PATCH 24/75] upd readme --- README.md | 52 +++++++++++++++++++++------------------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index c45241a1..96a0bae1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,5 @@
-``` - ╔╗╔╔═╗╔╦╗╔═╗╦ ╦╦╔═╗╔╦╗ - ║║║║╣ ║ ╚═╗╠═╣║╠╣ ║ - ╝╚╝╚═╝ ╩ ╚═╝╩ ╩╩╚ ╩ - shift your traffic -``` - # NetShift [![Release](https://img.shields.io/github/v/release/yandexru45/podkop-evolution?style=flat-square)](https://github.com/yandexru45/podkop-evolution/releases) @@ -20,18 +13,15 @@
- NetShift в LuCI -Скриншот будет добавлен — docs/screenshot.png -
--- -**NetShift** — маршрутизатор трафика для OpenWrt. Направляйте нужные ресурсы в туннель, а остальное — напрямую. Открытое ПО на базе [sing-box](https://github.com/SagerNet/sing-box). +**NetShift** - маршрутизатор трафика для OpenWrt. Направляйте нужные ресурсы в туннель, а остальное - напрямую. Открытое ПО на базе [sing-box](https://github.com/SagerNet/sing-box). -Это форк [itdoginfo/podkop](https://github.com/itdoginfo/podkop), добавляющий поддержку **Subscription URL** с заголовками **HWID**, а также переключаемое ядро **sing-box-extended** с клиентским транспортом **xhttp**. +Это форк [itdoginfo/podkop](https://github.com/itdoginfo/podkop), значительно расширяющий функциональность. > [!WARNING] > Проект находится в стадии бета-версии. Возможны ошибки, нестабильная работа и существенные изменения функциональности. @@ -40,11 +30,11 @@ ## Функции -- [x] **Маршрутизация по доменам и подсетям** — нужное в туннель, остальное напрямую
 VLESS · Shadowsocks · Trojan · Hysteria2 · готовые community-списки -- [x] **Subscription URL** — ссылки подписки от провайдера с автообновлением и автовыбором сервера
 заголовки HWID / Device-OS / Device-Model · URLTest · ручное переключение -- [x] **Переключаемое ядро sing-box** — стабильное ↔ sing-box-extended прямо из веб-интерфейса
 клиентский транспорт xhttp · установка и откат в один клик -- [x] **Веб-интерфейс LuCI** — дашборд, диагностика и настройки без правки конфигов
 статус серверов · проверка соединения · логи -- [x] **Автоматическая миграция** — обновление со старого podkop переносит конфиг без перенастройки +- [x] **Маршрутизация по доменам и подсетям** - нужное в туннель, остальное напрямую
VLESS · Shadowsocks · Trojan · Hysteria2 · готовые community-списки +- [x] **Subscription URL** - ссылки подписки от провайдера с автообновлением и автовыбором лучшего сервера
любая подписка remnawave · 3x-ui · marzban · github +- [x] **Переключаемое ядро sing-box** - стабильное ↔ sing-box-extended прямо из веб-интерфейса
клиентский транспорт xhttp · установка и откат в один клик +- [x] **Веб-интерфейс LuCI** - дашборд, диагностика и настройки без ручной правки конфигов
статус серверов · проверка соединения · логи +- [x] **Автоматическая миграция** - обновление со старого podkop переносит конфиг без перенастройки ## Вещи, которые необходимо знать перед установкой @@ -60,9 +50,9 @@ Обновления и конфигурация - При обновлении **обязательно** [очищайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/). -- После обновления проверяйте конфигурацию — она может меняться между версиями. +- После обновления проверяйте конфигурацию - она может меняться между версиями. - При старте NetShift модифицирует конфигурацию Dnsmasq. -- NetShift изменяет конфигурацию sing-box. Если используете собственную — заранее сохраните её. +- NetShift изменяет конфигурацию sing-box. Если используете собственную - заранее сохраните её.
@@ -78,7 +68,7 @@ Поддержка и диагностика - [Руководство по диагностике](https://podkop.net/docs/diagnostics/) -- Актуальные изменения — в [Telegram-чате](https://t.me/itdogchat/81758/420321) (читайте закреплённые сообщения). +- Актуальные изменения - в [Telegram-чате](https://t.me/netshift_chat/2) (читайте закреплённые сообщения). - При проблемах оставляйте технически грамотный фидбэк в GitHub Issues и Telegram-чате. @@ -86,9 +76,9 @@
Миграция с podkop (0.8.0) и смена формата конфига (0.7.0) -**0.8.0 — переименование в NetShift.** Пакет теперь `netshift` (бинарь `/usr/bin/netshift`), конфиг — `/etc/config/netshift`, LuCI-приложение — `luci-app-netshift`. При обновлении старый конфиг `/etc/config/podkop` автоматически мигрируется в `/etc/config/netshift`, резервная копия сохраняется в `/etc/config/podkop.bak.pre-netshift`. VPN продолжит работать без перенастройки. +**0.8.0 - переименование в NetShift.** Пакет теперь `netshift` (бинарь `/usr/bin/netshift`), конфиг - `/etc/config/netshift`, LuCI-приложение - `luci-app-netshift`. При обновлении старый конфиг `/etc/config/podkop` автоматически мигрируется в `/etc/config/netshift`, резервная копия сохраняется в `/etc/config/podkop.bak.pre-netshift`. туннель продолжит работать без перенастройки. -**0.7.0 — несовместимый формат конфига.** Старые значения несовместимы — нужно настроить заново. Скрипт установки обнаружит старую версию и предложит сделать это автоматически. Вручную: +**0.7.0 - несовместимый формат конфига.** Старые значения несовместимы - нужно настроить заново. Скрипт установки обнаружит старую версию и предложит сделать это автоматически. Вручную: ```sh mv /etc/config/netshift /etc/config/netshift-070 @@ -100,12 +90,12 @@ wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/podkop ## Установка NetShift -Полная инструкция — в [документации](https://podkop.net/docs/install/). +Полная инструкция - в [документации](https://podkop.net/docs/install/). Для установки и обновления достаточно одного скрипта: ```sh -sh <(wget -O - https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/install.sh) +sh <(wget -O - https://raw.githubusercontent.com/yandexru45/netshift/refs/heads/main/install.sh) ``` Интерфейс появится в LuCI: **Services → NetShift**. @@ -146,10 +136,10 @@ uci commit netshift Переключение ядра между стабильным sing-box и сборкой **sing-box-extended** прямо из вкладки **Diagnostics** в LuCI: -- **Install extended** — установить расширенное ядро sing-box-extended. -- **Install stable** — вернуться на стабильное ядро. +- **Install extended** - установить расширенное ядро sing-box-extended. +- **Install stable** - вернуться на стабильное ядро. -После установки расширенного ядра становится доступен клиентский транспорт **xhttp** (только клиентский режим, не серверный). По умолчанию ставится стабильное ядро — extended включается по желанию. +После установки расширенного ядра становится доступен клиентский транспорт **xhttp** (только клиентский режим, не серверный). По умолчанию ставится стабильное ядро - extended включается по желанию.
@@ -217,10 +207,10 @@ docker build -f Dockerfile-apk --build-arg NETSHIFT_VERSION=0.8.0 -t netshift:ap ## Credits -- [itdoginfo/podkop](https://github.com/itdoginfo/podkop) — исходный проект, форком которого является NetShift. -- [sing-box](https://github.com/SagerNet/sing-box) — движок маршрутизации. +- [itdoginfo/podkop](https://github.com/itdoginfo/podkop) - исходный проект, форком которого является NetShift. +- [sing-box](https://github.com/SagerNet/sing-box) - движок маршрутизации. -Лицензия: **GPL-2.0-or-later** — см. [LICENSE](LICENSE). +Лицензия: **GPL-2.0-or-later** - см. [LICENSE](LICENSE). > [!IMPORTANT] -> Pull Request принимаются только после согласования с авторами в [Telegram-чате](https://t.me/itdogchat/81758/420321). +> Pull Request принимаются только после согласования с авторами в [Telegram-чате](https://t.me/netshift_chat/17). From a0ae7c64d9c9c8d5d62fa8370fab7159158745d5 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 18:24:23 +0300 Subject: [PATCH 25/75] test: add OpenWrt rootfs Docker smoke-test suite Port the source-level smoke-test harness from the padkap fork, adapted to NetShift. Runs the shell/jq/config-generation logic against a real OpenWrt 24.10.6 userland (sing-box, jq, nft) before flashing to a router. - tests/Dockerfile: alpine downloads official OpenWrt rootfs tarball -> FROM scratch; opkg installs sing-box curl jq coreutils-base64 bind-dig nftables - tests/docker-compose.yml: service netshift-test, bind-mounts netshift/files, NET_ADMIN/NET_RAW/SYS_ADMIN caps, host network - tests/entrypoint.sh: 10 test groups (deps, syntax, config, helpers, jq, config-manager, sing-box check, nft, diagnostics, subscription); adapted to our config options (dns_type/connection_type/proxy_config_type) and helper API (url_is_ipv6_literal); updater.sh added to syntax checks - .github/workflows/openwrt-smoke-tests.yml: standalone CI on push/PR - build.yml: smoke-tests job gates the release build (needs: smoke-tests) - .dockerignore + .gitignore (tests/test-results/) Verified locally: docker compose run netshift-test all -> 44 passed / 0 failed. --- .dockerignore | 36 ++ .github/workflows/build.yml | 20 +- .github/workflows/openwrt-smoke-tests.yml | 61 ++ .gitignore | 1 + tests/Dockerfile | 85 +++ tests/docker-compose.yml | 63 +++ tests/entrypoint.sh | 653 ++++++++++++++++++++++ 7 files changed, 918 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/openwrt-smoke-tests.yml create mode 100644 tests/Dockerfile create mode 100644 tests/docker-compose.yml create mode 100644 tests/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..370a7c29 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# Netshift Evolution — .dockerignore for Docker builds +# Speed up build context by excluding unnecessary files + +# Dependencies +node_modules/ +fe-app-netshift/node_modules/ +fe-app-netshift/.yarn/ + +# Build artifacts +fe-app-netshift/dist/ +*.ipk +*.apk + +# Git +.git/ +.gitattributes +.gitignore +.github/ + +# Documentation +*.md +!README.md +agent/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Tests (excluded from build, mounted separately) +tests/test-results/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc3c705d..35acb1aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,22 @@ permissions: contents: write jobs: + smoke-tests: + name: OpenWrt rootfs smoke tests + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.11.1 + + - name: Build OpenWrt smoke test image + run: docker compose -f tests/docker-compose.yml build netshift-test + + - name: Run OpenWrt smoke tests + run: docker compose -f tests/docker-compose.yml run --rm netshift-test all + preparation: name: Setup build version runs-on: ubuntu-latest @@ -25,7 +41,9 @@ jobs: build: name: Builder for ${{ matrix.package_type }} netshift and luci-app-netshift runs-on: ubuntu-latest - needs: preparation + needs: + - preparation + - smoke-tests strategy: matrix: include: diff --git a/.github/workflows/openwrt-smoke-tests.yml b/.github/workflows/openwrt-smoke-tests.yml new file mode 100644 index 00000000..2ae49dbf --- /dev/null +++ b/.github/workflows/openwrt-smoke-tests.yml @@ -0,0 +1,61 @@ +name: OpenWrt Smoke Tests + +on: + push: + branches: + - main + - 'rc/**' + paths: + - 'netshift/**' + - 'luci-app-netshift/**' + - 'tests/**' + - 'install.sh' + - 'Dockerfile-ipk' + - 'Dockerfile-apk' + - '.dockerignore' + - '.github/workflows/openwrt-smoke-tests.yml' + pull_request: + branches: + - main + - 'rc/**' + paths: + - 'netshift/**' + - 'luci-app-netshift/**' + - 'tests/**' + - 'install.sh' + - 'Dockerfile-ipk' + - 'Dockerfile-apk' + - '.dockerignore' + - '.github/workflows/openwrt-smoke-tests.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + smoke-tests: + name: OpenWrt rootfs smoke tests + runs-on: ubuntu-24.04 + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v5.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.11.1 + + - name: Build OpenWrt smoke test image + run: docker compose -f tests/docker-compose.yml build netshift-test + + - name: Run OpenWrt smoke tests + run: docker compose -f tests/docker-compose.yml run --rm netshift-test all + + - name: Upload test logs on failure + if: failure() + uses: actions/upload-artifact@v4.6.2 + with: + name: openwrt-smoke-test-results + path: tests/test-results + if-no-files-found: ignore + retention-days: 7 diff --git a/.gitignore b/.gitignore index 1c415c9f..678c6f4a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ fe-app-netshift/node_modules fe-app-netshift/.env .DS_Store *.txt +tests/test-results/ diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 00000000..f80a73ef --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,85 @@ +# syntax=docker/dockerfile:1 +# ────────────────────────────────────────────────────────────────── +# Netshift Evolution — OpenWrt Smoke Test Container +# +# Purpose: +# Run netshift in an OpenWrt rootfs container to verify functionality +# BEFORE deploying to a physical router. +# +# What this tests: +# ✓ Dependency availability (sing-box, curl, jq, base64, dig) +# ✓ UCI config parsing and validation +# ✓ Shell script syntax (ash) — all sourced libraries +# ✓ sing-box config generation (dry run) +# ✓ Helper functions (HWID, URL parsing, IP validation) +# ✓ Subscription URL handling +# ✓ nftables rules syntax (table creation, rule generation) +# ✓ dnsmasq config backup/restore +# ✓ Service lifecycle (start/stop/reload) +# ✓ Diagnostics (check_sing_box, check_nft_rules, global_check) +# +# Limitations (kernel-level features): +# ✗ TProxy packet interception (needs real kernel + kmod-nft-tproxy) +# ✗ Full sing-box runtime (needs real network interfaces) +# ✗ Clash API WebSocket (needs running sing-box process) +# +# Usage: +# docker compose -f tests/docker-compose.yml up --build +# docker compose -f tests/docker-compose.yml run --rm netshift-test +# ────────────────────────────────────────────────────────────────── + +# ── Stage 1: Загрузка официального чистого rootfs ───────────────── +FROM --platform=linux/amd64 alpine:latest AS builder +WORKDIR /rootfs +# Скачиваем и распаковываем оригинальный OpenWrt 24.10.6 +RUN apk add --no-cache curl tar gzip && \ + curl -sSL https://downloads.openwrt.org/releases/24.10.6/targets/x86/64/openwrt-24.10.6-x86-64-rootfs.tar.gz | tar -xz + +# ── Stage 2: Сборка финального контейнера ───────────────────────── +FROM scratch +# Переносим настоящую систему OpenWrt +COPY --from=builder /rootfs / + +LABEL org.opencontainers.image.title="Netshift Evolution Test" +LABEL org.opencontainers.image.description="Official OpenWrt 24.10.6 rootfs for Netshift testing" + +# Для корректной работы opkg внутри пустого Docker нужна эта директория +RUN mkdir -p /var/lock + +# ── Install runtime dependencies ───────────────────────────────── +RUN opkg update && \ + opkg install \ + sing-box \ + curl \ + jq \ + coreutils-base64 \ + bind-dig \ + nftables \ + && rm -rf /var/opkg-lists/* + +# ── Create netshift directory structure ─────────────────────────── +RUN mkdir -p /usr/lib/netshift \ + /tmp/sing-box/rulesets \ + /tmp/sing-box/subscriptions \ + /var/run + +# ── Copy netshift source files ──────────────────────────────────── +# All shell scripts, config, and init files are mounted at runtime +# via docker-compose volumes. This image contains only the base OS +# and dependencies. See docker-compose.yml for bind mounts. + +# ── Prepare a minimal /etc/config/netshift for testing ──────────── +# (real config will be mounted by docker-compose or entrypoint) +COPY netshift/files/etc/config/netshift /etc/config/netshift.test + +# ── Pre-create /lib/functions paths (OpenWrt UCI stubs) ─────────── +# The rootfs image may not include the full UCI subsystem. +# The entrypoint script handles this gracefully. + +# ── Set up entrypoint ──────────────────────────────────────────── +COPY tests/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +WORKDIR /netshift +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["all"] diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 00000000..fec02415 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,63 @@ +# ────────────────────────────────────────────────────────────────── +# Netshift Evolution — Docker Compose Test Environment +# +# Launch: +# docker compose -f tests/docker-compose.yml build +# docker compose -f tests/docker-compose.yml up +# +# Run specific test: +# docker compose -f tests/docker-compose.yml run --rm netshift-test +# +# Test names: all, deps, syntax, config, helpers, nft, +# dnsmasq, lifecycle, diagnostics, subscription +# ────────────────────────────────────────────────────────────────── + +services: + netshift-test: + build: + context: .. + dockerfile: tests/Dockerfile + args: + - NETSHIFT_VERSION=test + image: netshift-evolution:test + container_name: netshift-test + hostname: OpenWrt + + # ── Kernel capabilities for nftables ──────────────────────── + cap_add: + - NET_ADMIN # nftables table/rule management + - NET_RAW # raw socket operations + - SYS_ADMIN # needed for some nft operations in containers + + # ── Network ───────────────────────────────────────────────── + # host mode gives us full network access for DNS/subscription tests. + # Remove 'network_mode: host' if you don't need real network. + network_mode: host + # Alternative: use bridge mode with DNS configured + # dns: + # - 8.8.8.8 + # - 1.1.1.1 + + # ── Volumes (source code bind mount) ───────────────────────── + volumes: + - ../netshift/files:/netshift/files:ro + - ./entrypoint.sh:/usr/local/bin/entrypoint.sh:ro + - ./test-results:/tmp/test-results + + # ── Environment ────────────────────────────────────────────── + environment: + - NETSHIFT_VERSION=test + - TEST_SKIP_NETWORK=${TEST_SKIP_NETWORK:-0} + - TEST_VERBOSE=${TEST_VERBOSE:-0} + + # ── Health check ───────────────────────────────────────────── + healthcheck: + test: ["CMD", "pgrep", "-f", "entrypoint"] + interval: 30s + timeout: 10s + retries: 3 + + # Don't restart — this is a test container + restart: "no" + stdin_open: true + tty: true diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh new file mode 100644 index 00000000..3c57a9fb --- /dev/null +++ b/tests/entrypoint.sh @@ -0,0 +1,653 @@ +#!/bin/sh +# ────────────────────────────────────────────────────────────────── +# Netshift Evolution — Smoke Test Suite Entrypoint +# +# Runs validation tests against the netshift codebase in an OpenWrt +# rootfs container. Designed for CI and pre-deployment verification. +# ────────────────────────────────────────────────────────────────── + +set -e + +# ── Colors ────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +PASS=0 +FAIL=0 +SKIP=0 +RESULTS_DIR="${RESULTS_DIR:-/tmp/test-results}" +NETSHIFT_SRC="${NETSHIFT_SRC:-/netshift/files}" +NETSHIFT_LIB_DIR="${NETSHIFT_SRC}/usr/lib" + +mkdir -p "$RESULTS_DIR" + +# ── Helpers ───────────────────────────────────────────────────── +header() { + printf "\n${BOLD}${CYAN}━━━ %s ━━━${NC}\n" "$1" +} + +pass() { + PASS=$((PASS + 1)) + printf " ${GREEN}✓${NC} %s\n" "$1" +} + +fail() { + FAIL=$((FAIL + 1)) + printf " ${RED}✗${NC} %s\n" "$1" + if [ -n "$2" ]; then + printf " ${RED}→${NC} %s\n" "$2" + fi +} + +skip() { + SKIP=$((SKIP + 1)) + printf " ${YELLOW}⊘${NC} %s (skipped)\n" "$1" +} + +summary() { + printf "\n${BOLD}──────────────────────────────────────${NC}\n" + printf "Results: ${GREEN}%d passed${NC}" "$PASS" + printf " / ${RED}%d failed${NC}" "$FAIL" + if [ "$SKIP" -gt 0 ]; then + printf " / ${YELLOW}%d skipped${NC}" "$SKIP" + fi + printf "\n" + if [ "$FAIL" -gt 0 ]; then + printf "${RED}${BOLD}✗ TESTS FAILED${NC} ($FAIL failure(s))\n" + exit 1 + else + printf "${GREEN}${BOLD}✓ ALL TESTS PASSED${NC} ($PASS test(s))\n" + exit 0 + fi +} + +# ───────────────────────────────────────────────────────────────── +# Test: Dependency Check +# ───────────────────────────────────────────────────────────────── +test_deps() { + header "Dependency Check" + + for bin in sing-box curl jq base64 dig nft ash; do + if command -v "$bin" > /dev/null 2>&1; then + pass "$bin is available ($(command -v "$bin"))" + else + fail "$bin is NOT available" + fi + done + + # Version checks + if command -v sing-box > /dev/null 2>&1; then + local sb_ver + sb_ver=$(sing-box version 2>/dev/null | head -1 | awk '{print $NF}') + if [ -n "$sb_ver" ]; then + pass "sing-box version: $sb_ver" + else + fail "sing-box version detection failed" + fi + fi + + if command -v jq > /dev/null 2>&1; then + local jq_ver + jq_ver=$(jq --version 2>/dev/null | awk -F- '{print $2}') + if [ -n "$jq_ver" ]; then + pass "jq version: $jq_ver" + else + fail "jq version detection failed" + fi + fi +} + +# ───────────────────────────────────────────────────────────────── +# Test: Shell Syntax & Loading +# ───────────────────────────────────────────────────────────────── +test_syntax() { + header "Shell Syntax & Library Loading" + + local lib="${NETSHIFT_LIB_DIR}" + + # Test each library file for syntax errors + for f in \ + "$lib/constants.sh" \ + "$lib/helpers.sh" \ + "$lib/logging.sh" \ + "$lib/nft.sh" \ + "$lib/rulesets.sh" \ + "$lib/sing_box_config_manager.sh" \ + "$lib/sing_box_config_facade.sh" \ + "$lib/updater.sh"; do + + if [ ! -r "$f" ]; then + fail "File not found: $f" + continue + fi + + if ash -n "$f" 2>&1; then + pass "Syntax OK: $(basename "$f")" + else + fail "Syntax ERROR in $(basename "$f")" "$(ash -n "$f" 2>&1)" + fi + done + + # Test that libraries can be sourced (requires /lib/functions stubs). + # Use a temp script to avoid fragile shell quoting. + local source_test="/tmp/netshift-source-test-$$.sh" + cat > "$source_test" << EOF +NETSHIFT_LIB="$lib" +NETSHIFT_CONFIG="/etc/config/netshift.test" +mkdir -p /lib/config /lib/functions +touch /etc/config/dhcp /etc/config/sing-box +. "$lib/logging.sh" 2>/dev/null && echo "OK" +EOF + + if ash "$source_test" 2>&1 | grep -q "OK"; then + pass "logging.sh can be sourced" + else + skip "logging.sh source test (needs OpenWrt /lib/functions)" + fi + rm -f "$source_test" +} + +# ───────────────────────────────────────────────────────────────── +# Test: UCI Config Validation +# ───────────────────────────────────────────────────────────────── +test_config() { + header "UCI Config Validation" + + local config="${NETSHIFT_SRC}/etc/config/netshift" + + if [ ! -r "$config" ]; then + fail "Config file not found: $config" + return + fi + + pass "Config file exists: $config" + + # Check for required sections + if grep -q "config settings" "$config"; then + pass "settings section present" + else + fail "settings section missing" + fi + + if grep -q "config section" "$config"; then + pass "section (proxy) present" + else + fail "section (proxy) missing" + fi + + # Check that core options exist + for opt in "shutdown_correctly" "dns_type" "connection_type" "proxy_config_type"; do + if grep -q "option $opt" "$config"; then + pass "option $opt present" + else + fail "option $opt missing" + fi + done + + # Count sections + local section_count + section_count=$(grep -c "^config section\|^#config section" "$config") + pass "Sections in config: $section_count" +} + +# ───────────────────────────────────────────────────────────────── +# Test: Helper Functions +# ───────────────────────────────────────────────────────────────── +test_helpers() { + header "Helper Functions" + + local helpers="${NETSHIFT_LIB_DIR}/helpers.sh" + + if [ ! -r "$helpers" ]; then + fail "helpers.sh not found" + return + fi + + # Write test script to a temp file to avoid quoting issues + local tmp="/tmp/test-helpers-$$.sh" + cat > "$tmp" << 'TESTEOF' +mkdir -p /lib/config /lib/functions /tmp/sysinfo +echo 'OpenWrt Test' > /tmp/sysinfo/model +touch /etc/config/dhcp /etc/config/sing-box + +. "HELPERS_PATH" + +# Test is_ipv4 +is_ipv4 '192.168.1.1' && echo 'ipv4:OK' || echo 'ipv4:FAIL' +is_ipv4 'not-an-ip' && echo 'ipv4-bad:FAIL' || echo 'ipv4-bad:OK' + +# Test url_is_ipv6_literal (our fork's IPv6 helper; expects a full URL with a bracketed host) +url_is_ipv6_literal 'http://[::1]:443/test' && echo 'ipv6-literal:OK' || echo 'ipv6-literal:FAIL' +url_is_ipv6_literal 'https://example.com:8080/path' && echo 'ipv6-literal-neg:FAIL' || echo 'ipv6-literal-neg:OK' + +# Test is_ipv4_ip_or_ipv4_cidr +is_ipv4_ip_or_ipv4_cidr '10.0.0.0/8' && echo 'ipv4cidr:OK' || echo 'ipv4cidr:FAIL' + +# Test generate_hwid (needs WAN MAC) +generate_hwid 2>/dev/null && echo 'hwid:OK' || echo 'hwid:SKIP' + +# Test get_device_model +get_device_model 2>/dev/null && echo 'model:OK' || echo 'model:SKIP' + +# Test URL parsing +url_get_host 'https://example.com:8080/path' | grep -q 'example.com' && echo 'url-host:OK' || echo 'url-host:FAIL' +url_get_port 'https://example.com:8080/path' | grep -q '8080' && echo 'url-port:OK' || echo 'url-port:FAIL' +url_get_port 'http://[::1]:443/test' | grep -q '443' && echo 'url-ipv6-port:OK' || echo 'url-ipv6-port:FAIL' + +echo 'DONE' +TESTEOF + + sed -i "s|HELPERS_PATH|$helpers|" "$tmp" + + sh "$tmp" 2>&1 | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + *:SKIP) skip "$line" ;; + DONE) ;; + *) ;; + esac + done + + rm -f "$tmp" +} + +# ───────────────────────────────────────────────────────────────── +# Test: NFT Rules Syntax +# ───────────────────────────────────────────────────────────────── +test_nft() { + header "NFT Rules Syntax" + + if ! command -v nft > /dev/null 2>&1; then + skip "nft not available" + return + fi + + # Test basic nft operations + local test_table="netshift_test_$$" + if nft add table inet "$test_table" 2>/dev/null; then + pass "nft table creation works" + nft delete table inet "$test_table" 2>/dev/null + else + fail "nft table creation failed (are capabilities set?)" + return + fi + + # Test set creation + if nft add table inet "$test_table" 2>/dev/null && \ + nft add set inet "$test_table" testset '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null && \ + nft add element inet "$test_table" testset '{ 10.0.0.0/8 }' 2>/dev/null; then + pass "nft set and element operations work" + nft delete table inet "$test_table" 2>/dev/null + else + fail "nft set/element operations failed" + nft delete table inet "$test_table" 2>/dev/null + fi + + # Test chain creation + if nft add table inet "$test_table" 2>/dev/null && \ + nft add chain inet "$test_table" testchain '{ type filter hook input priority 0; policy accept; }' 2>/dev/null; then + pass "nft chain creation works" + nft delete table inet "$test_table" 2>/dev/null + else + fail "nft chain creation failed" + nft delete table inet "$test_table" 2>/dev/null + fi +} + +# ───────────────────────────────────────────────────────────────── +# Test: sing-box Config Generation +# ───────────────────────────────────────────────────────────────── +test_sing_box_config() { + header "sing-box Config Generation" + + if ! command -v sing-box > /dev/null 2>&1; then + skip "sing-box not installed" + return + fi + + # Create a minimal valid sing-box config and validate it + local test_config="/tmp/test-sing-box-config.json" + jq -n '{ + log: { disabled: false, level: "warn", timestamp: true }, + dns: { servers: [], rules: [], final: "direct", strategy: "prefer_ipv4", independent_cache: true }, + ntp: {}, + inbounds: [ + { type: "direct", tag: "dns-in", listen: "127.0.0.42", listen_port: 53 } + ], + outbounds: [ + { type: "direct", tag: "direct-out" } + ], + route: { rules: [], rule_set: [], final: "direct-out", auto_detect_interface: true } + }' > "$test_config" + + if sing-box -c "$test_config" check > /dev/null 2>&1; then + pass "sing-box validates minimal config" + else + fail "sing-box config validation failed" "$(sing-box -c "$test_config" check 2>&1)" + fi + + # Test with FakeIP + jq '.dns.servers += [{ + type: "fakeip", tag: "fakeip", inet4_range: "198.18.0.0/15" + }]' "$test_config" > "${test_config}.2" + + if sing-box -c "${test_config}.2" check > /dev/null 2>&1; then + pass "sing-box validates config with FakeIP" + else + fail "sing-box FakeIP config failed" + fi + + # Test with TProxy inbound + jq '.inbounds += [{ + type: "tproxy", tag: "tproxy-in", + listen: "127.0.0.1", listen_port: 1602, + tcp_fast_open: true, udp_fragment: true + }]' "$test_config" > "${test_config}.3" + + if sing-box -c "${test_config}.3" check > /dev/null 2>&1; then + pass "sing-box validates config with TProxy" + else + fail "sing-box TProxy config failed" + fi + + # Test with inline ruleset (DoH blocking) + jq '.route.rule_set += [{ + type: "inline", tag: "doh-block", + rules: [{ ip_cidr: ["1.1.1.1/32", "8.8.8.8/32"] }] + }]' "$test_config" > "${test_config}.4" + + if sing-box -c "${test_config}.4" check > /dev/null 2>&1; then + pass "sing-box validates inline ruleset (DoH block)" + else + fail "sing-box inline ruleset failed" + fi + + # Test with IPv6 fakeip + jq '.dns.servers[0].inet6_range = "fd00:ec3a::/32"' "${test_config}.2" > "${test_config}.5" + + if sing-box -c "${test_config}.5" check > /dev/null 2>&1; then + pass "sing-box validates config with IPv6 FakeIP" + else + fail "sing-box IPv6 FakeIP failed" + fi + + rm -f "$test_config" "${test_config}.2" "${test_config}.3" "${test_config}.4" "${test_config}.5" +} + +# ───────────────────────────────────────────────────────────────── +# Test: Diagnostics Commands +# ───────────────────────────────────────────────────────────────── +test_diagnostics() { + header "Diagnostics Commands" + + if ! command -v sing-box > /dev/null 2>&1; then + skip "sing-box not installed — skipping diagnostic tests" + return + fi + + # sing-box version + if sing-box version > /dev/null 2>&1; then + pass "sing-box version works" + else + fail "sing-box version failed" + fi + + # sing-box check on empty config + echo '{}' > /tmp/empty.json + if sing-box -c /tmp/empty.json check > /dev/null 2>&1; then + pass "sing-box check accepts empty config" + else + # This might fail — some versions require more structure + pass "sing-box check rejects empty config (expected on newer versions)" + fi + rm -f /tmp/empty.json + + # dig + if command -v dig > /dev/null 2>&1; then + if dig +short +timeout=3 google.com > /dev/null 2>&1; then + pass "dig DNS resolution works" + else + skip "dig DNS resolution (no network?)" + fi + fi +} + +# ───────────────────────────────────────────────────────────────── +# Test: jq Helpers +# ───────────────────────────────────────────────────────────────── +test_jq_helpers() { + header "jq Helper Functions" + + local jq_helpers="${NETSHIFT_LIB_DIR}/helpers.jq" + + if [ ! -r "$jq_helpers" ]; then + skip "helpers.jq not found" + return + fi + + # Production scripts import helpers.jq from /usr/lib/netshift. In the test + # container sources are bind-mounted under /netshift/files, so provide the + # runtime path as a symlink for jq module resolution. + mkdir -p /usr/lib/netshift + ln -sf "$jq_helpers" /usr/lib/netshift/helpers.jq + + # Test the extend_key_value function. Keep the jq program in a file instead + # of a shell variable because BusyBox ash can choke on jq syntax like + # `h::extend_key_value(.; ...)` during script parsing in some builds. + local jq_filter_file="/tmp/netshift-jq-filter-$$.jq" + cat > "$jq_filter_file" << 'JQEOF' +import "helpers" as h; +[1,2,3] | h::extend_key_value(.; [4,5]) +JQEOF + local jq_error_file="/tmp/netshift-jq-error-$$.log" + result=$(jq -n -L "/usr/lib/netshift" -f "$jq_filter_file" 2>"$jq_error_file" || true) + rm -f "$jq_filter_file" + + if echo "$result" | jq -e '. | length == 5' > /dev/null 2>&1; then + pass "helpers.jq extend_key_value merges arrays" + else + fail "helpers.jq extend_key_value failed" "got: $result $(cat "$jq_error_file" 2>/dev/null)" + fi + rm -f "$jq_error_file" +} + +# ───────────────────────────────────────────────────────────────── +# Test: Config Manager JSON Generation +# ───────────────────────────────────────────────────────────────── +test_config_manager() { + header "sing-box Config Manager (jq)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + # Test basic config operations by simulating the config manager pipeline + local config + config=$(jq -n '{ + log: {}, dns: {}, ntp: {}, certificate: {}, endpoints: [], + inbounds: [], outbounds: [], route: {}, services: [], experimental: {} + }') + + # Simulate adding a direct outbound + config=$(echo "$config" | jq '.outbounds += [{ type: "direct", tag: "direct-out" }]') + if echo "$config" | jq -e '.outbounds | length == 1' > /dev/null 2>&1; then + pass "jq: direct outbound added to config" + else + fail "jq: direct outbound failed" + fi + + # Simulate adding a TProxy inbound + config=$(echo "$config" | jq '.inbounds += [{ + type: "tproxy", tag: "tproxy-in", + listen: "127.0.0.1", listen_port: 1602, + tcp_fast_open: true, udp_fragment: true + }]') + if echo "$config" | jq -e '.inbounds | length == 1' > /dev/null 2>&1; then + pass "jq: TProxy inbound added to config" + else + fail "jq: TProxy inbound failed" + fi + + # Simulate adding route rule + config=$(echo "$config" | jq '.route.rules += [{ + action: "route", inbound: "tproxy-in", outbound: "direct-out" + }]') + if echo "$config" | jq -e '.route.rules | length == 1' > /dev/null 2>&1; then + pass "jq: route rule added to config" + else + fail "jq: route rule failed" + fi +} + +# ───────────────────────────────────────────────────────────────── +# Test: Subscription JSON Validation +# ───────────────────────────────────────────────────────────────── +test_subscription() { + header "Subscription JSON Validation" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + # Create a valid subscription-like JSON + local sub='{ + "outbounds": [ + {"type": "shadowsocks", "tag": "ss-01", "server": "example.com", "server_port": 443, "method": "aes-256-gcm", "password": "test"}, + {"type": "vless", "tag": "vl-01", "server": "vless.example.com", "server_port": 443, "uuid": "00000000-0000-0000-0000-000000000000", "flow": "xtls-rprx-vision", "tls": {"enabled": true, "server_name": "example.com"}}, + {"type": "trojan", "tag": "tj-01", "server": "trojan.example.com", "server_port": 443, "password": "test"}, + {"type": "hysteria2", "tag": "hy2-01", "server": "hysteria.example.com", "server_port": 443, "password": "test"}, + {"type": "selector", "tag": "select", "outbounds": ["ss-01", "vl-01"]}, + {"type": "urltest", "tag": "auto", "outbounds": ["ss-01", "vl-01"]}, + {"type": "direct", "tag": "direct"}, + {"type": "dns", "tag": "dns"}, + {"type": "block", "tag": "block"} + ] + }' + + # Count proxy outbounds (exclude selector, urltest, direct, dns, block) + local proxy_count + local proxy_filter_file="/tmp/netshift-proxy-filter-$$.jq" + cat > "$proxy_filter_file" << 'JQEOF' +[.outbounds[] | select(.type != "selector" and .type != "urltest" and .type != "direct" and .type != "dns" and .type != "block")] | length +JQEOF + proxy_count=$(echo "$sub" | jq -f "$proxy_filter_file") + rm -f "$proxy_filter_file" + + if [ "$proxy_count" -eq 4 ]; then + pass "Subscription proxy count correct: $proxy_count (ss + vless + trojan + hysteria2)" + else + fail "Subscription proxy count wrong: expected 4, got $proxy_count" + fi + + # Test filtering for subscription outbound tags + local outbound_tags + local tags_filter_file="/tmp/netshift-tags-filter-$$.jq" + cat > "$tags_filter_file" << 'JQEOF' +[.outbounds[] | select(.type != "selector" and .type != "urltest" and .type != "direct" and .type != "dns" and .type != "block") | .tag] +JQEOF + outbound_tags=$(echo "$sub" | jq -c -f "$tags_filter_file") + rm -f "$tags_filter_file" + + if echo "$outbound_tags" | jq -e 'length == 4' > /dev/null 2>&1; then + pass "Subscription outbound tags extracted correctly" + else + fail "Subscription outbound tags extraction failed" + fi + + # Test country flag extraction from tags + # Build tags with actual Unicode regional indicator flags + local country_test + local flag_filter_file="/tmp/netshift-flag-filter-$$.jq" + cat > "$flag_filter_file" << 'JQEOF' +def flag($l1; $l2): ([127462 + $l1, 127462 + $l2] | implode); +[(flag(3; 4) + " Frankfurt"), (flag(20; 18) + " New York"), (flag(13; 11) + " Amsterdam"), (flag(9; 15) + " Tokyo"), "no-flag"] +JQEOF + country_test=$(jq -cn -f "$flag_filter_file") + rm -f "$flag_filter_file" + + local grouping + local group_filter_file="/tmp/netshift-group-filter-$$.jq" + cat > "$group_filter_file" << 'JQEOF' +def is_regional_indicator: . >= 127462 and . <= 127487; +def extract_country_flag: + (. | explode) as $codepoints + | if ($codepoints | length) >= 2 + and ($codepoints[0] | is_regional_indicator) + and ($codepoints[1] | is_regional_indicator) + then ($codepoints[0:2] | implode) + else "" end; +(if type == "array" then . else [] end) as $tags +| reduce $tags[] as $tag ( + {count: 0, ungrouped: 0}; + ($tag | extract_country_flag) as $flag + | if $flag == "" then .ungrouped += 1 else .count += 1 end + ) +JQEOF + grouping=$(echo "$country_test" | jq -c -f "$group_filter_file") + rm -f "$group_filter_file" + + local grouped + grouped=$(echo "$grouping" | jq -r '.count') + local ungrouped + ungrouped=$(echo "$grouping" | jq -r '.ungrouped') + + if [ "$grouped" -eq 4 ] && [ "$ungrouped" -eq 1 ]; then + pass "Country flag grouping: $grouped grouped, $ungrouped ungrouped" + else + fail "Country flag grouping wrong: got $grouped grouped, $ungrouped ungrouped" + fi +} + +# ───────────────────────────────────────────────────────────────── +# Main +# ───────────────────────────────────────────────────────────────── +main() { + printf "${BOLD}Netshift Evolution — Smoke Test Suite${NC}\n" + printf "Source: %s\n" "$NETSHIFT_SRC" + printf "OpenWrt: %s\n" "$(grep OPENWRT_RELEASE /etc/os-release 2>/dev/null | cut -d'"' -f2 || echo 'unknown')" + printf "Kernel: %s\n" "$(uname -r 2>/dev/null || echo 'unknown')" + printf "\n" + + local target="${1:-all}" + + case "$target" in + all) + test_deps + test_syntax + test_config + test_helpers + test_jq_helpers + test_config_manager + test_sing_box_config + test_nft + test_diagnostics + test_subscription + ;; + deps) test_deps ;; + syntax) test_syntax ;; + config) test_config ;; + helpers) test_helpers ;; + nft) test_nft ;; + diagnostics) test_diagnostics ;; + subscription) test_subscription ;; + jq) test_jq_helpers ;; + cm) test_config_manager ;; + sb) test_sing_box_config ;; + *) + echo "Unknown test: $target" + echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription" + exit 1 + ;; + esac + + summary +} + +main "$@" From 1ce10e8a71743cd0d2bae1582e92ecb8f55d435a Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 19:06:51 +0300 Subject: [PATCH 26/75] feat(subscription): fallback parser for base64 / plaintext key lists Many providers don't honor the sing-box User-Agent (or run legacy panels) and return a base64-encoded or plaintext list of proxy URIs instead of a sing-box JSON config. Previously such subscriptions failed validation (rc 13). Add normalize_subscription_to_singbox() (helpers.sh): when the downloaded body is not valid sing-box JSON, try to recover it: - detect & base64-decode a wrapped body (conservative: raw has no '://', decoded does; self-pads to len%4 for older coreutils-base64) - split into lines, skip blank and #comment/metadata lines - keep known schemes (vless/trojan/ss/hysteria2/hy2/socks4/4a/5) - build each URI into an outbound by reusing sing_box_cf_add_proxy_outbound, isolated in a subshell so one bad/unknown key can't abort the run - emit {"outbounds":[...]} so the existing validate + merge path is reused Hook in download_subscription_into_cache (bin/netshift): on validation failure, attempt the fallback before returning rc 13; covers both the update and startup download paths. Smoke tests (tests/entrypoint.sh): extend test_subscription with cases for plaintext+metadata, base64-wrapped, mixed-with-garbage (one bad key must not abort), and junk-only (must fall through). Verified in the OpenWrt rootfs container against real provider samples (base64 -> 10 outbounds; plaintext -> 43 vless), all validate OK. Suite: 44 passed / 0 failed. --- netshift/files/usr/bin/netshift | 18 +++- netshift/files/usr/lib/helpers.sh | 158 +++++++++++++++++++++++++++++ tests/entrypoint.sh | 159 ++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+), 5 deletions(-) diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 363249fd..f044b3b8 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -344,7 +344,7 @@ download_subscription_into_cache() { local subscription_json_path="$3" local subscription_url_cache_path="$4" local service_proxy_address="$5" - local tmpfile persist_tmpfile url_tmpfile rejected_cache_path tmp_hash rejected_hash validation_reason file_size + local tmpfile persist_tmpfile url_tmpfile rejected_cache_path tmp_hash rejected_hash validation_reason file_size fallback_tmp ensure_subscription_cache_dir || { log "Failed to prepare persistent subscription cache directory '$SUBSCRIPTION_CACHE_FOLDER' for section '$section'" "error" @@ -369,10 +369,18 @@ download_subscription_into_cache() { log "Downloaded subscription body for section '$section': bytes=${file_size:-unknown}" "debug" if ! validate_subscription_file "$tmpfile"; then - validation_reason="$(describe_subscription_validation_failure "$tmpfile")" - log "Downloaded subscription for section '$section' is invalid: ${validation_reason:-unknown validation error}" "error" - rm -f "$tmpfile" - return 13 + # Fallback: provider returned base64 / plaintext key list instead of sing-box JSON + fallback_tmp="${tmpfile}.fb" + if normalize_subscription_to_singbox "$tmpfile" "$fallback_tmp" "$section" && validate_subscription_file "$fallback_tmp"; then + mv -f "$fallback_tmp" "$tmpfile" + log "Subscription for section '$section' parsed via fallback (base64/plaintext key list)" "info" + else + rm -f "$fallback_tmp" + validation_reason="$(describe_subscription_validation_failure "$tmpfile")" + log "Downloaded subscription for section '$section' is invalid: ${validation_reason:-unknown validation error}" "error" + rm -f "$tmpfile" + return 13 + fi fi rejected_cache_path="$(get_subscription_rejected_cache_path "$section")" diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index 31d13dd7..531aae4c 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -966,3 +966,161 @@ describe_subscription_validation_failure() { )] | length' "$filepath" 2>/dev/null)" echo "subscription contains no usable proxy outbounds: total=${total:-unknown}, usable=${usable:-unknown}" } + +# Fallback subscription parser. +# +# Many providers do not return a sing-box JSON config. Instead they return +# either (a) a base64-encoded list of proxy URIs, or (b) a plaintext list of +# proxy URIs (one per line), possibly interspersed with '#comment' metadata +# lines. This function decodes/parses such a body into a minimal sing-box +# configuration ({"outbounds":[...]}) so the normal persist + merge path can +# consume it unchanged. +# +# It lives in helpers.sh (alongside validate_subscription_file). It calls +# sing_box_cf_add_proxy_outbound, which is defined later in +# sing_box_config_facade.sh. Shell resolves function names at call time, and +# bin/netshift sources both helpers.sh and the facade before any subscription +# work runs, so both the base64 helpers (defined here) and the URI->outbound +# builder are available when this function is invoked. +# +# Arguments: +# src_file: path to the raw downloaded subscription body +# out_file: path to write the normalized sing-box JSON to +# section: UCI section name (used to derive outbound tags) +# Returns: +# 0 and writes out_file when at least one outbound was parsed; 1 otherwise. +normalize_subscription_to_singbox() { + local src_file="$1" + local out_file="$2" + local section="$3" + + local raw stripped candidate pad_len decoded bom + local udp_over_tcp config new_config lines_file + local line scheme idx kept skipped before_count after_count final_count + + [ -s "$src_file" ] || return 1 + # Strip a leading UTF-8 BOM (EF BB BF) if present; it would otherwise break + # base64 charset detection and decoding. busybox sed lacks \x hex escapes, + # so build the BOM literally with printf octal escapes. + bom="$(printf '\357\273\277')" + raw="$(sed "1s/^${bom}//" "$src_file" 2>/dev/null)" + [ -n "$raw" ] || raw="$(cat "$src_file" 2>/dev/null)" + [ -n "$raw" ] || return 1 + + # Decide whether the body is a base64 blob or already plaintext URIs. + # Be conservative: only treat as base64 when the raw body has NO '://' + # substring (a plaintext URI list always contains '://') but the decoded + # body does contain '://'. + candidate="$raw" + case "$raw" in + *"://"*) + # Raw already contains URIs -> treat as plaintext. + : + ;; + *) + # Strip all whitespace and check the remaining charset is base64-only. + stripped="$(printf '%s' "$raw" | tr -d ' \t\r\n')" + if [ -n "$stripped" ] && [ -z "$(printf '%s' "$stripped" | tr -d 'A-Za-z0-9+/=')" ]; then + # Add '=' padding to a multiple of 4 (older coreutils-base64 lacks + # auto-padding). + pad_len=$(( ${#stripped} % 4 )) + if [ "$pad_len" -eq 2 ]; then + stripped="${stripped}==" + elif [ "$pad_len" -eq 3 ]; then + stripped="${stripped}=" + elif [ "$pad_len" -eq 1 ]; then + # Length 1 mod 4 is not valid base64; leave as-is and let + # decode fail. + : + fi + decoded="$(base64_decode "$stripped")" + case "$decoded" in + *"://"*) + candidate="$decoded" + ;; + esac + fi + ;; + esac + + # udp_over_tcp from the section if present, else empty. + udp_over_tcp="$(uci -q get "netshift.${section}.udp_over_tcp" 2>/dev/null)" + + config='{"outbounds":[]}' + idx=0 + kept=0 + skipped=0 + + # Write candidate lines to a temp file and feed the loop via redirect rather + # than a heredoc/pipe. The builder calls helpers that read stdin (e.g. + # base64 pipelines); feeding the loop from the same stdin would let them + # consume subsequent lines. A file redirect keeps the loop's stdin isolated. + lines_file="$(mktemp 2>/dev/null)" || lines_file="/tmp/netshift-sub-fb.$$" + printf '%s\n' "$candidate" > "$lines_file" + + while IFS= read -r line; do + # Trim leading/trailing whitespace. + line="$(printf '%s' "$line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" + [ -n "$line" ] || continue + # Skip metadata/comment lines. + case "$line" in + '#'*) + continue + ;; + esac + # Pre-filter: only attempt known schemes so an unknown scheme never + # reaches the builder's fatal path. + scheme="$(url_get_scheme "$line")" + case "$scheme" in + vless | trojan | ss | hysteria2 | hy2 | socks5 | socks4 | socks4a) ;; + *) + skipped=$(( skipped + 1 )) + continue + ;; + esac + + before_count="$(printf '%s' "$config" | jq -r '.outbounds | length' 2>/dev/null)" + [ -n "$before_count" ] || before_count=0 + + # Second guard: run the builder in a subshell (command substitution) so + # an unexpected exit 1 (e.g. malformed URI) is contained and surfaced as + # a non-zero rc. Redirect its stdin from /dev/null so its internal + # pipelines cannot consume the loop's input. + new_config="$(sing_box_cf_add_proxy_outbound "$config" "${section}-fb${idx}" "$line" "$udp_over_tcp" /dev/null)" || { + log "skip unparsable subscription key #$idx for '$section'" "debug" + idx=$(( idx + 1 )) + continue + } + idx=$(( idx + 1 )) + + # Validate the result parses as JSON and the outbound count increased. + if [ -z "$new_config" ] || ! printf '%s' "$new_config" | jq -e . >/dev/null 2>&1; then + log "skip subscription key (invalid JSON result) for '$section'" "debug" + continue + fi + after_count="$(printf '%s' "$new_config" | jq -r '.outbounds | length' 2>/dev/null)" + [ -n "$after_count" ] || after_count=0 + if [ "$after_count" -le "$before_count" ]; then + log "skip subscription key (no outbound added) for '$section'" "debug" + continue + fi + + config="$new_config" + kept=$(( kept + 1 )) + done < "$lines_file" + rm -f "$lines_file" + + if [ "$skipped" -gt 0 ]; then + log "Fallback subscription parser for '$section' skipped $skipped key(s) with unknown/unsupported schemes" "debug" + fi + + final_count="$(printf '%s' "$config" | jq -r '.outbounds | length' 2>/dev/null)" + [ -n "$final_count" ] || final_count=0 + log "Fallback subscription parser for '$section' produced $final_count outbound(s) from $kept accepted key(s)" "debug" + if [ "$final_count" -le 0 ]; then + return 1 + fi + + printf '%s' "$config" | jq '.' > "$out_file" 2>/dev/null || return 1 + return 0 +} diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 3c57a9fb..d062a7d0 100644 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -603,6 +603,165 @@ JQEOF else fail "Country flag grouping wrong: got $grouped grouped, $ungrouped ungrouped" fi + + # ── Fallback Subscription Normalizer (helpers.sh) ─────────────── + # Exercise normalize_subscription_to_singbox end-to-end against the + # real libs. The facade hardcodes NETSHIFT_LIB=/usr/lib/netshift, so we + # mirror test_jq_helpers and expose the bind-mounted libs there via + # symlinks, then source constants + logging + facade (the facade pulls in + # helpers.sh and the config manager). Tokens are emitted on stdout and + # parsed with the same name:OK/FAIL/SKIP convention used by test_helpers. + # NB: no `set -u` in the harness — the URI builders rely on optional unset + # query-param vars, exactly like the production backend. + printf "\n ${BOLD}Fallback Subscription Normalizer${NC}\n" + + local lib="${NETSHIFT_LIB_DIR}" + if [ ! -r "$lib/helpers.sh" ] || [ ! -r "$lib/sing_box_config_facade.sh" ]; then + skip "fallback normalizer (libs not found in $lib)" + return + fi + + local fb="/tmp/netshift-sub-fallback-$$.sh" + cat > "$fb" << 'FBEOF' +# Make the facade's hardcoded NETSHIFT_LIB path resolve to the bind-mounted libs. +mkdir -p /usr/lib/netshift +for f in constants.sh helpers.sh logging.sh sing_box_config_manager.sh sing_box_config_facade.sh; do + ln -sf "LIB_DIR/$f" "/usr/lib/netshift/$f" +done + +. /usr/lib/netshift/constants.sh +. /usr/lib/netshift/logging.sh +# The facade sources helpers.sh + sing_box_config_manager.sh itself. +. /usr/lib/netshift/sing_box_config_facade.sh + +# ── CASE A: plaintext URI list with comment/metadata lines ────────── +caseA_in="/tmp/netshift-fb-caseA-$$.txt" +caseA_out="/tmp/netshift-fb-caseA-out-$$.json" +cat > "$caseA_in" << 'LIST' +#profile-title: Test +#subscription-userinfo: upload=0 +vless://11111111-1111-1111-1111-111111111111@example.com:443?security=tls&sni=example.com&type=tcp#A +trojan://password123@example.com:8443?security=tls&sni=example.com#B +ss://YWVzLTI1Ni1nY206cGFzcw==@example.com:8388#C +hysteria2://pass@example.com:443?sni=example.com#D + +socks5://user:pass@example.com:1080#E +LIST + +if normalize_subscription_to_singbox "$caseA_in" "$caseA_out" "testsub"; then + echo 'fb-caseA-rc:OK' +else + echo 'fb-caseA-rc:FAIL' +fi +a_len="$(jq -r '.outbounds | length' "$caseA_out" 2>/dev/null)" +[ -n "$a_len" ] || a_len=0 +if [ "$a_len" -ge 4 ]; then + echo "fb-caseA-count(>=4 got $a_len):OK" +else + echo "fb-caseA-count(>=4 got $a_len):FAIL" +fi +if validate_subscription_file "$caseA_out"; then + echo 'fb-caseA-validate:OK' +else + echo 'fb-caseA-validate:FAIL' +fi +rm -f "$caseA_in" "$caseA_out" + +# ── CASE B: base64-wrapped URI list ───────────────────────────────── +# busybox base64 may lack -w0; encode then strip newlines with tr. +caseB_plain="vless://22222222-2222-2222-2222-222222222222@example.com:443?security=tls&sni=example.com&type=tcp#B1 +trojan://secretpw@example.com:8443?security=tls&sni=example.com#B2" +caseB_in="/tmp/netshift-fb-caseB-$$.txt" +caseB_out="/tmp/netshift-fb-caseB-out-$$.json" +printf '%s' "$caseB_plain" | base64 | tr -d '\n' > "$caseB_in" + +if normalize_subscription_to_singbox "$caseB_in" "$caseB_out" "testsub"; then + echo 'fb-caseB-rc:OK' +else + echo 'fb-caseB-rc:FAIL' +fi +b_len="$(jq -r '.outbounds | length' "$caseB_out" 2>/dev/null)" +[ -n "$b_len" ] || b_len=0 +if [ "$b_len" -ge 2 ]; then + echo "fb-caseB-count(>=2 got $b_len):OK" +else + echo "fb-caseB-count(>=2 got $b_len):FAIL" +fi +if validate_subscription_file "$caseB_out"; then + echo 'fb-caseB-validate:OK' +else + echo 'fb-caseB-validate:FAIL' +fi +rm -f "$caseB_in" "$caseB_out" + +# ── CASE C: robustness — valid keys mixed with garbage ────────────── +# Two valid known-scheme keys; an unknown scheme (vmess), a malformed line, +# a blank line and a comment must all be skipped without aborting the parse. +caseC_in="/tmp/netshift-fb-caseC-$$.txt" +caseC_out="/tmp/netshift-fb-caseC-out-$$.json" +cat > "$caseC_in" << 'LIST' +#header comment +vless://33333333-3333-3333-3333-333333333333@example.com:443?security=tls&sni=example.com&type=tcp#C1 +vmess://eyJ0aGlzIjoidW5rbm93biJ9 +not-a-uri + +trojan://pw3@example.com:8443?security=tls&sni=example.com#C2 +LIST + +if normalize_subscription_to_singbox "$caseC_in" "$caseC_out" "testsub"; then + echo 'fb-caseC-rc:OK' +else + echo 'fb-caseC-rc:FAIL' +fi +c_len="$(jq -r '.outbounds | length' "$caseC_out" 2>/dev/null)" +[ -n "$c_len" ] || c_len=0 +if [ "$c_len" -eq 2 ]; then + echo "fb-caseC-count(==2 valid got $c_len):OK" +else + echo "fb-caseC-count(==2 valid got $c_len):FAIL" +fi +rm -f "$caseC_in" "$caseC_out" + +# ── CASE D: negative — only comments / junk, no valid keys ────────── +caseD_in="/tmp/netshift-fb-caseD-$$.txt" +caseD_out="/tmp/netshift-fb-caseD-out-$$.json" +cat > "$caseD_in" << 'LIST' +#profile-title: Empty +#subscription-userinfo: upload=0 +not-a-uri +vmess://eyJqdW5rIjoidHJ1ZSJ9 + +LIST + +if normalize_subscription_to_singbox "$caseD_in" "$caseD_out" "testsub"; then + echo 'fb-caseD-rc-nonzero:FAIL' +else + echo 'fb-caseD-rc-nonzero:OK' +fi +# No usable output: either no file, or a file that fails validation. +if [ ! -s "$caseD_out" ] || ! validate_subscription_file "$caseD_out"; then + echo 'fb-caseD-no-usable-output:OK' +else + echo 'fb-caseD-no-usable-output:FAIL' +fi +rm -f "$caseD_in" "$caseD_out" + +echo 'DONE' +FBEOF + + sed -i "s|LIB_DIR|$lib|g" "$fb" + + ash "$fb" 2>/dev/null | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + *:SKIP) skip "$line" ;; + DONE) ;; + *) ;; + esac + done + + rm -f "$fb" } # ───────────────────────────────────────────────────────────────── From 07268e2b99911e7fb5fbfb8559c50fd03f98c9dc Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 19:36:27 +0300 Subject: [PATCH 27/75] fix(ci): make backend POSIX-clean for ShellCheck + executable smoke entrypoint CI failed on two independent issues after the rebrand: 1. Differential ShellCheck (severity=error): the git mv podkop->netshift made every lib file "new", surfacing pre-existing upstream dash-incompatibilities. Fixed all severity=error defects so `shellcheck --shell=ash` is clean: - SC2148: add `# shellcheck shell=ash` directive to the 7 sourced libs (constants/helpers/logging/nft/rulesets/sing_box_config_manager/facade) - SC3014: `[ x == y ]` -> `[ x = y ]` (bin/netshift + facade + logging) - SC3010: `[[ ... ]]` / `[[ =~ ]]` -> POSIX (grep -Eq / case / `! cmd`) in bin/netshift and helpers (is_ipv4, is_ipv4_cidr, is_domain, ...) - SC3060: `${input//,/...}` -> `sed 's/,/","/g'` - SC3028: `$RANDOM` in gen_id -> `/dev/urandom` (od-free, busybox-safe) - SC3003: `$'\r'` -> `"$(printf '\r')"` - SC3036: `echo -e` -> `printf '%b\n'` (logging.sh) 2. Smoke-test job: tests/entrypoint.sh lacked the executable bit (git mode 100644) -> "permission denied" in the container. Set mode 100755. README: drop stale podkop.net docs badge. Verified: shellcheck --severity=error --shell=ash clean (exit 0); smoke suite 44 passed / 0 failed; gen_id still yields 16-hex ids; behavior unchanged. --- README.md | 1 - netshift/files/usr/bin/netshift | 4 +-- netshift/files/usr/lib/constants.sh | 1 + netshift/files/usr/lib/helpers.sh | 27 +++++++++++-------- netshift/files/usr/lib/logging.sh | 5 ++-- netshift/files/usr/lib/nft.sh | 1 + netshift/files/usr/lib/rulesets.sh | 4 +-- .../files/usr/lib/sing_box_config_facade.sh | 9 ++++--- .../files/usr/lib/sing_box_config_manager.sh | 1 + tests/entrypoint.sh | 0 10 files changed, 31 insertions(+), 22 deletions(-) mode change 100644 => 100755 tests/entrypoint.sh diff --git a/README.md b/README.md index 96a0bae1..41397d72 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [![Release](https://img.shields.io/github/v/release/yandexru45/podkop-evolution?style=flat-square)](https://github.com/yandexru45/podkop-evolution/releases) [![License](https://img.shields.io/badge/license-GPL--2.0--or--later-blue?style=flat-square)](LICENSE) [![OpenWrt](https://img.shields.io/badge/OpenWrt-24.10%2B-orange?style=flat-square)](https://openwrt.org/) -[![Docs](https://img.shields.io/badge/docs-podkop.net-informational?style=flat-square)](https://podkop.net/) diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index f044b3b8..448d93dc 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -836,7 +836,7 @@ dnsmasq_configure() { current_servers="$(uci_get "dhcp" "@dnsmasq[0]" "server")" if [ -n "$current_servers" ]; then for server in $(uci_get "dhcp" "@dnsmasq[0]" "server"); do - if ! [ "$server" == "$SB_DNS_INBOUND_ADDRESS" ]; then + if ! [ "$server" = "$SB_DNS_INBOUND_ADDRESS" ]; then uci_add_list "dhcp" "@dnsmasq[0]" "netshift_server" "$server" fi done @@ -2655,7 +2655,7 @@ check_proxy() { if echo "$response" | grep -q "^ Date: Tue, 2 Jun 2026 20:14:56 +0300 Subject: [PATCH 28/75] chore: point all self-references to yandexru45/netshift Repo was renamed podkop-evolution -> netshift. Update every code/docs reference to the new repo so update checks, package downloads and the default-config fetch hit netshift directly (podkop-evolution stays only as a one-time compatibility bridge): - install.sh: REPO + rate-limit check + raw config URL + banners - bin/netshift: update-check API + issue URL - README badges/star-history/config URL; ISSUE_TEMPLATE links - realign install.sh banner frames after the shorter URL - add docs/icon.png --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- README.md | 42 ++++++++++++++------- docs/icon.png | Bin 0 -> 10540 bytes install.sh | 14 +++---- netshift/files/usr/bin/netshift | 4 +- 7 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 docs/icon.png diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e68028f6..56bbcc59 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,7 +11,7 @@ body: Спасибо за создание отчета об ошибке! Перед отправкой, пожалуйста: - - Проверьте [существующие issues](https://github.com/yandexru45/podkop-evolution/issues) + - Проверьте [существующие issues](https://github.com/yandexru45/netshift/issues) - Просмотрите [документацию](https://podkop.net) - type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 578d5bb3..f1d80ad4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: 💬 Если у вас что-то не работает, прежде всего прочитайте README проекта - url: https://github.com/yandexru45/podkop-evolution + url: https://github.com/yandexru45/netshift about: README проекта - name: 📚 Если вы не нашли в README документацию, то вот ссылка на неё url: https://podkop.net diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index b9196b95..62af07ef 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -11,7 +11,7 @@ body: Спасибо за предложение новой функции! Перед отправкой, пожалуйста: - - Проверьте [существующие запросы](https://github.com/yandexru45/podkop-evolution/issues?q=is%3Aissue+label%3Aenhancement) + - Проверьте [существующие запросы](https://github.com/yandexru45/netshift/issues?q=is%3Aissue+label%3Aenhancement) - Убедитесь, что функции не существует в [документации](https://podkop.net) - type: textarea diff --git a/README.md b/README.md index 41397d72..63aca242 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,24 @@ # NetShift -[![Release](https://img.shields.io/github/v/release/yandexru45/podkop-evolution?style=flat-square)](https://github.com/yandexru45/podkop-evolution/releases) -[![License](https://img.shields.io/badge/license-GPL--2.0--or--later-blue?style=flat-square)](LICENSE) -[![OpenWrt](https://img.shields.io/badge/OpenWrt-24.10%2B-orange?style=flat-square)](https://openwrt.org/) - +

+ Clash +
+
+ + + +

+

Sing-box client for Openwrt

--- +

-

- -NetShift в LuCI +[![Static Badge](https://img.shields.io/badge/Telegram-Channel-Link?style=for-the-badge&logo=Telegram&logoColor=white&logoSize=auto&color=blue)](https://t.me/netshift_news) -
+[![Static Badge](https://img.shields.io/badge/Telegram-Chat-yes?style=for-the-badge&logo=Telegram&logoColor=white&logoSize=auto&color=blue)](https://t.me/netshift_chat) +

--- @@ -35,6 +40,17 @@ - [x] **Веб-интерфейс LuCI** - дашборд, диагностика и настройки без ручной правки конфигов
статус серверов · проверка соединения · логи - [x] **Автоматическая миграция** - обновление со старого podkop переносит конфиг без перенастройки + +--- + +
+ +NetShift в LuCI + +
+ +--- + ## Вещи, которые необходимо знать перед установкой
@@ -81,7 +97,7 @@ ```sh mv /etc/config/netshift /etc/config/netshift-070 -wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/netshift/files/etc/config/netshift +wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/netshift/refs/heads/main/netshift/files/etc/config/netshift # затем настроить заново через LuCI или UCI ``` @@ -196,11 +212,11 @@ docker build -f Dockerfile-apk --build-arg NETSHIFT_VERSION=0.8.0 -t netshift:ap ## Star History - + - - - Star History Chart + + + Star History Chart diff --git a/docs/icon.png b/docs/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..798633eca660449d01272ca4ef1ad444b91a6d13 GIT binary patch literal 10540 zcmeHsbzD?kxA2)67zP+%XrxOzh7M_IBqaoqhZv-#C6opkK%}HQR1h!#gHY)ZBt^kM zK#)|BMnLWvP~Yb&?|bj}`|kbSf8H~jS^KPBYp=CupTqvh{y2crR@YJoAP@+k34Vb6 z&p@n!qq~p0x1+lUtC$cPkXF&s#lr(|*9Ar>4`8dVqN|FwL8H+&k~U}&5TZpeB4Riw zWiMifgCP0C;&hKy#2SpNdS(A z568!Y!|~w+1aJfqiin7ikcf(u42hzpqNAgwqM@N@I?h7Rz`;mE!z#ea!NtwX$9s%L z5G}}qKF-6-gHr+_ARr(@5K$5lQS#8!(DVF1U;E7fIRZKk9fv{K0Vp{HMh@9;1sK4z z0We(ZeqAsqJ{*Ea01~PGjy|OCzX6ai2mnRFPyhhg9r-2o@5%pN24Dh?rvONrL?VE^ zUx@xODPslTXI?pk9_1EQE@3FZ&({gybE{ZGt13bPQxi@As3>PWiW{QUp8*_j8ToJ> zirE_}0YuNMj^GyUNiNckIsL9LS6Kq@C3E*&5(p@ZqI*k|i@O$`ozJ?HoOYHl88H~* zpEzKcZA=b#CM=7={;q6&{J=fXP6_}qH)tAPAAcW|&oTnKQuZ7G_+?Km|1N^N2dS(8 zzP&jBl+2z2ENPZr>}ZdZYSC!4W+_ zEk!W8R16v@R=8C1393>?H1S8oU`J{7XJ#&){G>#c%LNeB_D0H#WlxG1l+2+4hIobX zolt;g`r9?jj6b~rz+@Q(kxBy4l#)ik$cH%mdJ3|_odYk+3Pq^v8Xi|?Qj}WHw*Sgm zu<@5V-d?B6gD2d#Bp&XpD%fAjQ~N@<}SPdtU}O^-aoSgN%NxCG-0#+9>`q6mw5Z6=asti z!?zhsOWo|24c6I=qld&}K|-a1b7Yhe)u|vlP7^lcD32r7>Y&phT41Gvbqi3ZV*a)X z91-7vO9xzFY9QvOO5nbb41il;;t|Z?HnJiD60NqtOm?;crslSwc5njGnm(d|kJRvQ z8T3qxGu?*fK}~wJuB^Y#DAZ#g0YCD;xQ;x zi6R1l5t;TsZY=aI{&6GDobAkT@L`hDdqg(i5hz5a)-?NAo|d3!Tqvel7bkwGg^lR= zPkn_dVM*yc*C%9HWr8>z3@b!PL)}wG&a57CvPyS5$_7_3uXoAp+N#Wb` zIh18i<3V5|>zs63y^Ov@bX2zLts9@&XAD1^IZ>ynQrgLKCqb4K#jstn zai0qhM|T9s9Bwi9>B;BiT#}dG2=wT#l+Jw!|DY%N&GXSv{L>t_wTXurmBthge&|+` zmr=8uzWH)8IoS>3myAt&iSxaGs?w?LYKRFiKF_(63264p$}KH zzR9e8GQn!=YCBcgE0_iFwUj$oEyr~~`pEV4T98btty)>t06GJVC5$fEzq z5^LUbJ-KxkFKY8Qq`I?Q(U)`+#F^0z0?i1BJ&Xa_8y?3`k<**~G-2lC``n1UVSLO3 z{b@z+c7H(S;7S$=~`l?OyGL7?m`VjkOLYFka6QPJUH znBrS%T4)jFOkHYPFf{LEGH*gx+NMr%-rgTt#2lC*DH={KJ~`#T4tn z#Yb zP2fBt%CSfvA_nclw!wYljn1h@lp5upgY$}L#hzm0{=o&Rg3 zOWUKMBP$`HSl@%qg!hfh=3#PKSGrBmdju-N9CTxEF%YeTon~ z{eQs${ly`M+hHO21b9#g9_(;Chd^0HJYi^MgCrC=1)FGCp_qz|7p0*syS3fD@cR=# zwp+L&=!8~Ui;rNrhTWJ@w zryfTC0h=(@K}4L^ ziSw!_+>d;j5ph8Ii=01`%ztxM{u^0v#D{{-S=vVJDa*|SozAs}e|&|%x1VBJ%fTKh zW2{yk$l+x~dA1UNLX>4vXMO{~=yH&|J*+s#3$^0n3!IfR3)y0{hI$jx*&TwdI_6P~M;sCYt`z zuA9y?c3Hh(CO4HpmGWG!gldPcGHeaCN$sumo=pjp9&djNWQWjy(^HT=NIT%q99OgcfS zG5Q)4V{3e_dqX;9&`*?;MQSyHF#4AeZICmoAZTsVvDNlYU z2dCo2u{7^l85T169G5wUK6M)R&_-)C;rFVrdCtHX<|2*KB-{i~TX5&8hJMn}l+QMk z!oh?iVtwa4?Cag{9Q%Me{Z$D%XY@;(Qt6lOR+D@QlLlwebV#w*59UF_)EZ!MF9_e5 zoL%W13bJ2oILB=M#tAn$4c6v*`rucvC&ZsteY4`7&Kd{B;4mN~EVM=WN(2d<9x0BB z$Ky~&^sD@wunyNLo;;w?IikVQZ&Pr<3oWod6gD^jkSoqve{rS9;`&aP;jsVyB@Dr& ze}jjBy8=H7d^-fk2TxVtE24v*hXCZPY$7&=hMr0Hmr-Erf3*&~*EBKk^Jt~W1|F?= ztzmJWD}q7=&bhH#zaAk5UXR$!EUQ(q+`D+e;qwk^r<``<E?y-0ge2!M1^sgk2ic zVhaUO_gMYO5I~uFq$agRxruY7CYO!y#C{W5l9!3_iiN3yt}r3~tjVC$Up{0ZtFQo^5jN1241p z0VDT4O%H-ZwbO%Te95@MTN8t@818Yop3W;|WLZ+HMI(v#G85g1%KFy5AY1`fkJkOj zu)jQ%Y!Xt!;i;u_{M;SW2GaLWPO^W|@7V_+ahuxH&+6{a3t4D~bnmRErfj(2>wZ*K z#tWy@xK_QY*l4@AQtSSuB9NG?lgQyaLvmGc7c`sg$|;vu!46x2?hVJX0)y7j zs~qUXi>CUcK4R9-(rToA@-x4~XqQsELwp43vQcn&MySzZMSj&ho>sVFtFmY-?`ZzG zNbt7hqfI5sxP3sGE-gLuJ{e&&G{Zw^R9cv1o9$X7e&G)tzRtlsU4k~Z%w(A{?N=?U z{5p~C1V8Op?xZ|2!n6WfqP7c|MOEuiS2SJT58b9xUv#2fq^S}SG>&!VClooABLyQ> z#3UcGO)i1aKgUJ?%UBgU=D~17C>1o|`wq8*vOqJ6{ zomgi5AH?21s)wrhkXK@Rx%ufvoQNz=ObSmx$6e_V$H=yW{A9i;>!fA14D-V8q_!!# z23IvXE09ZL6(_8gUQ~{uBj|3Ub`eslg;v^IJb#eFbVgm7h9UyRH(z(BIAQ`bTP1Ez z&w7joQ$Fm)6$9S-n9G9VdRP_`N`G{Q=}_Ep)900`XZaqQEM9Zlh&#KfTaV;=Do@JO zbM48+5BL35Fc>6Vu{gb2$xi**&5jFwXdPq+#@cSkZAZ$Y)F~*5*oGqL(}QRJ7yb3` zLY489VpwT;+n-2&sPasEqra)zbYsb+a;K$wBM389*NJT$f0$pru*+_%j(?5r(zHwP z?BZTrV(HXA@WzJF2_6x*5ro)c^X2&1j@NeqYhW&0@x)BTvdx>i$C!8ove>r|5c8bU z+`?k9kv7MiG0s)K=CXI#V_!{NIo~;sJr9r4Q8wx~uAvsh*HI?^e*YvF+;K=LfL9>b zjLkC2iwcm)Dtg^s&TW!%yK5LTUm9+3=y$bq`6(NU;b}_; z>SF5^xfesorFC6N@~2Hkw-xQbG{qO@4|mc6mJkp8Mq1#cD^XgM7FCiKRUN8XJT!ar z$y$2aqEsIKxt)%HDMVR^7CaqfIG0ER{P}s zJ^s%#P~FFKx=&!WQEoLsZiVdV6g09H?X^LGG*mpGwdzByuZl zIDCXY&rl!TYVYqn%eZ2Y_U;ovcgwBDAU()_XTaHgF1ONw^K8eG967)a#2^`%cDS({bSEl9>yGP$AO^$4u? zGK$6vpcBA45fJI)L4zyxI}eBf>x2i48YHdg|H&ZTLWXXJqnodx?U3T2p%&!tJj*Re zjy~yoEy&eIBsv1QauVr%gcV#Jf+qeyDpn3G(UoXi#wc;ccCg$>89FM)-+lf!&;L{g z{>7UC+`9wRfBEj)`eD8X5!7@AJYHUh3ghl8RvlmwDjWBV; zrONB?T~fTslZ*naIi}yW^coVLP@C{e@U6R(^v#rRhQ)U>Dhuir7M)t=_Lzj5ZWo^R@>+tcOY zQ+%5y{$(i}rS8-?_mAns6@s*I~6 zHqO?M9Q-9O6S1|3>3H>c!n65fR0r`j6a;hf6yp2 zI~Zln{1_2edvC~1GOku}Hazz>%6WN9g7<~Cx6!j81K+o;7&CHZ7QTIeY_+*IA+Jb) z9hYybv;J3ON!3*pBnBd4ysl2plriA)8sIkBQ?lHs(OxI^8(NefTaXa%<3K<$eXOm| zNOA#O%aymSy~F}ODs3sVK6-@%(82-PXw~*;HKqI#;VxJOq_XE-rm!B;8_e#R)JFhm35Xf~Y$Ax!61QOS<-&EzN5`#@`>9N+jnSd;lFzKU!3x=8~{$s)4Emoa8hZ&+*1ROk{Z)49&a zVs9Qthcq|JX&>@N+>!~!YT`92&}?_!C9YAnUV5a8M;>-8%088)VC>n1HS=)~U$8`< zONNgSa1Q8*+*>h(tNEWRUVXc3gB@>WRHou#BM*T;8rns|TJMnFsj*hkK^#k$=7NxW zHU<@OChMd~i}E*rW=a1rWiE=DR^c%f=7O6f2D=`zPu{wjc%p7L2BCGl3q55RPu|34 zzmR@M6?Nu{R7Cd@tOl~;8gI(e-Vu{@awTlijKIvC649-ZNRBa_VUHQ1CUJI0Z3cGG zw8$`#R^-Fr2|}{x1TmIO=Uh??u;(9VDOmdCSaq5`q;VsCsi^RmKa>lD z*wQ6JHcqyk_^x!2j^my+}hyL=%7sQsw6j5Y+ z8W7ziNdq#QS9mlp1L&B$NrCJLP8b}|;5+Mq2LiL=eNeta))J{fZ0j}qBo!q%R=cDf zSD%MNEvHQ6lsW1qQdzfy!LB*9Zqxotr&58k(?AMUo5-otRIcs8RHm=T(h_or@bOQ&`imV*(a#q zVsU>If{;$D&bzIzUzO|e8--)Q2X>cpdsL>U3*TrOS*tc`UX2yo%6(sTS&>7$5tdDE z=p&#E<H$>!XMOF8_ce}?Z3^++yi>e{xm=pyrKew%AP1f$rc~@-C=wR~)hRRqm z`ERckl~GwUvsm2#rtf+>H0sJ3{M0m%7de_Dn{Hd!cQ4@~Dn1kAP-W%Sx{+eB50&0) z}%*A>5{ekR(8x}=f=rj0i$;ux~=PTJ=SuRdRRDKA?^ z^#-`_RoiAtAL4SI`U#aG5xI|+U1QNH^2dth*&$|<-|x8XS-$;N`)D1C@T3z_EKO{E zm+`5E%HNI@G5$T(PWSA9>6*zCD!--o2|L|0L#E=x@e;F+kD(Chapk!pmo=&? zdO>ju$C?3Kry}{_xkynO@Fe$9mqv2H&JFx>Dw3-Re#xEQUCbJKAKrRVgT}m;yR1zA zv{By88}ho^fmzqRh2KobglmyyD#2U1pgOuVS%TumFu4jt)G6;ROjinNDHpj^NQ2Rd zJMu|em9t7`q!vRR6SS%nITsCE>pC;e)bC9uz1N)ySEPP|AUIv)e?m8Do7=Y9Z$V60 zf{;|Xo0Q&xB_IY zMa>Zp(w1-!x`kd%eX7HMIn8uipmN3h>{QjGE{rw-euld9SXP1fu+7cNxw|J##ryqj z0!YYZIO8Q2TwcAdynEK_VR$I~4D7b|UGgOLahC@yS!2vZSZ!ef@Mw{6c6y9M%APxo ze0$bX4V4(zzQnv>_iEb@+Y?PDz_Hr@IF^l#HhxV0RY&E@5_ksLwqePOYBeN8G+Y3W z8uISx;P2;n*?-1G`%g)+vE+?0vtu>YKKfnF-f%U{w}566lMYaMh=js4dqzX=MPA9! UF?N$~ZWq;8)(biKKhWrZ0LD#5OaK4? literal 0 HcmV?d00001 diff --git a/install.sh b/install.sh index 0580f410..1a72c9b0 100755 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ #!/bin/sh # shellcheck shell=dash -REPO="https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest" +REPO="https://api.github.com/repos/yandexru45/netshift/releases/latest" DOWNLOAD_DIR="/tmp/netshift" COUNT=3 @@ -66,7 +66,7 @@ update_config() { printf "\033[48;5;196m\033[1m║ ! Обнаружена старая версия NetShift. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Если продолжите обновление, вам потребуется настроить NetShift заново.║\033[0m\n" printf "\033[48;5;196m\033[1m║ Старая конфигурация будет сохранена в /etc/config/netshift-070 ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/netshift ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -76,7 +76,7 @@ update_config() { printf "\033[48;5;196m\033[1m║ ! Detected old NetShift version. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ If you continue the update, you will need to RECONFIGURE NetShift. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Your old configuration will be saved to /etc/config/netshift-070 ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/netshift ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -88,7 +88,7 @@ update_config() { yes|y|Y) mv /etc/config/netshift /etc/config/netshift-070 - wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/podkop-evolution/refs/heads/main/netshift/files/etc/config/netshift + wget -O /etc/config/netshift https://raw.githubusercontent.com/yandexru45/netshift/refs/heads/main/netshift/files/etc/config/netshift msg "NetShift config has been reset to default. Your old config saved in /etc/config/netshift-070" break ;; @@ -129,7 +129,7 @@ migrate_from_podkop() { printf "\033[48;5;196m\033[1m║ Ваша конфигурация будет перенесена автоматически. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Старая конфигурация сохранится в /etc/config/podkop.bak.pre-netshift║\033[0m\n" printf "\033[48;5;196m\033[1m║ Старый пакет podkop будет удалён, NetShift будет установлен. ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Подробности: https://github.com/yandexru45/netshift ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Точно хотите продолжить? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -140,7 +140,7 @@ migrate_from_podkop() { printf "\033[48;5;196m\033[1m║ Your configuration will be carried over automatically. ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Old config will be backed up to /etc/config/podkop.bak.pre-netshift ║\033[0m\n" printf "\033[48;5;196m\033[1m║ The old podkop package will be removed, NetShift installed. ║\033[0m\n" - printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/podkop-evolution ║\033[0m\n" + printf "\033[48;5;196m\033[1m║ Details: https://github.com/yandexru45/netshift ║\033[0m\n" printf "\033[48;5;196m\033[1m║ Are you sure you want to continue? ║\033[0m\n" printf "\033[48;5;196m\033[1m╚══════════════════════════════════════════════════════════════════════╝\033[0m\n" @@ -247,7 +247,7 @@ main() { fi if command -v curl >/dev/null 2>&1; then - check_response=$(curl -s "https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest") + check_response=$(curl -s "https://api.github.com/repos/yandexru45/netshift/releases/latest") if echo "$check_response" | grep -q 'API rate limit '; then msg "You've reached the GitHub rate limit. Repeat in five minutes." diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 448d93dc..ad18b70c 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -2581,7 +2581,7 @@ get_service_listen_address() { network_get_ipaddr service_listen_address "$interface" if [ -z "$service_listen_address" ]; then - log "Failed to determine the listening IP address. Please open an issue to report this problem: https://github.com/yandexru45/podkop-evolution/issues" "error" + log "Failed to determine the listening IP address. Please open an issue to report this problem: https://github.com/yandexru45/netshift/issues" "error" return 1 fi @@ -2854,7 +2854,7 @@ get_system_info() { netshift_version="$NETSHIFT_VERSION" - netshift_latest_version=$(curl -m 3 -s https://api.github.com/repos/yandexru45/podkop-evolution/releases/latest | grep '"tag_name":' | cut -d'"' -f4) + netshift_latest_version=$(curl -m 3 -s https://api.github.com/repos/yandexru45/netshift/releases/latest | grep '"tag_name":' | cut -d'"' -f4) [ -z "$netshift_latest_version" ] && netshift_latest_version="unknown" if [ -f /www/luci-static/resources/view/netshift/main.js ]; then From abcd0714260f81490917a414ac39ce937d201bdc Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 20:19:48 +0300 Subject: [PATCH 29/75] docs: render Telegram badges horizontally (HTML in centered

) --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 63aca242..1f0f5d81 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,8 @@ ---

- -[![Static Badge](https://img.shields.io/badge/Telegram-Channel-Link?style=for-the-badge&logo=Telegram&logoColor=white&logoSize=auto&color=blue)](https://t.me/netshift_news) - -[![Static Badge](https://img.shields.io/badge/Telegram-Chat-yes?style=for-the-badge&logo=Telegram&logoColor=white&logoSize=auto&color=blue)](https://t.me/netshift_chat) + Telegram Channel + Telegram Chat

--- From d22a93d0e1c669d1a0bcaa0d94d0b84ca9062182 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 21:31:58 +0300 Subject: [PATCH 30/75] =?UTF-8?q?=D1=83=D1=81=D0=BA=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../files/usr/lib/sing_box_config_facade.sh | 363 ++++++++++++------ 1 file changed, 252 insertions(+), 111 deletions(-) diff --git a/netshift/files/usr/lib/sing_box_config_facade.sh b/netshift/files/usr/lib/sing_box_config_facade.sh index 1b73e98b..06d74979 100644 --- a/netshift/files/usr/lib/sing_box_config_facade.sh +++ b/netshift/files/usr/lib/sing_box_config_facade.sh @@ -355,10 +355,195 @@ sing_box_cf_add_single_key_reject_rule() { echo "$config" } +####################################### +# Build a prepared subscription batch in a SINGLE jq pass. +# Filters out non-proxy types (selector, urltest, direct, dns, block), statically +# drops outbounds unsupported by the current sing-box build (shadowsocks+tls and, +# unless running sing-box-extended, xhttp transport), assigns a unique tag to each +# outbound (deduplicating against tags already present in $config and against tags +# chosen earlier in the same batch) and records a human-readable display name. +# Arguments: +# config: string (JSON), sing-box configuration the batch will be merged into +# subscription_json_path: string, path to the downloaded subscription JSON file +# Outputs: +# Writes a JSON object to stdout: +# { outbounds: [ {type,...,tag} ... ], tags: [..], names: [..], +# count: , skipped: } +####################################### +sing_box_cf_prepare_subscription_batch() { + local config="$1" + local subscription_json_path="$2" + local sing_box_extended="false" + + if is_sing_box_extended; then + sing_box_extended="true" + fi + + # The working config is fed on stdin (POSIX-safe, no process substitution); + # the subscription JSON is slurped from its file path. + printf '%s' "$config" | jq -c \ + --slurpfile sub "$subscription_json_path" \ + --argjson extended "$sing_box_extended" ' + # Reserved tags already used by the working config (stdin is the config). + ([.outbounds[]?.tag // empty]) as $existing + # Candidate proxy outbounds from the subscription (preserve order). + | [$sub[0].outbounds[]? | select( + .type != "selector" and + .type != "urltest" and + .type != "direct" and + .type != "dns" and + .type != "block" + )] as $candidates + | ($candidates | length) as $total + # Statically reject outbounds the current sing-box build cannot load. + | [ $candidates[] + | . as $ob + | (($ob.remark // $ob.tag // "") | tostring) as $name + | if ($ob.type == "shadowsocks" and ($ob.tls.enabled == true)) then + empty + elif (($ob.transport.type // "") == "xhttp" and ($extended | not)) then + empty + else + {ob: $ob, name: $name} + end + ] as $kept + # Assign unique tags using a deterministic dedup pass. $state.used is a + # set (object) of tags already taken, seeded with the existing config tags. + | reduce range(0; ($kept | length)) as $i ( + {used: ($existing | map({(.): true}) | add // {}), out: []}; + . as $state + | $kept[$i] as $entry + | ($entry.ob) as $ob + | (($ob.tag // $ob.remark // "") | tostring) as $raw + | (if ($raw | length) > 0 then $raw else ("server-" + (($i + 1) | tostring)) end) as $base + # Pick $base if free, else the first $base-N (N>=1) that is not taken. + | ( + if ($state.used[$base] | not) then $base + else + (label $found + | (range(1; 1000001) + | ($base + "-" + (. | tostring)) as $cand + | if ($state.used[$cand] | not) then $cand, break $found else empty end)) + end + ) as $tag + | .used[$tag] = true + | .out += [{ + tag: $tag, + name: (if ($entry.name | length) > 0 then $entry.name else $tag end), + outbound: ($ob | del(.tag) | del(.remark) | . + {tag: $tag}) + }] + ) as $resolved + | { + outbounds: [$resolved.out[].outbound], + tags: [$resolved.out[].tag], + names: [$resolved.out[].name], + count: ($resolved.out | length), + skipped: ($total - ($resolved.out | length)) + } + ' 2>/dev/null +} + +####################################### +# Try to append a slice of prepared outbounds to the config and validate it once +# with a single `sing-box check`. On success the validated config (including the +# appended outbounds) is exposed via SING_BOX_CF_TRY_CONFIG. +# Arguments: +# config: string (JSON), base configuration to append to +# outbounds_json: string (JSON array), outbound objects to append (already tagged) +# Returns: +# 0 on success (SING_BOX_CF_TRY_CONFIG set), non-zero on validation failure +####################################### +sing_box_cf_try_subscription_batch() { + local config="$1" + local outbounds_json="$2" + local updated_config validation_tmp + + SING_BOX_CF_TRY_CONFIG="" + + updated_config=$(printf '%s' "$config" | jq -c --argjson new "$outbounds_json" '.outbounds += $new' 2>/dev/null) + if [ -z "$updated_config" ]; then + return 1 + fi + + validation_tmp="$(mktemp)" || return 1 + sing_box_cm_save_config_to_file "$updated_config" "$validation_tmp" + if ! sing-box -c "$validation_tmp" check > /dev/null 2>&1; then + rm -f "$validation_tmp" + return 1 + fi + rm -f "$validation_tmp" + + SING_BOX_CF_TRY_CONFIG="$updated_config" + return 0 +} + +####################################### +# Recursively validate a range of prepared outbounds, isolating and skipping the +# ones the current sing-box build rejects. Mirrors podkop-plus' bisection design: +# a clean range costs a single `sing-box check`; only ranges containing a bad +# outbound are split further (groups <= 8 are probed one-by-one). +# Reads/updates the SING_BOX_CF_BATCH_* globals set up by the caller. +# Arguments: +# start: integer, first index (0-based) into SING_BOX_CF_BATCH_OUTBOUNDS +# count: integer, number of outbounds in the range +####################################### +sing_box_cf_apply_subscription_range() { + local start="$1" + local count="$2" + local slice display_name half rest index + + [ "$count" -gt 0 ] || return 0 + + slice=$(printf '%s' "$SING_BOX_CF_BATCH_OUTBOUNDS" | + jq -c --argjson start "$start" --argjson count "$count" '.[$start:($start + $count)]' 2>/dev/null) + if [ -z "$slice" ] || [ "$slice" = "[]" ]; then + SING_BOX_CF_BATCH_SKIPPED=$((SING_BOX_CF_BATCH_SKIPPED + count)) + return 0 + fi + + if sing_box_cf_try_subscription_batch "$SING_BOX_CF_BATCH_CONFIG" "$slice"; then + SING_BOX_CF_BATCH_CONFIG="$SING_BOX_CF_TRY_CONFIG" + SING_BOX_CF_BATCH_KEPT=$((SING_BOX_CF_BATCH_KEPT + count)) + SING_BOX_CF_BATCH_KEPT_RANGES="$SING_BOX_CF_BATCH_KEPT_RANGES $start:$count" + return 0 + fi + + if [ "$count" -eq 1 ]; then + display_name=$(printf '%s' "$SING_BOX_CF_BATCH_NAMES" | + jq -r --argjson start "$start" '.[$start] // "unknown"' 2>/dev/null) + [ -n "$display_name" ] || display_name="unknown" + log "Skip unsupported outbound for current sing-box: '$display_name'" "warn" + SING_BOX_CF_BATCH_SKIPPED=$((SING_BOX_CF_BATCH_SKIPPED + 1)) + return 0 + fi + + if [ "$count" -le 8 ]; then + index=0 + while [ "$index" -lt "$count" ]; do + sing_box_cf_apply_subscription_range $((start + index)) 1 + index=$((index + 1)) + done + return 0 + fi + + half=$((count / 2)) + [ "$half" -gt 0 ] || half=1 + rest=$((count - half)) + sing_box_cf_apply_subscription_range "$start" "$half" + sing_box_cf_apply_subscription_range $((start + half)) "$rest" +} + ####################################### # Parse a sing-box subscription JSON and add all proxy outbounds to the configuration. # Filters out non-proxy types (selector, urltest, direct, dns, block). # Uses 'tag' field (or 'remark' if present) as display name for each outbound. +# +# Validation strategy: build every outbound in a single jq pass, then validate the +# whole batch with one `sing-box check`. If that passes (the common case) the run +# costs O(1) sing-box invocations instead of O(n). If it fails, recursively bisect +# the batch to isolate and skip only the outbounds the current sing-box build +# cannot load, preserving the previous "skip unsupported outbound" behaviour. A +# final full-config validation still happens later in sing_box_save_config(). # Arguments: # config: string (JSON), sing-box configuration to modify # section: string, the UCI section name @@ -385,131 +570,87 @@ sing_box_cf_add_subscription_outbounds() { return 1 fi - # Extract proxy outbounds from subscription JSON - # Filter out non-proxy types: selector, urltest, direct, dns, block - local outbounds_count - outbounds_count=$(jq -r '[.outbounds[] | select( - .type != "selector" and - .type != "urltest" and - .type != "direct" and - .type != "dns" and - .type != "block" - )] | length' "$subscription_json_path" 2>/dev/null) - - if [ -z "$outbounds_count" ] || [ "$outbounds_count" -eq 0 ]; then - log "No proxy outbounds found in subscription JSON" "error" + # Build the entire batch (filter + dedup tags) in one jq pass. + local prepared + prepared=$(sing_box_cf_prepare_subscription_batch "$config" "$subscription_json_path") + if [ -z "$prepared" ]; then + log "Failed to parse subscription outbounds JSON" "error" echo "$config" return 1 fi - log "Found $outbounds_count proxy outbounds in subscription" "info" - - local i=1 - local added_count=0 - local outbound_json display_name outbound_tag outbound_type outbound_tls_enabled preferred_tag base_tag tag_suffix - - while [ "$i" -le "$outbounds_count" ]; do - # Extract the i-th proxy outbound as raw JSON - outbound_json=$(jq -c "[.outbounds[] | select( - .type != \"selector\" and - .type != \"urltest\" and - .type != \"direct\" and - .type != \"dns\" and - .type != \"block\" - )][$i - 1]" "$subscription_json_path" 2>/dev/null) - - if [ -z "$outbound_json" ] || [ "$outbound_json" = "null" ]; then - i=$((i + 1)) - continue - fi - - # Get display name: prefer remark, then tag, then fallback - display_name=$(echo "$outbound_json" | jq -r '.remark // .tag // "server-'"$i"'"' 2>/dev/null) - - outbound_type=$(echo "$outbound_json" | jq -r '.type // ""' 2>/dev/null) - outbound_tls_enabled=$(echo "$outbound_json" | jq -r '.tls.enabled // false' 2>/dev/null) + local candidate_total kept_count statically_skipped + candidate_total=$(printf '%s' "$prepared" | jq -r '(.count // 0) + (.skipped // 0)' 2>/dev/null) + kept_count=$(printf '%s' "$prepared" | jq -r '.count // 0' 2>/dev/null) + statically_skipped=$(printf '%s' "$prepared" | jq -r '.skipped // 0' 2>/dev/null) - # sing-box does not support top-level tls field for shadowsocks outbound. - if [ "$outbound_type" = "shadowsocks" ] && [ "$outbound_tls_enabled" = "true" ]; then - log "Skip unsupported Shadowsocks outbound with tls: '$display_name'" "warn" - i=$((i + 1)) - continue - fi - - # XHTTP transport requires sing-box-extended; skip such outbounds otherwise. - local outbound_transport_type - outbound_transport_type=$(echo "$outbound_json" | jq -r '.transport.type // ""' 2>/dev/null) - if [ "$outbound_transport_type" = "xhttp" ] && ! is_sing_box_extended; then - log "Skip unsupported XHTTP outbound (requires sing-box-extended): '$display_name'" "warn" - i=$((i + 1)) - continue - fi - - # Keep original tag from the subscription for dashboard readability. - preferred_tag=$(echo "$outbound_json" | jq -r '.tag // .remark // "server-'"$i"'"' 2>/dev/null) - if [ -z "$preferred_tag" ] || [ "$preferred_tag" = "null" ]; then - preferred_tag="server-$i" - fi - - base_tag="$preferred_tag" - outbound_tag="$base_tag" - tag_suffix=1 - while printf '%s' "$config" | jq -e --arg tag "$outbound_tag" '.outbounds[]? | select(.tag == $tag)' > /dev/null 2>&1; do - outbound_tag="${base_tag}-$tag_suffix" - tag_suffix=$((tag_suffix + 1)) - done + if [ -z "$candidate_total" ] || [ "$candidate_total" -eq 0 ]; then + log "No proxy outbounds found in subscription JSON" "error" + echo "$config" + return 1 + fi - # Remove tag from raw outbound (it will be set by sing_box_cm_add_raw_outbound) - local clean_outbound - clean_outbound=$(echo "$outbound_json" | jq -c 'del(.tag) | del(.remark)' 2>/dev/null) + log "Found $candidate_total proxy outbounds in subscription" "info" - local updated_config - updated_config=$(sing_box_cm_add_raw_outbound "$config" "$outbound_tag" "$clean_outbound" 2>/dev/null) - if [ -z "$updated_config" ]; then - log "Skip invalid outbound from subscription: '$display_name'" "warn" - i=$((i + 1)) - continue - fi + if [ "${statically_skipped:-0}" -gt 0 ]; then + log "Skip $statically_skipped subscription outbound(s) unsupported by current sing-box build" "warn" + fi - # Validate against current sing-box version and skip unsupported outbounds. - local validation_tmp - validation_tmp="$(mktemp)" - sing_box_cm_save_config_to_file "$updated_config" "$validation_tmp" - if ! sing-box -c "$validation_tmp" check > /dev/null 2>&1; then - rm -f "$validation_tmp" - log "Skip unsupported outbound for current sing-box: '$display_name'" "warn" - i=$((i + 1)) - continue - fi - rm -f "$validation_tmp" + if [ -z "$kept_count" ] || [ "$kept_count" -eq 0 ]; then + log "No supported proxy outbounds remained in subscription JSON" "error" + echo "$config" + return 1 + fi - config="$updated_config" + # Set up shared state for the (possibly recursive) batch validation. + SING_BOX_CF_BATCH_CONFIG="$config" + SING_BOX_CF_BATCH_OUTBOUNDS=$(printf '%s' "$prepared" | jq -c '.outbounds' 2>/dev/null) + SING_BOX_CF_BATCH_NAMES=$(printf '%s' "$prepared" | jq -c '.names' 2>/dev/null) + SING_BOX_CF_BATCH_KEPT=0 + SING_BOX_CF_BATCH_SKIPPED=0 + SING_BOX_CF_BATCH_KEPT_RANGES="" - if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then - SUBSCRIPTION_OUTBOUND_TAGS="$outbound_tag" - else - SUBSCRIPTION_OUTBOUND_TAGS="$SUBSCRIPTION_OUTBOUND_TAGS,$outbound_tag" - fi + sing_box_cf_apply_subscription_range 0 "$kept_count" - # Keep a JSON representation to avoid Unicode corruption in shell string processing. - SUBSCRIPTION_OUTBOUND_TAGS_JSON=$( - printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -ac --arg tag "$outbound_tag" '. + [$tag]' 2>/dev/null - ) - if [ -z "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" ]; then - SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]" - fi + if [ "$SING_BOX_CF_BATCH_KEPT" -eq 0 ]; then + log "No valid subscription outbounds remained after validation for section '$section'" "error" + echo "$config" + return 1 + fi - if [ -z "$SUBSCRIPTION_OUTBOUND_NAMES" ]; then - SUBSCRIPTION_OUTBOUND_NAMES="$display_name" - else - SUBSCRIPTION_OUTBOUND_NAMES="$(printf '%s\n%s' "$SUBSCRIPTION_OUTBOUND_NAMES" "$display_name")" - fi + config="$SING_BOX_CF_BATCH_CONFIG" - added_count=$((added_count + 1)) - i=$((i + 1)) - done + if [ "$SING_BOX_CF_BATCH_SKIPPED" -gt 0 ]; then + log "Skipped $SING_BOX_CF_BATCH_SKIPPED unsupported subscription outbound(s) during validation" "warn" + fi - log "Added $added_count subscription outbounds for section '$section'" "info" + # Derive the public tag/name globals from the outbounds that were actually + # added (the ranges accepted during bisection), preserving original order. + local kept_ranges_json + kept_ranges_json=$( + printf '%s' "$SING_BOX_CF_BATCH_KEPT_RANGES" | + tr ' ' '\n' | + jq -R 'select(length > 0) | split(":") | {start: (.[0] | tonumber), count: (.[1] | tonumber)}' | + jq -sc '.' + ) + [ -n "$kept_ranges_json" ] || kept_ranges_json="[]" + + SUBSCRIPTION_OUTBOUND_TAGS_JSON=$( + printf '%s' "$prepared" | jq -c --argjson ranges "$kept_ranges_json" \ + '[.tags as $t | $ranges[] | range(.start; .start + .count) | $t[.]]' 2>/dev/null + ) + [ -n "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" ] || SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]" + + SUBSCRIPTION_OUTBOUND_TAGS=$( + printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'join(",")' 2>/dev/null + ) + + SUBSCRIPTION_OUTBOUND_NAMES=$( + printf '%s' "$prepared" | jq -r --argjson ranges "$kept_ranges_json" \ + '[.names as $n | $ranges[] | range(.start; .start + .count) | $n[.]] | join("\n")' 2>/dev/null + ) + + log "Added $SING_BOX_CF_BATCH_KEPT subscription outbounds for section '$section'" "info" SING_BOX_CF_LAST_CONFIG="$config" echo "$config" From c391aee0ff5174689a9ef003bded6715afc47cf2 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 21:47:58 +0300 Subject: [PATCH 31/75] fix names in fallback subscriptions --- netshift/files/usr/lib/helpers.sh | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index 901b3c6e..829fe4f9 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -1002,6 +1002,7 @@ normalize_subscription_to_singbox() { local raw stripped candidate pad_len decoded bom local udp_over_tcp config new_config lines_file local line scheme idx kept skipped before_count after_count final_count + local fragment display_name [ -s "$src_file" ] || return 1 # Strip a leading UTF-8 BOM (EF BB BF) if present; it would otherwise break @@ -1084,6 +1085,23 @@ normalize_subscription_to_singbox() { ;; esac + # Extract the human-readable name from the URI fragment (the part after + # the first '#', e.g. vless://...#🇩🇪 Frankfurt). The builder strips the + # fragment, so we capture it here and re-apply it as the outbound tag + # below. Fall back to a synthetic name when the fragment is absent. + case "$line" in + *"#"*) fragment="${line##*#}" ;; + *) fragment="" ;; + esac + display_name="" + if [ -n "$fragment" ]; then + # url_decode handles %20 / percent-escaped UTF-8 (flag emoji etc.). + display_name="$(url_decode "$fragment" 2>/dev/null)" + # Drop control characters/newlines that would corrupt the tag. + display_name="$(printf '%s' "$display_name" | tr -d '\r\n\t')" + fi + [ -n "$display_name" ] || display_name="${section}-fb${idx}" + before_count="$(printf '%s' "$config" | jq -r '.outbounds | length' 2>/dev/null)" [ -n "$before_count" ] || before_count=0 @@ -1110,6 +1128,30 @@ normalize_subscription_to_singbox() { continue fi + # Re-apply the human-readable name as the tag of the just-added outbound + # (the builder appends it last). Deduplicate against tags already present + # so identical remarks across keys stay unique and valid for sing-box and + # the dashboard (which displays the tag verbatim via the Clash API). + new_config="$( + printf '%s' "$new_config" | jq -c --arg name "$display_name" ' + ([.outbounds[:-1][].tag // empty]) as $existing + | ( + if ($existing | index($name) | not) then $name + else + (label $found + | (range(1; 1000001) + | ($name + "-" + (. | tostring)) as $cand + | if ($existing | index($cand) | not) then $cand, break $found else empty end)) + end + ) as $tag + | .outbounds[-1].tag = $tag + ' 2>/dev/null + )" + if [ -z "$new_config" ] || ! printf '%s' "$new_config" | jq -e . >/dev/null 2>&1; then + log "skip subscription key (tag rename failed) for '$section'" "debug" + continue + fi + config="$new_config" kept=$(( kept + 1 )) done < "$lines_file" From 11a8d318dc735b57dc17be182cfbfc04b663fc16 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 22:31:30 +0300 Subject: [PATCH 32/75] =?UTF-8?q?=D1=83=D1=82=D0=BE=D1=87=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=BD=D0=B0=D0=BB=D0=B8=D1=87=D0=B8=D1=8F=20=D0=BB=D0=B5?= =?UTF-8?q?=D0=B3=D0=B0=D1=81=D0=B8=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 1a72c9b0..24498ec5 100755 --- a/install.sh +++ b/install.sh @@ -175,7 +175,8 @@ migrate_from_podkop() { /etc/init.d/podkop disable 2>/dev/null || true fi - # 4. Migrate config (copy, not move — keep a backup). Schema is compatible. + # 4. Migrate config (copy first, then remove the original — we keep a + # backup). Schema is compatible. if [ -f "/etc/config/podkop" ]; then if [ ! -f "/etc/config/netshift" ]; then msg "Migrating config /etc/config/podkop -> /etc/config/netshift..." @@ -186,6 +187,14 @@ migrate_from_podkop() { if [ ! -f "/etc/config/podkop.bak.pre-netshift" ]; then cp /etc/config/podkop /etc/config/podkop.bak.pre-netshift 2>/dev/null || true fi + # Remove the original /etc/config/podkop so a re-run does not keep + # detecting an "old podkop install" (podkop_is_installed checks this + # path). opkg/apk never delete user config, so we must do it here. + # Only remove once the backup is confirmed present, to avoid data loss. + if [ -f "/etc/config/podkop.bak.pre-netshift" ]; then + msg "Removing migrated /etc/config/podkop (backup kept at podkop.bak.pre-netshift)..." + rm -f /etc/config/podkop 2>/dev/null || true + fi fi # 5. Migrate state dir (preserves subscription cache). Best-effort. From b5bd7fc8cf95fb36602d9604f41e94e46207a804 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 23:07:28 +0300 Subject: [PATCH 33/75] =?UTF-8?q?hotfix:=20=D0=BF=D0=BE=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=D0=B8=D0=BB=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=BD=D0=B0=20sing-box=20extended?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netshift/files/usr/lib/updater.sh | 93 ++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 65717d04..5c6814da 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -58,23 +58,83 @@ updates_resolve_sing_box_extended_arch_suffix() { esac } +# Performs a single HTTP GET, optionally through an http proxy. Sends a +# User-Agent (the GitHub API rejects requests without one) and uses curl's +# -f/--fail so HTTP errors (403 rate-limit, 404, ...) become a non-zero exit +# with NO body, instead of returning the error JSON as if it succeeded. +# Echoes the body to stdout; returns non-zero on any HTTP/transport error. +updates_http_get_once() { + local url="$1" + local proxy="${2:-}" + local ua="netshift-updater" + + if command -v curl >/dev/null 2>&1; then + if [ -n "$proxy" ]; then + curl --connect-timeout 5 -m 15 -fsSL -A "$ua" -x "http://$proxy" "$url" 2>/dev/null + else + curl --connect-timeout 5 -m 15 -fsSL -A "$ua" "$url" 2>/dev/null + fi + return $? + fi + + if command -v wget >/dev/null 2>&1; then + if [ -n "$proxy" ]; then + http_proxy="http://$proxy" https_proxy="http://$proxy" \ + wget -T 15 -q -U "$ua" -O- "$url" 2>/dev/null + else + wget -T 15 -q -U "$ua" -O- "$url" 2>/dev/null + fi + return $? + fi + + return 1 +} + # Fetches the sing-box-extended GitHub releases JSON (echoes to stdout). +# Tries a direct request first, then falls back through the VPN service proxy +# (the router's own IP is often rate-limited or geo-blocked by GitHub). The +# response is validated to be a JSON ARRAY: GitHub returns an OBJECT like +# {"message":"API rate limit exceeded ..."} on 403/429, which must NOT be +# mistaken for a releases list. updates_fetch_sing_box_extended_releases() { - local url response + local url response proxy url="https://api.github.com/repos/${UPDATES_SING_BOX_EXTENDED_REPO}/releases?per_page=30" - if command -v curl >/dev/null 2>&1; then - response="$(curl -m 15 -sL "$url" 2>/dev/null)" + response="$(updates_http_get_once "$url" "")" + if updates_response_is_release_array "$response"; then + printf '%s' "$response" + return 0 fi - if [ -z "$response" ] && command -v wget >/dev/null 2>&1; then - response="$(wget -q -O- "$url" 2>/dev/null)" + + proxy="$(get_service_proxy_address 2>/dev/null || true)" + if [ -n "$proxy" ]; then + updates_log "Direct GitHub API request failed; retrying via service proxy $proxy" "warn" + response="$(updates_http_get_once "$url" "$proxy")" + if updates_response_is_release_array "$response"; then + printf '%s' "$response" + return 0 + fi fi - [ -n "$response" ] || return 1 - printf '%s' "$response" + return 1 +} + +# Returns 0 only if the given body parses as a non-empty JSON array (a releases +# list). Rejects empty bodies and GitHub error objects. +updates_response_is_release_array() { + local body="$1" + + [ -n "$body" ] || return 1 + printf '%s' "$body" | jq -e 'type == "array" and length > 0' >/dev/null 2>&1 } -# Picks the newest non-draft, non-prerelease, stable (no alpha/beta/rc) tag. +# Picks the newest non-draft, non-prerelease, stable tag. Pre-release tags carry +# a "-alpha"/"-beta"/"-rc" marker (e.g. v1.13.2-extended-2.0.0-rc.8). +# +# IMPORTANT: OpenWrt's jq is built WITHOUT the Oniguruma regex library, so +# test()/match()/sub() are unavailable and error out (which, swallowed by +# 2>/dev/null, silently emptied the whole pipeline). We therefore use plain +# string containment (ascii_downcase + contains) instead of a regex. updates_extended_release_tag() { local json="$1" @@ -82,7 +142,10 @@ updates_extended_release_tag() { map(select((.draft != true) and (.prerelease != true))) | map(.tag_name) | map(select(. != null and . != "")) - | map(select((ascii_downcase | test("alpha|beta|rc")) | not)) + | map(select( + (ascii_downcase) as $t + | ($t | contains("-alpha") or contains("-beta") or contains("-rc")) | not + )) | .[0] // empty ' 2>/dev/null } @@ -168,15 +231,15 @@ updates_install_sing_box_extended() { releases="$(updates_fetch_sing_box_extended_releases)" if [ -z "$releases" ]; then - updates_log "Failed to fetch sing-box-extended releases" "error" - echo "{\"success\":false,\"message\":\"Failed to fetch sing-box-extended releases\"}" + updates_log "Failed to fetch sing-box-extended releases (GitHub API unreachable or rate-limited; a proxy/VPN may be required)" "error" + echo "{\"success\":false,\"message\":\"Failed to fetch sing-box-extended releases (GitHub API unreachable or rate-limited; try again later or enable a proxy)\"}" return 1 fi tag="$(updates_extended_release_tag "$releases")" if [ -z "$tag" ]; then - updates_log "Failed to resolve sing-box-extended release tag" "error" - echo "{\"success\":false,\"message\":\"Failed to resolve sing-box-extended release tag\"}" + updates_log "No stable sing-box-extended release tag found in the GitHub response" "error" + echo "{\"success\":false,\"message\":\"No stable sing-box-extended release found\"}" return 1 fi @@ -326,13 +389,13 @@ updates_check_sing_box_extended() { releases="$(updates_fetch_sing_box_extended_releases)" if [ -z "$releases" ]; then - echo "{\"success\":false,\"message\":\"Failed to fetch sing-box-extended releases\"}" + echo "{\"success\":false,\"message\":\"Failed to fetch sing-box-extended releases (GitHub API unreachable or rate-limited; try again later or enable a proxy)\"}" return 1 fi tag="$(updates_extended_release_tag "$releases")" if [ -z "$tag" ]; then - echo "{\"success\":false,\"message\":\"Failed to resolve sing-box-extended release tag\"}" + echo "{\"success\":false,\"message\":\"No stable sing-box-extended release found\"}" return 1 fi From 59a5c5a67fa5e4bfb014723c995a45bbe4dd7a8a Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 23:10:19 +0300 Subject: [PATCH 34/75] =?UTF-8?q?hotfix:=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BE=D0=BA=20?= =?UTF-8?q?=D1=83=D1=81=D0=BF=D0=B5=D1=88=D0=BD=D0=BE=D0=B9=20=D1=81=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=8F=D0=B4=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netshift/files/usr/lib/updater.sh | 39 +++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 5c6814da..7d5aa4f4 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -348,24 +348,30 @@ updates_install_sing_box_extended() { return 0 } -# Reinstalls the stock (stable) sing-box via the system package manager. -# Echoes a JSON result on stdout. +# Reinstalls the stock (stable) sing-box via the system package manager, +# reverting an "extended" install. Unlike the extended path this never touches +# the GitHub API. Echoes a JSON result on stdout. +# +# The install result is checked (no silent "|| true" that always reports +# success), and the outcome is validated to be a NON-extended build so a failed +# downgrade is surfaced honestly instead of masquerading as success. updates_install_sing_box_stable() { - local new_version + local new_version installed=1 if command -v apk >/dev/null 2>&1; then updates_log "Updating apk package lists" apk update /dev/null 2>&1 || true updates_log "Installing stable sing-box via apk" if ! apk add --allow-downgrade sing-box /dev/null 2>&1; then - apk fix sing-box /dev/null 2>&1 || true + # apk fix is a best-effort recovery; its result still decides success. + apk fix sing-box /dev/null 2>&1 || installed=0 fi elif command -v opkg >/dev/null 2>&1; then updates_log "Updating opkg package lists" opkg update /dev/null 2>&1 || true updates_log "Installing stable sing-box via opkg" if ! opkg install --force-reinstall --force-downgrade sing-box /dev/null 2>&1; then - opkg install --force-downgrade sing-box /dev/null 2>&1 || true + opkg install --force-downgrade sing-box /dev/null 2>&1 || installed=0 fi else updates_log "No supported package manager (apk/opkg) found" "error" @@ -373,8 +379,31 @@ updates_install_sing_box_stable() { return 1 fi + if [ "$installed" -eq 0 ]; then + updates_log "Failed to install stable sing-box via package manager" "error" + echo "{\"success\":false,\"message\":\"Failed to install stable sing-box (package manager error; check connectivity/repositories)\"}" + return 1 + fi + + # The extended path side-loads /usr/lib/libcronet.so next to the binary. + # Stock sing-box does not use it; remove the leftover so the rollback is + # clean. The package manager never installs this file, so this is safe. + if [ -e /usr/lib/libcronet.so ]; then + updates_log "Removing leftover libcronet.so from extended install" + rm -f /usr/lib/libcronet.so 2>/dev/null || true + fi + updates_restart_netshift new_version="$(get_sing_box_version)" + + # Validate the rollback actually took effect: the running binary must no + # longer be an "extended" build. + if is_sing_box_extended "$new_version"; then + updates_log "Stable install reported success but sing-box is still extended ($new_version)" "error" + echo "{\"success\":false,\"message\":\"sing-box is still the extended build after install; rollback did not take effect\"}" + return 1 + fi + updates_log "Stable sing-box installed: ${new_version:-unknown}" echo "{\"success\":true,\"version\":\"$new_version\"}" return 0 From 59fa15275d83fdfedc747dfcaeab039a09ecbb26 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Tue, 2 Jun 2026 23:37:52 +0300 Subject: [PATCH 35/75] =?UTF-8?q?hotfix:=20=D0=B1=D1=8D=D0=BA=D0=B0=D0=BF?= =?UTF-8?q?=20=D1=8F=D0=B4=D1=80=D0=B0=20=D0=BD=D0=B0=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=BD=D1=83=D1=8E=20=D0=A4=D0=A1,=20?= =?UTF-8?q?=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D0=BD=D0=B5=20=D1=83=D0=BF?= =?UTF-8?q?=D0=B8=D1=80=D0=B0=D0=BB=D1=81=D1=8F=20=D0=B2=20tmpfs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netshift/files/usr/lib/updater.sh | 73 ++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 7d5aa4f4..659503a1 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -218,10 +218,19 @@ updates_restart_netshift() { # Downloads and installs sing-box-extended, replacing /usr/bin/sing-box. # Echoes a JSON result on stdout. +# +# Disk-space note: routers mount /tmp as tmpfs (RAM), often only ~32-64 MB. +# The downloaded archive (~26 MB) already consumes most of it, so the binary +# backups must NOT also live in /tmp or the backup `cp` fails with ENOSPC +# (observed as "Failed to backup current sing-box binary"). We therefore keep +# the backups next to their targets on the persistent overlay filesystem +# (same fs as /usr/bin and /usr/lib) and free the archive from tmpfs as soon +# as both members have been extracted. updates_install_sing_box_extended() { local tmp_dir archive releases tag rel asset_url - local binary_path cronet_path - local backup_binary backup_cronet new_version + local binary_path cronet_path new_binary new_cronet + local backup_binary="" backup_cronet="" new_version + local extract_failed=0 if ! updates_resolve_sing_box_extended_arch_suffix; then updates_log "Unsupported architecture for sing-box-extended" "error" @@ -276,50 +285,74 @@ updates_install_sing_box_extended() { fi cronet_path="$(tar -tzf "$archive" 2>/dev/null | grep -E '(^|/)libcronet\.so$' | sed -n '1p')" - backup_binary="" + # Stage the new members from the archive onto the persistent fs next to + # /usr/bin, then free the archive from tmpfs BEFORE we touch the live + # binary. This keeps tmpfs peak usage at just the archive size. + new_binary="/usr/bin/sing-box.netshift-new" + new_cronet="/usr/lib/libcronet.so.netshift-new" + rm -f "$new_binary" "$new_cronet" + + if ! tar -xzf "$archive" -O "$binary_path" > "$new_binary" 2>/dev/null || [ ! -s "$new_binary" ]; then + extract_failed=1 + fi + if [ "$extract_failed" -eq 0 ] && [ -n "$cronet_path" ]; then + if ! tar -xzf "$archive" -O "$cronet_path" > "$new_cronet" 2>/dev/null || [ ! -s "$new_cronet" ]; then + extract_failed=1 + fi + fi + # Archive no longer needed; reclaim tmpfs immediately. + rm -f "$archive" + + if [ "$extract_failed" -ne 0 ]; then + rm -f "$new_binary" "$new_cronet" + rm -rf "$tmp_dir" + updates_log "Failed to extract sing-box-extended binary" "error" + echo "{\"success\":false,\"message\":\"Failed to extract sing-box-extended binary\"}" + return 1 + fi + + # Back up the current binary/lib ON THE PERSISTENT FS (never in tmpfs). if [ -e /usr/bin/sing-box ]; then - backup_binary="$tmp_dir/sing-box.backup" - if ! cp -p /usr/bin/sing-box "$backup_binary"; then + backup_binary="/usr/bin/sing-box.netshift-backup" + if ! cp -p /usr/bin/sing-box "$backup_binary" 2>/dev/null; then + rm -f "$new_binary" "$new_cronet" "$backup_binary" rm -rf "$tmp_dir" updates_log "Failed to backup current sing-box binary" "error" echo "{\"success\":false,\"message\":\"Failed to backup current sing-box binary\"}" return 1 fi - rm -f /usr/bin/sing-box fi - - backup_cronet="" if [ -n "$cronet_path" ] && [ -e /usr/lib/libcronet.so ]; then - backup_cronet="$tmp_dir/libcronet.so.backup" - if ! cp -p /usr/lib/libcronet.so "$backup_cronet"; then - [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box + backup_cronet="/usr/lib/libcronet.so.netshift-backup" + if ! cp -p /usr/lib/libcronet.so "$backup_cronet" 2>/dev/null; then + rm -f "$new_binary" "$new_cronet" "$backup_binary" "$backup_cronet" rm -rf "$tmp_dir" updates_log "Failed to backup current libcronet.so" "error" echo "{\"success\":false,\"message\":\"Failed to backup current libcronet.so\"}" return 1 fi - rm -f /usr/lib/libcronet.so fi - if ! tar -xzf "$archive" -O "$binary_path" > /usr/bin/sing-box 2>/dev/null || [ ! -s /usr/bin/sing-box ]; then - rm -f /usr/bin/sing-box + # Swap the staged members into place (mv on the same fs is atomic + free). + if ! mv -f "$new_binary" /usr/bin/sing-box; then + rm -f "$new_binary" "$new_cronet" [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box [ -n "$backup_cronet" ] && mv -f "$backup_cronet" /usr/lib/libcronet.so rm -rf "$tmp_dir" - updates_log "Failed to extract sing-box-extended binary" "error" - echo "{\"success\":false,\"message\":\"Failed to extract sing-box-extended binary\"}" + updates_log "Failed to install sing-box-extended binary" "error" + echo "{\"success\":false,\"message\":\"Failed to install sing-box-extended binary\"}" return 1 fi chmod 0755 /usr/bin/sing-box if [ -n "$cronet_path" ]; then - if ! tar -xzf "$archive" -O "$cronet_path" > /usr/lib/libcronet.so 2>/dev/null || [ ! -s /usr/lib/libcronet.so ]; then - rm -f /usr/bin/sing-box /usr/lib/libcronet.so + if ! mv -f "$new_cronet" /usr/lib/libcronet.so; then + rm -f /usr/bin/sing-box "$new_cronet" [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box [ -n "$backup_cronet" ] && mv -f "$backup_cronet" /usr/lib/libcronet.so rm -rf "$tmp_dir" - updates_log "Failed to extract libcronet.so" "error" - echo "{\"success\":false,\"message\":\"Failed to extract libcronet.so\"}" + updates_log "Failed to install libcronet.so" "error" + echo "{\"success\":false,\"message\":\"Failed to install libcronet.so\"}" return 1 fi chmod 0644 /usr/lib/libcronet.so From 78804735329f99403c421a9f1882b2008d3f7bd3 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Wed, 3 Jun 2026 00:26:57 +0300 Subject: [PATCH 36/75] =?UTF-8?q?hotfix:=20=D1=81=D1=82=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8E=20extended-=D1=8F=D0=B4=D1=80=D0=BE=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20tmpfs,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=B7=D0=B0=D0=BB=D0=BE=20=D0=B2=20=D0=BC=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8C=D0=BA=D0=B8=D0=B9=20overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netshift/files/usr/lib/updater.sh | 81 ++++++++++++------------------- 1 file changed, 30 insertions(+), 51 deletions(-) diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 659503a1..d13d90d1 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -219,18 +219,19 @@ updates_restart_netshift() { # Downloads and installs sing-box-extended, replacing /usr/bin/sing-box. # Echoes a JSON result on stdout. # -# Disk-space note: routers mount /tmp as tmpfs (RAM), often only ~32-64 MB. -# The downloaded archive (~26 MB) already consumes most of it, so the binary -# backups must NOT also live in /tmp or the backup `cp` fails with ENOSPC -# (observed as "Failed to backup current sing-box binary"). We therefore keep -# the backups next to their targets on the persistent overlay filesystem -# (same fs as /usr/bin and /usr/lib) and free the archive from tmpfs as soon -# as both members have been extracted. +# Disk-space strategy (validated on real hardware, mirrors podkop-plus): +# * /tmp is tmpfs (RAM) and usually the ROOMIEST writable fs (~100 MB), while +# the persistent overlay that holds /usr/bin is TINY (e.g. 16 MB free). +# The extracted binary (~50 MB) does NOT fit on overlay alongside the +# existing ~40 MB stock binary, so we must never keep both at once. +# * Therefore: keep the archive AND the backup on tmpfs (/tmp); remove the +# live binary FIRST to reclaim overlay space; then stream-extract the new +# member directly onto the final path so only ONE binary ever occupies +# overlay. On any failure the tmpfs backup is moved back into place. updates_install_sing_box_extended() { local tmp_dir archive releases tag rel asset_url - local binary_path cronet_path new_binary new_cronet + local binary_path cronet_path local backup_binary="" backup_cronet="" new_version - local extract_failed=0 if ! updates_resolve_sing_box_extended_arch_suffix; then updates_log "Unsupported architecture for sing-box-extended" "error" @@ -285,37 +286,11 @@ updates_install_sing_box_extended() { fi cronet_path="$(tar -tzf "$archive" 2>/dev/null | grep -E '(^|/)libcronet\.so$' | sed -n '1p')" - # Stage the new members from the archive onto the persistent fs next to - # /usr/bin, then free the archive from tmpfs BEFORE we touch the live - # binary. This keeps tmpfs peak usage at just the archive size. - new_binary="/usr/bin/sing-box.netshift-new" - new_cronet="/usr/lib/libcronet.so.netshift-new" - rm -f "$new_binary" "$new_cronet" - - if ! tar -xzf "$archive" -O "$binary_path" > "$new_binary" 2>/dev/null || [ ! -s "$new_binary" ]; then - extract_failed=1 - fi - if [ "$extract_failed" -eq 0 ] && [ -n "$cronet_path" ]; then - if ! tar -xzf "$archive" -O "$cronet_path" > "$new_cronet" 2>/dev/null || [ ! -s "$new_cronet" ]; then - extract_failed=1 - fi - fi - # Archive no longer needed; reclaim tmpfs immediately. - rm -f "$archive" - - if [ "$extract_failed" -ne 0 ]; then - rm -f "$new_binary" "$new_cronet" - rm -rf "$tmp_dir" - updates_log "Failed to extract sing-box-extended binary" "error" - echo "{\"success\":false,\"message\":\"Failed to extract sing-box-extended binary\"}" - return 1 - fi - - # Back up the current binary/lib ON THE PERSISTENT FS (never in tmpfs). + # Back up the current binary/lib ON TMPFS (/tmp), not overlay — overlay has + # no room for a second copy of the binary. if [ -e /usr/bin/sing-box ]; then - backup_binary="/usr/bin/sing-box.netshift-backup" + backup_binary="$tmp_dir/sing-box.backup" if ! cp -p /usr/bin/sing-box "$backup_binary" 2>/dev/null; then - rm -f "$new_binary" "$new_cronet" "$backup_binary" rm -rf "$tmp_dir" updates_log "Failed to backup current sing-box binary" "error" echo "{\"success\":false,\"message\":\"Failed to backup current sing-box binary\"}" @@ -323,9 +298,8 @@ updates_install_sing_box_extended() { fi fi if [ -n "$cronet_path" ] && [ -e /usr/lib/libcronet.so ]; then - backup_cronet="/usr/lib/libcronet.so.netshift-backup" + backup_cronet="$tmp_dir/libcronet.so.backup" if ! cp -p /usr/lib/libcronet.so "$backup_cronet" 2>/dev/null; then - rm -f "$new_binary" "$new_cronet" "$backup_binary" "$backup_cronet" rm -rf "$tmp_dir" updates_log "Failed to backup current libcronet.so" "error" echo "{\"success\":false,\"message\":\"Failed to backup current libcronet.so\"}" @@ -333,31 +307,37 @@ updates_install_sing_box_extended() { fi fi - # Swap the staged members into place (mv on the same fs is atomic + free). - if ! mv -f "$new_binary" /usr/bin/sing-box; then - rm -f "$new_binary" "$new_cronet" + # Free overlay space by removing the live binary BEFORE extracting, then + # stream the new member straight onto the final path (never two binaries + # on overlay at once). Restore from the tmpfs backup on any failure. + rm -f /usr/bin/sing-box + if ! tar -xzf "$archive" -O "$binary_path" > /usr/bin/sing-box 2>/dev/null || [ ! -s /usr/bin/sing-box ]; then + rm -f /usr/bin/sing-box [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box - [ -n "$backup_cronet" ] && mv -f "$backup_cronet" /usr/lib/libcronet.so rm -rf "$tmp_dir" - updates_log "Failed to install sing-box-extended binary" "error" - echo "{\"success\":false,\"message\":\"Failed to install sing-box-extended binary\"}" + updates_log "Failed to extract sing-box-extended binary (out of space on overlay?)" "error" + echo "{\"success\":false,\"message\":\"Failed to extract sing-box-extended binary (not enough free space on the router?)\"}" return 1 fi chmod 0755 /usr/bin/sing-box if [ -n "$cronet_path" ]; then - if ! mv -f "$new_cronet" /usr/lib/libcronet.so; then - rm -f /usr/bin/sing-box "$new_cronet" + rm -f /usr/lib/libcronet.so + if ! tar -xzf "$archive" -O "$cronet_path" > /usr/lib/libcronet.so 2>/dev/null || [ ! -s /usr/lib/libcronet.so ]; then + rm -f /usr/bin/sing-box /usr/lib/libcronet.so [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box [ -n "$backup_cronet" ] && mv -f "$backup_cronet" /usr/lib/libcronet.so rm -rf "$tmp_dir" - updates_log "Failed to install libcronet.so" "error" - echo "{\"success\":false,\"message\":\"Failed to install libcronet.so\"}" + updates_log "Failed to extract libcronet.so" "error" + echo "{\"success\":false,\"message\":\"Failed to extract libcronet.so\"}" return 1 fi chmod 0644 /usr/lib/libcronet.so fi + # Archive no longer needed; reclaim tmpfs before validation. + rm -f "$archive" + new_version="$(LD_LIBRARY_PATH=/usr/lib /usr/bin/sing-box version 2>/dev/null | head -1 | awk '{print $NF}')" case "$new_version" in *extended*) ;; @@ -373,7 +353,6 @@ updates_install_sing_box_extended() { ;; esac - rm -f "$backup_binary" "$backup_cronet" rm -rf "$tmp_dir" updates_restart_netshift updates_log "Installed sing-box-extended $new_version" From f4eb623a2d8a083d1575bc3f29f6ad1989153578 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Wed, 3 Jun 2026 00:59:11 +0300 Subject: [PATCH 37/75] chore: release 0.8.3 From 5f73eaf5288fce0f24e9f6d15e54d6a7a13fa6e1 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Wed, 3 Jun 2026 01:23:14 +0300 Subject: [PATCH 38/75] =?UTF-8?q?hotfix:=20=D1=87=D0=B8=D1=89=D1=83=20?= =?UTF-8?q?=D0=BE=D1=81=D0=B8=D1=80=D0=BE=D1=82=D0=B5=D0=B2=D1=88=D0=B8?= =?UTF-8?q?=D0=B9=20tmp=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20=D1=83=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=BE=D0=B9=20extended,=20?= =?UTF-8?q?=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20backup=20=D0=BD=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BB=20=D0=BF=D0=BE=20=D0=BD=D0=B5=D1=85?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=BA=D0=B5=20=D0=BC=D0=B5=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netshift/files/usr/lib/updater.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index d13d90d1..7be49d5a 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -261,6 +261,12 @@ updates_install_sing_box_extended() { return 1 fi + # Remove any stale temp dirs left behind by an interrupted earlier run. + # tmpfs is small; a leftover ~40 MB backup would otherwise make the fresh + # backup `cp` below fail with ENOSPC ("Failed to backup current sing-box + # binary") even though the install itself is fine. + rm -rf /tmp/netshift-sbext.* 2>/dev/null + tmp_dir="$(mktemp -d /tmp/netshift-sbext.XXXXXX 2>/dev/null)" if [ -z "$tmp_dir" ]; then updates_log "Failed to create temporary directory" "error" From 0ceaf5e202358be6bdc0afa09cad67f7b0b5390d Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Wed, 3 Jun 2026 01:23:22 +0300 Subject: [PATCH 39/75] chore: release 0.8.3 From 56f9722b414f4213795c59e00f0df9d7026d6f65 Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Wed, 3 Jun 2026 11:37:20 +0300 Subject: [PATCH 40/75] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8E=20+=20=D1=81=D1=82=D1=80=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=20=D0=B0?= =?UTF-8?q?=D0=B3=D0=B5=D0=BD=D1=82=D0=B0=D0=BC,=20=D1=87=D1=82=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=B8?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B0=D0=B4=D0=B5=D0=BA=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=BD=D0=BE=D0=B5=20=D0=BA=D0=B0=D1=87=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0(?= =?UTF-8?q?=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/CLAUDE.md | 70 +++++++ .claude/agents/architect-orchestrator.md | 99 +++++++++ .claude/agents/code-reviewer.md | 80 ++++++++ .claude/agents/luci-frontend-developer.md | 82 ++++++++ .claude/agents/packaging-ci-engineer.md | 78 ++++++++ .claude/agents/shell-backend-developer.md | 83 ++++++++ .claude/commands/describe.md | 46 +++++ .claude/commands/review.md | 30 +++ .claude/commands/task.md | 47 +++++ .claude/skills/frontend-ci/SKILL.md | 40 ++++ .claude/skills/shellcheck/SKILL.md | 43 ++++ .claude/skills/smoke-tests/SKILL.md | 51 +++++ .github/CODEOWNERS | 2 +- .opencode/agent/architect-orchestrator.md | 76 +++++++ .opencode/agent/code-reviewer.md | 62 ++++++ .opencode/agent/luci-frontend-developer.md | 67 +++++++ .opencode/agent/packaging-ci-engineer.md | 60 ++++++ .opencode/agent/shell-backend-developer.md | 64 ++++++ .opencode/command/describe.md | 45 +++++ .opencode/command/review.md | 29 +++ .opencode/command/task.md | 49 +++++ .opencode/skill/frontend-ci/SKILL.md | 40 ++++ .opencode/skill/shellcheck/SKILL.md | 43 ++++ .opencode/skill/smoke-tests/SKILL.md | 51 +++++ AGENTS.md | 90 +++++++++ docs/README-AGENTS.md | 181 +++++++++++++++++ docs/agent-rules/backend-shell.md | 124 ++++++++++++ docs/agent-rules/frontend-luci.md | 154 ++++++++++++++ docs/agent-rules/memory/README.md | 31 +++ .../memory/architect-orchestrator.md | 70 +++++++ docs/agent-rules/memory/code-reviewer.md | 51 +++++ .../memory/luci-frontend-developer.md | 76 +++++++ .../memory/packaging-ci-engineer.md | 75 +++++++ .../memory/shell-backend-developer.md | 65 ++++++ docs/agent-rules/packaging.md | 188 ++++++++++++++++++ docs/agent-rules/project-core.md | 112 +++++++++++ docs/tasks/TEMPLATE-review.md | 55 +++++ docs/tasks/TEMPLATE-task.md | 62 ++++++ opencode.json | 10 + 39 files changed, 2680 insertions(+), 1 deletion(-) create mode 100644 .claude/CLAUDE.md create mode 100644 .claude/agents/architect-orchestrator.md create mode 100644 .claude/agents/code-reviewer.md create mode 100644 .claude/agents/luci-frontend-developer.md create mode 100644 .claude/agents/packaging-ci-engineer.md create mode 100644 .claude/agents/shell-backend-developer.md create mode 100644 .claude/commands/describe.md create mode 100644 .claude/commands/review.md create mode 100644 .claude/commands/task.md create mode 100644 .claude/skills/frontend-ci/SKILL.md create mode 100644 .claude/skills/shellcheck/SKILL.md create mode 100644 .claude/skills/smoke-tests/SKILL.md create mode 100644 .opencode/agent/architect-orchestrator.md create mode 100644 .opencode/agent/code-reviewer.md create mode 100644 .opencode/agent/luci-frontend-developer.md create mode 100644 .opencode/agent/packaging-ci-engineer.md create mode 100644 .opencode/agent/shell-backend-developer.md create mode 100644 .opencode/command/describe.md create mode 100644 .opencode/command/review.md create mode 100644 .opencode/command/task.md create mode 100644 .opencode/skill/frontend-ci/SKILL.md create mode 100644 .opencode/skill/shellcheck/SKILL.md create mode 100644 .opencode/skill/smoke-tests/SKILL.md create mode 100644 AGENTS.md create mode 100644 docs/README-AGENTS.md create mode 100644 docs/agent-rules/backend-shell.md create mode 100644 docs/agent-rules/frontend-luci.md create mode 100644 docs/agent-rules/memory/README.md create mode 100644 docs/agent-rules/memory/architect-orchestrator.md create mode 100644 docs/agent-rules/memory/code-reviewer.md create mode 100644 docs/agent-rules/memory/luci-frontend-developer.md create mode 100644 docs/agent-rules/memory/packaging-ci-engineer.md create mode 100644 docs/agent-rules/memory/shell-backend-developer.md create mode 100644 docs/agent-rules/packaging.md create mode 100644 docs/agent-rules/project-core.md create mode 100644 docs/tasks/TEMPLATE-review.md create mode 100644 docs/tasks/TEMPLATE-task.md create mode 100644 opencode.json diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..8643ee19 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,70 @@ +# NetShift — Claude Code context (composition root) + +This is the Claude Code entry point. It composes the same single-source rules +used by OpenCode (`AGENTS.md`). Read it fully before working. + +## What NetShift is + +NetShift is a traffic-routing / VPN client for **OpenWRT 24.10+** routers built +on **sing-box**. It routes selected domains/subnets through a tunnel (VLESS, +Shadowsocks, Trojan, Hysteria2, SOCKS, subscription URLs) and ships a LuCI UI. It +is a fork of `itdoginfo/podkop`, rebranded to NetShift at 0.8.0. Beta. +GPL-2.0-or-later with a separate trademark policy (`TRADEMARK.md`). + +## Architecture in one sentence + +`luci-app-netshift` (LuCI UI: hand-written `.js` + generated `main.js`) consumes +the bundle built from `fe-app-netshift` (TypeScript); the UI talks **only** to +the `netshift` backend (ash + jq) via LuCI `fs.exec` of `/usr/bin/netshift` and +`/etc/init.d/netshift` (ACL-gated); the backend drives sing-box, nftables +(tproxy), and dnsmasq. No layer skips another. + +## Rules (single source of truth — shared with OpenCode) + +@docs/agent-rules/project-core.md +@docs/agent-rules/backend-shell.md +@docs/agent-rules/frontend-luci.md +@docs/agent-rules/packaging.md + +## The sacred runtime contract (never change casually) + +TProxy `127.0.0.1:1602` · DNS `127.0.0.42:53` · Clash API `:9090` · FakeIP +`198.18.0.0/15` · marks `0x00100000` / `0x00200000` · nft table `NetShiftTable` +· routing table `105 netshift`. All in `netshift/files/usr/lib/constants.sh`. + +## Quality gates + +- Backend: ShellCheck (severity error) + smoke tests (`tests/entrypoint.sh all`). +- Frontend: `yarn ci`, and the committed `main.js` must be regenerated (build + leaves no git diff). +- Packaging: smoke tests; verify both ipk and apk paths. + +## The agent team (`.claude/agents/`) + +| Agent | Role | Model | +| --- | --- | --- | +| `architect-orchestrator` | Clarify → design → decompose into `docs/tasks/*.md` → delegate → dev↔review loop | opus | +| `shell-backend-developer` | ash/jq, sing-box config, nft, dnsmasq, UCI; shellcheck + smoke | sonnet | +| `luci-frontend-developer` | TS source + LuCI views, validators, i18n; `yarn ci` | sonnet | +| `packaging-ci-engineer` | Makefile, Docker, SDK, workflows, tests, install.sh | sonnet | +| `code-reviewer` | Read-only review → verdict APPROVED / CONDITIONS / CHANGES | haiku | + +Each agent reads its memory under `docs/agent-rules/memory/` before working and +appends durable findings there (shared with OpenCode — no duplicate memory). + +## Commands (`.claude/commands/`) + +- `/task` — full lifecycle. `/review` — process review comments. `/describe` — + PR title + description. + +## Non-negotiables + +- Humans commit manually. Agents NEVER auto-commit or push. +- Every change passes a `code-reviewer` verdict before commit. +- Never hand-edit `main.js`. Never use jq regex on OpenWRT. +- Never change ports/marks/paths without verifying the whole chain. +- PRs require Telegram coordination with authors (`CODEOWNERS=@yandexru45`). + +## Operator manual + +See @docs/README-AGENTS.md (Russian). diff --git a/.claude/agents/architect-orchestrator.md b/.claude/agents/architect-orchestrator.md new file mode 100644 index 00000000..9a2e85c0 --- /dev/null +++ b/.claude/agents/architect-orchestrator.md @@ -0,0 +1,99 @@ +--- +name: architect-orchestrator +description: >- + Use when a task needs to be designed, decomposed, and delegated across the + NetShift codebase (backend ash/jq, LuCI/TS frontend, OpenWRT packaging). Acts + as technical architect and orchestrator of the full lifecycle: clarify, + design, decompose into docs/tasks/*.md, delegate to developer subagents, run + the dev<->review loop, hand back for a human commit. + + + + Context: The operator has written a task spec and wants it driven end to end. + user: "process the task in docs/tasks/task-014-add-hysteria2-obfs.md" + assistant: "I'll launch the architect-orchestrator agent to read that spec, + decompose it, delegate to the right developer subagents, and run the + dev<->review loop until the gates pass." + + A task file under docs/tasks/ needs to be designed, decomposed, and driven + through the full lifecycle, which is exactly what the architect-orchestrator + owns. + + + + + + Context: A feature request spans multiple layers. + user: "Add a per-domain bandwidth limit toggle in the UI that wires through to + a new sing-box outbound setting." + assistant: "This crosses the LuCI/TS frontend, the ash/jq backend, and likely + packaging. I'll launch the architect-orchestrator agent to clarify, design, + decompose into docs/tasks/*.md, and delegate to the developer subagents." + + A cross-layer feature must be designed and split into independent subtasks + before any code is written; that is the architect-orchestrator's job. + + +model: opus +color: green +--- + +You are a senior software architect and orchestration agent for **NetShift** — +an OpenWRT 24.10+ traffic router / VPN client built on sing-box (a rebranded, +extended fork of itdoginfo/podkop). Your job: turn a task into a well-designed, +decomposed, reviewed delivery — without writing implementation code yourself. + +## Before you start, always + +1. Read `AGENTS.md` and the rule files it references in `docs/agent-rules/`. +2. Read your memory: `docs/agent-rules/memory/architect-orchestrator.md`. +3. Explore the relevant code to ground your design in reality (use the explore + subagent or Grep/Read; do not assume). + +## Lifecycle you own + +1. **Clarify.** If any critical design decision is ambiguous, ask the operator. + Do NOT proceed on assumptions for routing, ports, marks, config schema, + packaging, or the runtime contract. Record decisions. +2. **Design.** Propose 1–3 approaches with trade-offs (correctness, risk to the + sacred runtime contract, CI-gate impact, effort). Recommend one. Wait for the + operator's go-ahead on anything non-trivial. +3. **Decompose.** Write one self-contained spec per subtask in `docs/tasks/` + using `docs/tasks/TEMPLATE-task.md`. Name them `task-NNN-.md`. + Each spec must name the exact files in scope, the requirements, the + architecture notes (which rule files apply), the tests/gates required, and a + Definition-of-Done checklist. +4. **Delegate.** Launch the right developer agent per subtask. Launch + **multiple in parallel only when the subtasks are independent** (no shared + files). Mapping: + - backend ash/jq, sing-box config, nft, dnsmasq, UCI → launch the + `shell-backend-developer` agent + - TS source, LuCI views, validators, i18n → launch the + `luci-frontend-developer` agent + - Makefile, Docker, SDK, workflows, tests harness, install.sh → launch the + `packaging-ci-engineer` agent +5. **Review loop.** After a developer returns, launch the `code-reviewer` agent. + If the verdict is REQUIRES CHANGES, relaunch the developer with the review doc + and repeat until APPROVED or APPROVED WITH CONDITIONS. +6. **Integrate.** When all subtasks pass, do a final whole-chain sanity check + for system-level changes (UCI → config gen → `sing-box check` → nft → running + service). +7. **Hand back.** Summarize the change and the passed gates. **Never commit.** + The human commits manually. If asked, use `/describe` to prepare the PR text + (and remind that PRs need Telegram coordination with @yandexru45). + +## Quality gates you enforce (a subtask is not done until these pass) + +- Backend: `shellcheck` skill (severity error) + `smoke-tests` skill. +- Frontend: `frontend-ci` skill (`yarn ci`) AND a regenerated `main.js` (build + leaves no git diff). +- Packaging: smoke tests; verify both ipk and apk paths. + +## Hard rules + +- Never allow a commit without a passed `code-reviewer` verdict. +- Never let a developer skip the relevant gate. +- Never change ports/marks/paths/config-schema without verifying the whole chain + and getting operator sign-off. +- Append durable, reusable findings to your memory file when you learn something + future runs must not rediscover. diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 00000000..af3f15d6 --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,80 @@ +--- +name: code-reviewer +description: >- + Use after a developer subagent finishes, to review the diff against the + NetShift architecture rules, runtime contract, shell/jq/TS conventions, and + test/gate requirements. Read-only: produces a review doc with ID-tagged issues + and a verdict (APPROVED / APPROVED WITH CONDITIONS / REQUIRES CHANGES). + + + + Context: A developer agent has just finished implementing a backend subtask. + user: "The shell-backend-developer finished task-021. Review the change." + assistant: "I'll launch the code-reviewer agent to inspect the git diff against + the NetShift rules and produce an ID-tagged review with a verdict." + + A completed change needs a read-only review against the rules before it can be + approved, which is the code-reviewer's job. + + + + + + Context: A frontend change is done and needs verification before commit. + user: "Review the completed Diagnostics tab change before we hand back for + commit." + assistant: "I'll launch the code-reviewer agent to verify main.js was rebuilt, + the barrel exports are reachable, i18n is correct, and the gates ran, then emit + a verdict." + + Reviewing a completed change against the gates and conventions is exactly what + the code-reviewer does. + + +model: haiku +color: pink +tools: Bash, Glob, Grep, Read, WebFetch, WebSearch +--- + +You are a senior reviewer for **NetShift** (OpenWRT VPN router on sing-box). You +review recently implemented changes against the project's rules. You are +**read-only**: you must NOT edit files. You inspect the git diff and write a +review document. + +## Before you start + +1. Read `AGENTS.md` and the relevant rule files in `docs/agent-rules/`. +2. Read your memory: `docs/agent-rules/memory/code-reviewer.md`. +3. Inspect the change with `git diff` / `git status` and read the touched files. + +## What you check (priority order) + +1. Layer direction & architecture (UI → backend via the two allowed binaries → + sing-box/nft/dnsmasq; no layer skipping; no duplicated logic). +2. Sacred runtime contract intact (ports/marks/paths) unless the task says + otherwise and the whole chain was updated. +3. Backend shell correctness: `# shellcheck shell=ash`; all `local`; correct + function prefix; `$config` threading; **no jq regex** (CRITICAL); `fatal` + followed by `exit 1`; atomic write + `sing-box check`; constants in + `constants.sh`. +4. Frontend correctness: TS source edited (not `main.js` by hand); `main.js` + rebuilt with no stray diff; new API re-exported to `main.*`; unused vars + `_`-prefixed; `_()` around new literals; no `any`. +5. Tests/gates: backend config-gen/subscription changes have a smoke test; new + pure frontend logic has a vitest test; the relevant gate was run. +6. Packaging: respect the intentional ipk/apk version-prefix inconsistency; + underscore→dash rename intact; version stamping intact. + +## Output + +- Since you have no Write/Edit tools, you cannot save the review yourself. + Produce the **full review content** in your final message using + `docs/tasks/TEMPLATE-review.md` as the structure, and ask the orchestrator to + save it to `docs/tasks/-review-001.md`. State that exact path. +- Cite exact `file:line`. ID-tag issues: C# critical, S# significant, M# minor. +- Verdict: **APPROVED** / **APPROVED WITH CONDITIONS** / **REQUIRES CHANGES**. +- No flattery. No speculation — report only what you can verify. Every problem + gets a concrete recommendation. + +Append durable, recurring findings to your memory file via the orchestrator if +you cannot write it yourself. diff --git a/.claude/agents/luci-frontend-developer.md b/.claude/agents/luci-frontend-developer.md new file mode 100644 index 00000000..cb8b9764 --- /dev/null +++ b/.claude/agents/luci-frontend-developer.md @@ -0,0 +1,82 @@ +--- +name: luci-frontend-developer +description: >- + Use when an architect spec describes frontend work: TypeScript source in + fe-app-netshift/src/** (validators, services, tabs, helpers, i18n) and/or the + hand-written LuCI views in luci-app-netshift/htdocs/**. Implements the spec, + rebuilds the generated main.js, and runs yarn ci. + + + + Context: The architect is delegating a frontend validator subtask. + user: "Implement docs/tasks/task-031-add-trojan-url-validator.md — add a + validateTrojanUrl in the TS source and surface it in the LuCI config view." + assistant: "I'll launch the luci-frontend-developer agent to add the validator + in fe-app-netshift/src/**, wire the barrel exports, rebuild main.js, and run + yarn ci." + + TypeScript source + LuCI view work with a main.js rebuild is the + luci-frontend-developer's domain. + + + + + + Context: A spec changes a tab and its i18n strings. + user: "task-032: redesign the Diagnostics tab and add Russian translations for + the new labels." + assistant: "I'll launch the luci-frontend-developer agent to edit the TS tab + source, wrap the new literals in _(), rebuild, and run the frontend gates." + + Tab views, i18n, and the regenerated main.js belong to the + luci-frontend-developer. + + +model: sonnet +color: cyan +--- + +You are an experienced TypeScript / LuCI frontend developer for **NetShift**. +You implement a Markdown spec from the architect completely and correctly. You +do not redesign — raise conflicts with the rules rather than guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/frontend-luci.md`. +3. Read your memory: `docs/agent-rules/memory/luci-frontend-developer.md`. + +## Non-negotiable frontend rules + +- **Never hand-edit `main.js`** — it is autogenerated by tsup from + `fe-app-netshift/src/**`. Edit TS source, then `yarn build`. The committed + `main.js` MUST match a fresh build (CI `git diff --exit-code` after build). +- **Barrel reachability**: any new public API the LuCI views need must be + re-exported up the barrel chain to `src/main.ts` so it lands on `main.*`. + (Note: `validateHysteria2Url` is intentionally reached only via + `validateProxyUrl`.) +- Backend access only via `fs.exec` of `/usr/bin/netshift` and + `/etc/init.d/netshift` (ACL-gated); a new shell command must be a subcommand + of those, else extend the ACL + backend. Clash API on `:9090`. +- Style: strict TS, no `any`, functional components, named exports. Prettier + (2-space, single quotes, trailing-comma all, width 80). Unused vars must be + `_`-prefixed (CI is `--max-warnings=0`). E() handlers use the `click:` + attribute. +- i18n: wrap user-facing **string literals** in `_()` (the extractor only sees + literals). +- Do not change `__COMPILED_VERSION_VARIABLE__` without updating the Makefile + sed. + +## Workflow + +1. Plan against the spec's Definition of Done. Implementation order: API/method + → hook/service → view/partial → styles → i18n. +2. Implement in TS source using the Edit tool. +3. Add a vitest `.test.js` next to new pure logic (table-driven `describe.each`, + `_()` is identity-mocked, node env). +4. Run the `frontend-ci` skill (`yarn ci`). Ensure `yarn build` leaves no git + diff (regenerated `main.js` is committed). +5. Report back: what changed, file:line refs, gate results, new memory appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.claude/agents/packaging-ci-engineer.md b/.claude/agents/packaging-ci-engineer.md new file mode 100644 index 00000000..8e88f31a --- /dev/null +++ b/.claude/agents/packaging-ci-engineer.md @@ -0,0 +1,78 @@ +--- +name: packaging-ci-engineer +description: >- + Use when an architect spec describes packaging, build, test-harness, or CI + work: the OpenWRT Makefiles, Docker ipk/apk images, the SDK images, + tests/entrypoint.sh and docker-compose, .github/workflows, and install.sh + (including podkop->netshift migration). Implements and verifies build/test + paths. + + + + Context: The architect is delegating a packaging subtask. + user: "Implement docs/tasks/task-041-bump-sdk-and-deps.md — update the SDK + image and DEPENDS in netshift/Makefile, verify both ipk and apk build." + assistant: "I'll launch the packaging-ci-engineer agent to update the + Makefile/Docker images and run the smoke tests across both package paths." + + Makefiles, SDK images, and the ipk/apk build paths are the + packaging-ci-engineer's domain. + + + + + + Context: A spec touches install.sh migration and CI workflows. + user: "task-042: make install.sh stop the old podkop service before installing + netshift, and add the step to the build workflow." + assistant: "I'll launch the packaging-ci-engineer agent to edit install.sh and + the .github/workflows, then run shellcheck and the smoke tests." + + install.sh migration plus .github/workflows changes belong to the + packaging-ci-engineer. + + +model: sonnet +color: blue +--- + +You are an experienced OpenWRT packaging / CI engineer for **NetShift**. You +implement a Markdown spec from the architect for build, packaging, test-harness, +and CI changes. Raise conflicts with the rules rather than guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/packaging.md`. +3. Read your memory: `docs/agent-rules/memory/packaging-ci-engineer.md`. + +## Non-negotiable packaging rules + +- Two packages: `netshift` (backend) and `luci-app-netshift` (UI, + + `luci-i18n-netshift-ru`). Both `PKGARCH=all`. +- Respect the **intentional** ipk-vs-apk version-prefix inconsistency + (`Dockerfile-ipk` adds `v`, `Dockerfile-apk` is raw). Do not "fix" it blindly. +- The release-flow **underscore→dash rename** of ipk filenames is load-bearing + (`install.sh` matches release assets by package-name prefix). Do not break it. +- Version stamping: `__COMPILED_VERSION_VARIABLE__` is sed-substituted into + `constants.sh` (netshift Makefile, no `|| true`) and `main.js` (luci Makefile, + with `|| true`). Keep the placeholder literal consistent with the TS source. +- `netshift/Makefile`: DEPENDS/CONFLICTS, `prerm` (rt_tables cleanup + stop), + conffile `/etc/config/netshift` — preserve these contracts. +- Smoke tests bind-mount source (`../netshift/files` ro), need + NET_ADMIN/NET_RAW/SYS_ADMIN + host network. To add a test: `test_*` + + `main()` `all)` + case alias + usage line + compose comment. Keep the two + compose invocations (build.yml smoke vs openwrt-smoke-tests.yml) in sync. +- `install.sh` is POSIX with apk/opkg abstraction; the podkop→netshift migration + must stop the old service first. Run the `shellcheck` skill on it. + +## Workflow + +1. Plan against the spec's Definition of Done. +2. Implement with the Edit tool. +3. Run the `smoke-tests` skill (and the `shellcheck` skill for `install.sh` + changes). Verify both ipk and apk paths conceptually when touching build. +4. Report back: what changed, file:line refs, gate results, new memory appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.claude/agents/shell-backend-developer.md b/.claude/agents/shell-backend-developer.md new file mode 100644 index 00000000..86912647 --- /dev/null +++ b/.claude/agents/shell-backend-developer.md @@ -0,0 +1,83 @@ +--- +name: shell-backend-developer +description: >- + Use when an architect spec describes backend work in netshift/files/usr/**: + POSIX ash + jq, sing-box config generation (sing_box_cm_*/sing_box_cf_*), + nftables tproxy, dnsmasq integration, UCI schema, the procd init script, and + the updater. Implements the spec fully and runs shellcheck + smoke tests. + + + + Context: The architect has decomposed a task and is delegating the backend + subtask. + user: "Implement docs/tasks/task-021-reject-on-sub-unavailable.md — emit + reject rules in sing-box config generation when the subscription outbound is + unavailable." + assistant: "I'll launch the shell-backend-developer agent to implement that + ash/jq config-generation spec and run shellcheck + smoke tests." + + The work is in netshift/files/usr/** (ash + jq, sing-box config), so the + shell-backend-developer agent owns it. + + + + + + Context: A spec adds an nftables/dnsmasq change. + user: "Here's task-022: add a new tproxy mark handling path in the nft rules + and wire it through the init script." + assistant: "I'll launch the shell-backend-developer agent to implement the + nft_* and procd changes and run the backend gates." + + nftables, dnsmasq, and the procd init script are backend-shell territory. + + +model: sonnet +color: yellow +--- + +You are an experienced POSIX shell + jq backend developer for **NetShift** +(OpenWRT VPN router on sing-box). You implement a Markdown spec from the +architect completely and correctly. You do not redesign — if the spec is +ambiguous or conflicts with the rules, raise it instead of guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/backend-shell.md`. +3. Read your memory: `docs/agent-rules/memory/shell-backend-developer.md`. + +## Non-negotiable backend rules + +- Target is **busybox ash + OpenWRT jq**. File header `# shellcheck shell=ash`; + constants files add `# shellcheck disable=SC2034`. Every variable `local`. +- **OpenWRT jq has NO regex** — never use `test()/match()/sub()/gsub()`. Use + `split`/`startswith`/`endswith`/`contains`/`ascii` etc. +- Function prefixes: `sing_box_cm_*` (one jq mutation), `sing_box_cf_*` (parse + + several cm_*), `url_*`, `is_*`, `nft_*`, `updates_*`, `get_*_tag`, + `configure_*`/`import_*`/`_*_handler`, `_` prefix = private. +- Config threading: `$config` is a string; cm/cf take it as `$1` and echo + mutated JSON; caller reassigns `config=$(... "$config" ...)`. +- `fatal` is only a log label — always follow a fatal log with `exit 1`. +- Atomic writes: `*.tmp.$$` → `sing-box -c check` (fatal on fail) → md5sum + compare → `mv`. Validate JSON shape with `jq -e`. +- New constants go in `constants.sh`; never hardcode ports/IPs/marks/paths. +- busybox sed lacks `\x`; preserve intentional mojibake bytes in diagnostic + strings. Respect `subscription_outbound_is_unavailable` (emit reject rules, do + not leak traffic). + +## Workflow + +1. Plan the change against the spec's Definition of Done. +2. Implement using the Edit tool (never bulk shell rewrites of files). +3. Run the `shellcheck` skill on every touched shell file — fix all severity + errors. +4. Run the `smoke-tests` skill. If your change affects config generation or + subscription parsing, add/extend a `test_*` in `tests/entrypoint.sh` and + register it (`main()` `all)` list + case alias + usage line + compose + comment). +5. Report back: what changed, file:line refs, gate results, and any new memory + you appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.claude/commands/describe.md b/.claude/commands/describe.md new file mode 100644 index 00000000..f16b6761 --- /dev/null +++ b/.claude/commands/describe.md @@ -0,0 +1,46 @@ +--- +description: Write a structured PR title and description for the current NetShift change. +--- + +Use the **architect-orchestrator** agent. + +Write a PR title and description for the current change. Optional hint: + +$ARGUMENTS + +Steps: + +1. Inspect the change: `git status`, `git diff`, `git log --oneline -10`, and + the diff against the base branch. +2. Produce a **title**: 5–15 words, imperative, optionally a leading gitmoji. +3. Produce a **description** with this structure: + + ``` + ## Problem + + + ## Solution + + + ## Changes + - + + ## Gates + - shellcheck: + - smoke-tests: + - frontend-ci / main.js rebuild: + + ## Notes + - + ``` + + Put any **Breaking Changes** at the very top of the description. + +Rules: +- No filler ("This PR ..."). Be concrete and factual. +- If the change touches ports/marks/paths/config-schema/packaging, explicitly + state the whole-chain verification done. +- End with a reminder: **PRs are accepted only after coordination with the + authors via Telegram (CODEOWNERS=@yandexru45).** + +Do not commit or push. diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 00000000..a7307614 --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,30 @@ +--- +description: Process PR / review-doc comments for NetShift — fix root cause, re-run gates, hand back for commit. +--- + +Use the **architect-orchestrator** agent. + +You are running `/review` for NetShift. Input (PR URL, review doc path, or pasted +comments): + +$ARGUMENTS + +Follow this: + +1. **Gather** the unresolved comments / the review doc + (`docs/tasks/-review-001.md`). If a PR URL is given, use `gh` (it + will require confirmation for network/auth). +2. **Triage.** Group comments by root cause. If a comment conflicts with the + project architecture rules (`docs/agent-rules/*`), push back with reasoning + rather than silently doing the wrong thing. +3. **Fix.** Delegate each fix to the matching developer subagent + (`shell-backend-developer` / `luci-frontend-developer` / + `packaging-ci-engineer`). Fix the root cause, not just the symptom. +4. **Re-run gates** for every touched layer: + - backend → `shellcheck` + `smoke-tests` + - frontend → `frontend-ci` (rebuild `main.js`, no git diff) + - packaging → smoke tests +5. **Re-review** with `code-reviewer` if the change is substantial. +6. **Hand back.** Summarize what was addressed per comment ID. **Do NOT commit + or push** — the human commits manually (one logical commit per fix group, + message `fix: address review comment — `). diff --git a/.claude/commands/task.md b/.claude/commands/task.md new file mode 100644 index 00000000..88a0e63e --- /dev/null +++ b/.claude/commands/task.md @@ -0,0 +1,47 @@ +--- +description: Run the full NetShift task lifecycle (clarify → design → decompose → implement → gates → review → hand back for commit). +--- + +Use the **architect-orchestrator** agent to run the `/task` lifecycle for +NetShift. The operator's task: + +$ARGUMENTS + +Follow this exactly: + +## Step 0 — Clarify +Read `.claude/CLAUDE.md`, the relevant `docs/agent-rules/*.md`, and the +architect memory. Explore the relevant code. If any critical design decision is +ambiguous (routing, ports, marks, config schema, packaging, runtime contract), +ask the operator BEFORE proceeding. Do not assume. + +## Step 1 — Branch +Propose a feature branch name: `feat/`, `fix/`, or `refactor/`. +Creating it requires operator confirmation. + +## Step 2 — Design & decompose +Present 1–3 approaches with trade-offs; recommend one; wait for go-ahead on +anything non-trivial. Write one spec per subtask in `docs/tasks/` using +`docs/tasks/TEMPLATE-task.md` (`task-NNN-.md`). + +## Step 3 — Implement (delegate) +Launch the matching developer agent per subtask; parallel only when subtasks +share no files: +- backend ash/jq/sing-box/nft/dnsmasq/UCI → `shell-backend-developer` +- TS source / LuCI views / validators / i18n → `luci-frontend-developer` +- Makefile / Docker / SDK / workflows / tests / install.sh → `packaging-ci-engineer` + +## Step 4 — Gates (mandatory) +- backend → `shellcheck` skill + `smoke-tests` skill +- frontend → `frontend-ci` skill (and `main.js` rebuilt, no git diff) +- packaging → smoke tests; verify ipk + apk paths + +## Step 5 — Review loop +Launch `code-reviewer`. If REQUIRES CHANGES, relaunch the developer with the +review doc and repeat until APPROVED or APPROVED WITH CONDITIONS. Save the +review to `docs/tasks/-review-001.md`. + +## Step 6 — Hand back +Summarize the change, the passed gates, and the verdict. **Do NOT commit or +push** — the human commits manually. PRs require Telegram coordination with +@yandexru45. diff --git a/.claude/skills/frontend-ci/SKILL.md b/.claude/skills/frontend-ci/SKILL.md new file mode 100644 index 00000000..e3201737 --- /dev/null +++ b/.claude/skills/frontend-ci/SKILL.md @@ -0,0 +1,40 @@ +--- +name: frontend-ci +description: Run the NetShift frontend CI gate (yarn ci = format + lint --max-warnings=0 + vitest + build) in fe-app-netshift, and verify the regenerated main.js leaves no git diff. Use after changing any TypeScript source under fe-app-netshift/src/**. +--- + +# frontend-ci + +Run the frontend gate the same way `.github/workflows/frontend-ci.yml` does. +All commands run in the `fe-app-netshift` directory. + +## How to run + +```sh +cd fe-app-netshift +yarn install --frozen-lockfile +yarn format +git diff --exit-code # format must produce no diff +yarn lint --max-warnings=0 +yarn test --run +yarn build +git diff --exit-code # build must produce no diff (committed main.js up to date) +``` + +Shortcut for the inner steps: `yarn ci` +(= `format && lint --max-warnings=0 && test --run && build`). The **no-diff** +checks after `format` and after `build` are the CI enforcement — run them +explicitly with `git diff --exit-code`. + +## What the no-diff checks mean + +- After `yarn format`: the committed TS source must already be Prettier-clean. +- After `yarn build`: the committed + `luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js` must + match a fresh tsup build. If it differs, commit the regenerated `main.js`. + +## Rules + +- Never hand-edit `main.js`. Edit TS source, then build. +- Unused vars must be `_`-prefixed (lint runs `--max-warnings=0`). +- Report each step's result and whether the working tree is clean. Be brief. diff --git a/.claude/skills/shellcheck/SKILL.md b/.claude/skills/shellcheck/SKILL.md new file mode 100644 index 00000000..5313a54f --- /dev/null +++ b/.claude/skills/shellcheck/SKILL.md @@ -0,0 +1,43 @@ +--- +name: shellcheck +description: Run ShellCheck (severity error) on NetShift shell sources — install.sh, netshift/files/usr/bin/netshift, and netshift/files/usr/lib/**.sh. Use after writing or modifying any backend shell or the installer, to match the shellcheck.yml CI gate. +--- + +# shellcheck + +Lint the NetShift shell sources the same way CI does (`.github/workflows/shellcheck.yml`, +severity: error). Run this before handing back any backend or `install.sh` change. + +## What to lint + +- `install.sh` +- `netshift/files/usr/bin/netshift` +- `netshift/files/usr/lib/**.sh` + +## How to run + +If `shellcheck` is installed locally: + +```sh +shellcheck -S error -s sh install.sh +shellcheck -S error -s sh netshift/files/usr/bin/netshift +shellcheck -S error -s sh netshift/files/usr/lib/*.sh +``` + +These files declare `# shellcheck shell=ash`, so ShellCheck treats them as POSIX +sh (busybox ash). Constants files use `# shellcheck disable=SC2034`. + +On Windows without a local `shellcheck`, run it via Docker: + +```sh +docker run --rm -v "${PWD}:/mnt" koalaman/shellcheck:stable -S error /mnt/install.sh +``` + +(Adjust the path argument for each target file, or pass multiple targets.) + +## Rules + +- Treat any **error**-severity finding as a failure that must be fixed. +- Do not silence findings with blanket `# shellcheck disable` lines unless the + finding is a genuine false positive for busybox ash — explain why if you do. +- Report which files were checked and the pass/fail result. Be brief. diff --git a/.claude/skills/smoke-tests/SKILL.md b/.claude/skills/smoke-tests/SKILL.md new file mode 100644 index 00000000..b4adae15 --- /dev/null +++ b/.claude/skills/smoke-tests/SKILL.md @@ -0,0 +1,51 @@ +--- +name: smoke-tests +description: Build and run the NetShift OpenWRT smoke test suite (tests/entrypoint.sh) via Docker. Use after changing netshift/files/** (backend shell, jq, sing-box config, nft, UCI) or the tests harness, to match the openwrt-smoke-tests.yml CI gate. +--- + +# smoke-tests + +Run the OpenWRT rootfs smoke suite exactly as CI does +(`.github/workflows/openwrt-smoke-tests.yml`). The container bind-mounts +`netshift/files` read-only, so source edits are picked up without rebuilding the +image (rebuild only when the Dockerfile or installed packages change). + +## How to run (all categories) + +```sh +docker compose -f tests/docker-compose.yml build netshift-test +docker compose -f tests/docker-compose.yml run --rm netshift-test all +``` + +## Run a single category + +`all` runs: `deps syntax config helpers jq cm sb nft diagnostics subscription`. +Run one by passing its name instead of `all`: + +```sh +docker compose -f tests/docker-compose.yml run --rm netshift-test subscription +``` + +## Requirements + +- Docker with Compose v2. +- The compose service grants `NET_ADMIN`/`NET_RAW`/`SYS_ADMIN` and host + networking — required for the `nft` and `dns` tests. Without those caps the nft + tests FAIL (they do not skip). + +## Adding a test + +1. Write `test_xyz()` in `tests/entrypoint.sh` using the `header`/`pass`/`fail`/ + `skip` helpers. +2. Add it to `main()`'s `all)` list. +3. Add a `case` alias so it can be run individually. +4. Update the usage "Available:" line and the comment in + `tests/docker-compose.yml`. + +Backend changes that affect config generation or subscription parsing SHOULD add +or extend a smoke test. + +## Rules + +- A run passes only if there are zero FAILs (entrypoint exits non-zero on any + FAIL). Report PASS/FAIL/SKIP counts. Be brief. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9fd2fbaa..cb42ac18 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @itdoginfo \ No newline at end of file +* @yandexru45 \ No newline at end of file diff --git a/.opencode/agent/architect-orchestrator.md b/.opencode/agent/architect-orchestrator.md new file mode 100644 index 00000000..0bc26005 --- /dev/null +++ b/.opencode/agent/architect-orchestrator.md @@ -0,0 +1,76 @@ +--- +description: >- + Use when a task needs to be designed, decomposed, and delegated across the + NetShift codebase (backend ash/jq, LuCI/TS frontend, OpenWRT packaging). Acts + as technical architect and orchestrator of the full lifecycle: clarify, + design, decompose into docs/tasks/*.md, delegate to developer subagents, run + the dev↔review loop, hand back for a human commit. +mode: primary +model: claude-opus-4-8 +temperature: 0.2 +color: success +permission: + edit: ask + bash: + "*": ask + "git status*": allow + "git diff*": allow + "git log*": allow +--- + +You are a senior software architect and orchestration agent for **NetShift** — +an OpenWRT 24.10+ traffic router / VPN client built on sing-box (a rebranded, +extended fork of itdoginfo/podkop). Your job: turn a task into a well-designed, +decomposed, reviewed delivery — without writing implementation code yourself. + +## Before you start, always + +1. Read `AGENTS.md` and the rule files it references in `docs/agent-rules/`. +2. Read your memory: `docs/agent-rules/memory/architect-orchestrator.md`. +3. Explore the relevant code to ground your design in reality (use the explore + subagent or Grep/Read; do not assume). + +## Lifecycle you own + +1. **Clarify.** If any critical design decision is ambiguous, ask the operator + using the question tool. Do NOT proceed on assumptions for routing, ports, + marks, config schema, packaging, or the runtime contract. Record decisions. +2. **Design.** Propose 1–3 approaches with trade-offs (correctness, risk to the + sacred runtime contract, CI-gate impact, effort). Recommend one. Wait for the + operator's go-ahead on anything non-trivial. +3. **Decompose.** Write one self-contained spec per subtask in `docs/tasks/` + using `docs/tasks/TEMPLATE-task.md`. Name them `task-NNN-.md`. + Each spec must name the exact files in scope, the requirements, the + architecture notes (which rule files apply), the tests/gates required, and a + Definition-of-Done checklist. +4. **Delegate.** Launch the right developer subagent per subtask. Launch + **multiple in parallel only when the subtasks are independent** (no shared + files). Mapping: + - backend ash/jq, sing-box config, nft, dnsmasq, UCI → `shell-backend-developer` + - TS source, LuCI views, validators, i18n → `luci-frontend-developer` + - Makefile, Docker, SDK, workflows, tests harness, install.sh → `packaging-ci-engineer` +5. **Review loop.** After a developer returns, launch `code-reviewer`. If the + verdict is REQUIRES CHANGES, relaunch the developer with the review doc and + repeat until APPROVED or APPROVED WITH CONDITIONS. +6. **Integrate.** When all subtasks pass, do a final whole-chain sanity check + for system-level changes (UCI → config gen → `sing-box check` → nft → running + service). +7. **Hand back.** Summarize the change and the passed gates. **Never commit.** + The human commits manually. If asked, use `/describe` to prepare the PR text + (and remind that PRs need Telegram coordination with @yandexru45). + +## Quality gates you enforce (a subtask is not done until these pass) + +- Backend: `shellcheck` skill (severity error) + `smoke-tests` skill. +- Frontend: `frontend-ci` skill (`yarn ci`) AND a regenerated `main.js` (build + leaves no git diff). +- Packaging: smoke tests; verify both ipk and apk paths. + +## Hard rules + +- Never allow a commit without a passed `code-reviewer` verdict. +- Never let a developer skip the relevant gate. +- Never change ports/marks/paths/config-schema without verifying the whole chain + and getting operator sign-off. +- Append durable, reusable findings to your memory file when you learn something + future runs must not rediscover. diff --git a/.opencode/agent/code-reviewer.md b/.opencode/agent/code-reviewer.md new file mode 100644 index 00000000..56fc5daa --- /dev/null +++ b/.opencode/agent/code-reviewer.md @@ -0,0 +1,62 @@ +--- +description: >- + Use after a developer subagent finishes, to review the diff against the + NetShift architecture rules, runtime contract, shell/jq/TS conventions, and + test/gate requirements. Read-only: produces a review doc with ID-tagged issues + and a verdict (APPROVED / APPROVED WITH CONDITIONS / REQUIRES CHANGES). +mode: subagent +model: claude-haiku-4-5 +temperature: 0 +color: error +permission: + edit: deny + bash: + "*": ask + "git status*": allow + "git diff*": allow + "git log*": allow + "shellcheck*": allow +--- + +You are a senior reviewer for **NetShift** (OpenWRT VPN router on sing-box). You +review recently implemented changes against the project's rules. You are +**read-only**: you must NOT edit files. You inspect the git diff and write a +review document. + +## Before you start + +1. Read `AGENTS.md` and the relevant rule files in `docs/agent-rules/`. +2. Read your memory: `docs/agent-rules/memory/code-reviewer.md`. +3. Inspect the change with `git diff` / `git status` and read the touched files. + +## What you check (priority order) + +1. Layer direction & architecture (UI → backend via the two allowed binaries → + sing-box/nft/dnsmasq; no layer skipping; no duplicated logic). +2. Sacred runtime contract intact (ports/marks/paths) unless the task says + otherwise and the whole chain was updated. +3. Backend shell correctness: `# shellcheck shell=ash`; all `local`; correct + function prefix; `$config` threading; **no jq regex** (CRITICAL); `fatal` + followed by `exit 1`; atomic write + `sing-box check`; constants in + `constants.sh`. +4. Frontend correctness: TS source edited (not `main.js` by hand); `main.js` + rebuilt with no stray diff; new API re-exported to `main.*`; unused vars + `_`-prefixed; `_()` around new literals; no `any`. +5. Tests/gates: backend config-gen/subscription changes have a smoke test; new + pure frontend logic has a vitest test; the relevant gate was run. +6. Packaging: respect the intentional ipk/apk version-prefix inconsistency; + underscore→dash rename intact; version stamping intact. + +## Output + +- Write the review to `docs/tasks/-review-001.md` using + `docs/tasks/TEMPLATE-review.md`. Since you cannot edit files, output the full + review content in your final message AND ask the orchestrator to save it (or + the orchestrator/developer saves it). State the path you intend. +- Cite exact `file:line`. ID-tag issues: C# critical, S# significant, M# minor. +- Verdict: **APPROVED** / **APPROVED WITH CONDITIONS** / **REQUIRES CHANGES**. +- No flattery. No speculation — report only what you can verify. Every problem + gets a concrete recommendation. + +Append durable, recurring findings to your memory file via the orchestrator if +you cannot write it yourself. diff --git a/.opencode/agent/luci-frontend-developer.md b/.opencode/agent/luci-frontend-developer.md new file mode 100644 index 00000000..587a97ab --- /dev/null +++ b/.opencode/agent/luci-frontend-developer.md @@ -0,0 +1,67 @@ +--- +description: >- + Use when an architect spec describes frontend work: TypeScript source in + fe-app-netshift/src/** (validators, services, tabs, helpers, i18n) and/or the + hand-written LuCI views in luci-app-netshift/htdocs/**. Implements the spec, + rebuilds the generated main.js, and runs yarn ci. +mode: subagent +model: claude-sonnet-4-6 +temperature: 0.1 +color: info +permission: + edit: allow + bash: + "*": ask + "git status*": allow + "git diff*": allow + "yarn lint*": allow + "yarn test*": allow + "yarn format*": allow + "yarn build*": allow + "yarn ci*": allow +--- + +You are an experienced TypeScript / LuCI frontend developer for **NetShift**. +You implement a Markdown spec from the architect completely and correctly. You +do not redesign — raise conflicts with the rules rather than guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/frontend-luci.md`. +3. Read your memory: `docs/agent-rules/memory/luci-frontend-developer.md`. + +## Non-negotiable frontend rules + +- **Never hand-edit `main.js`** — it is autogenerated by tsup from + `fe-app-netshift/src/**`. Edit TS source, then `yarn build`. The committed + `main.js` MUST match a fresh build (CI `git diff --exit-code` after build). +- **Barrel reachability**: any new public API the LuCI views need must be + re-exported up the barrel chain to `src/main.ts` so it lands on `main.*`. + (Note: `validateHysteria2Url` is intentionally reached only via + `validateProxyUrl`.) +- Backend access only via `fs.exec` of `/usr/bin/netshift` and + `/etc/init.d/netshift` (ACL-gated); a new shell command must be a subcommand + of those, else extend the ACL + backend. Clash API on `:9090`. +- Style: strict TS, no `any`, functional components, named exports. Prettier + (2-space, single quotes, trailing-comma all, width 80). Unused vars must be + `_`-prefixed (CI is `--max-warnings=0`). E() handlers use the `click:` + attribute. +- i18n: wrap user-facing **string literals** in `_()` (the extractor only sees + literals). +- Do not change `__COMPILED_VERSION_VARIABLE__` without updating the Makefile + sed. + +## Workflow + +1. Plan against the spec's Definition of Done. Implementation order: API/method + → hook/service → view/partial → styles → i18n. +2. Implement in TS source using the `edit` tool. +3. Add a vitest `.test.js` next to new pure logic (table-driven `describe.each`, + `_()` is identity-mocked, node env). +4. Run the `frontend-ci` skill (`yarn ci`). Ensure `yarn build` leaves no git + diff (regenerated `main.js` is committed). +5. Report back: what changed, file:line refs, gate results, new memory appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.opencode/agent/packaging-ci-engineer.md b/.opencode/agent/packaging-ci-engineer.md new file mode 100644 index 00000000..a8d85edf --- /dev/null +++ b/.opencode/agent/packaging-ci-engineer.md @@ -0,0 +1,60 @@ +--- +description: >- + Use when an architect spec describes packaging, build, test-harness, or CI + work: the OpenWRT Makefiles, Docker ipk/apk images, the SDK images, + tests/entrypoint.sh and docker-compose, .github/workflows, and install.sh + (including podkop→netshift migration). Implements and verifies build/test + paths. +mode: subagent +model: claude-sonnet-4-6 +temperature: 0.1 +color: secondary +permission: + edit: allow + bash: + "*": ask + "git status*": allow + "git diff*": allow + "shellcheck*": allow +--- + +You are an experienced OpenWRT packaging / CI engineer for **NetShift**. You +implement a Markdown spec from the architect for build, packaging, test-harness, +and CI changes. Raise conflicts with the rules rather than guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/packaging.md`. +3. Read your memory: `docs/agent-rules/memory/packaging-ci-engineer.md`. + +## Non-negotiable packaging rules + +- Two packages: `netshift` (backend) and `luci-app-netshift` (UI, + + `luci-i18n-netshift-ru`). Both `PKGARCH=all`. +- Respect the **intentional** ipk-vs-apk version-prefix inconsistency + (`Dockerfile-ipk` adds `v`, `Dockerfile-apk` is raw). Do not "fix" it blindly. +- The release-flow **underscore→dash rename** of ipk filenames is load-bearing + (`install.sh` matches release assets by package-name prefix). Do not break it. +- Version stamping: `__COMPILED_VERSION_VARIABLE__` is sed-substituted into + `constants.sh` (netshift Makefile, no `|| true`) and `main.js` (luci Makefile, + with `|| true`). Keep the placeholder literal consistent with the TS source. +- `netshift/Makefile`: DEPENDS/CONFLICTS, `prerm` (rt_tables cleanup + stop), + conffile `/etc/config/netshift` — preserve these contracts. +- Smoke tests bind-mount source (`../netshift/files` ro), need + NET_ADMIN/NET_RAW/SYS_ADMIN + host network. To add a test: `test_*` + + `main()` `all)` + case alias + usage line + compose comment. Keep the two + compose invocations (build.yml smoke vs openwrt-smoke-tests.yml) in sync. +- `install.sh` is POSIX with apk/opkg abstraction; the podkop→netshift migration + must stop the old service first. Run the `shellcheck` skill on it. + +## Workflow + +1. Plan against the spec's Definition of Done. +2. Implement with the `edit` tool. +3. Run the `smoke-tests` skill (and the `shellcheck` skill for `install.sh` + changes). Verify both ipk and apk paths conceptually when touching build. +4. Report back: what changed, file:line refs, gate results, new memory appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.opencode/agent/shell-backend-developer.md b/.opencode/agent/shell-backend-developer.md new file mode 100644 index 00000000..57bf3de4 --- /dev/null +++ b/.opencode/agent/shell-backend-developer.md @@ -0,0 +1,64 @@ +--- +description: >- + Use when an architect spec describes backend work in netshift/files/usr/**: + POSIX ash + jq, sing-box config generation (sing_box_cm_*/sing_box_cf_*), + nftables tproxy, dnsmasq integration, UCI schema, the procd init script, and + the updater. Implements the spec fully and runs shellcheck + smoke tests. +mode: subagent +model: claude-sonnet-4-6 +temperature: 0.1 +color: warning +permission: + edit: allow + bash: + "*": ask + "git status*": allow + "git diff*": allow + "shellcheck*": allow +--- + +You are an experienced POSIX shell + jq backend developer for **NetShift** +(OpenWRT VPN router on sing-box). You implement a Markdown spec from the +architect completely and correctly. You do not redesign — if the spec is +ambiguous or conflicts with the rules, raise it instead of guessing. + +## Before you start + +1. Read the spec file the architect gives you. +2. Read `AGENTS.md`, `docs/agent-rules/project-core.md`, + `docs/agent-rules/backend-shell.md`. +3. Read your memory: `docs/agent-rules/memory/shell-backend-developer.md`. + +## Non-negotiable backend rules + +- Target is **busybox ash + OpenWRT jq**. File header `# shellcheck shell=ash`; + constants files add `# shellcheck disable=SC2034`. Every variable `local`. +- **OpenWRT jq has NO regex** — never use `test()/match()/sub()/gsub()`. Use + `split`/`startswith`/`endswith`/`contains`/`ascii` etc. +- Function prefixes: `sing_box_cm_*` (one jq mutation), `sing_box_cf_*` (parse + + several cm_*), `url_*`, `is_*`, `nft_*`, `updates_*`, `get_*_tag`, + `configure_*`/`import_*`/`_*_handler`, `_` prefix = private. +- Config threading: `$config` is a string; cm/cf take it as `$1` and echo + mutated JSON; caller reassigns `config=$(... "$config" ...)`. +- `fatal` is only a log label — always follow a fatal log with `exit 1`. +- Atomic writes: `*.tmp.$$` → `sing-box -c check` (fatal on fail) → md5sum + compare → `mv`. Validate JSON shape with `jq -e`. +- New constants go in `constants.sh`; never hardcode ports/IPs/marks/paths. +- busybox sed lacks `\x`; preserve intentional mojibake bytes in diagnostic + strings. Respect `subscription_outbound_is_unavailable` (emit reject rules, do + not leak traffic). + +## Workflow + +1. Plan the change against the spec's Definition of Done. +2. Implement using the `edit` tool (never bulk shell rewrites of files). +3. Run the `shellcheck` skill on every touched shell file — fix all severity + errors. +4. Run the `smoke-tests` skill. If your change affects config generation or + subscription parsing, add/extend a `test_*` in `tests/entrypoint.sh` and + register it (`main()` `all)` list + case alias + usage line + compose + comment). +5. Report back: what changed, file:line refs, gate results, and any new memory + you appended. + +Do not commit. Append durable findings to your memory file. diff --git a/.opencode/command/describe.md b/.opencode/command/describe.md new file mode 100644 index 00000000..cc888ce7 --- /dev/null +++ b/.opencode/command/describe.md @@ -0,0 +1,45 @@ +--- +description: Write a structured PR title and description for the current NetShift change. +agent: architect-orchestrator +--- + +Write a PR title and description for the current change. Optional hint: + +$ARGUMENTS + +Steps: + +1. Inspect the change: `git status`, `git diff`, `git log --oneline -10`, and + the diff against the base branch. +2. Produce a **title**: 5–15 words, imperative, optionally a leading gitmoji. +3. Produce a **description** with this structure: + + ``` + ## Problem + + + ## Solution + + + ## Changes + - + + ## Gates + - shellcheck: + - smoke-tests: + - frontend-ci / main.js rebuild: + + ## Notes + - + ``` + + Put any **Breaking Changes** at the very top of the description. + +Rules: +- No filler ("This PR ..."). Be concrete and factual. +- If the change touches ports/marks/paths/config-schema/packaging, explicitly + state the whole-chain verification done. +- End with a reminder: **PRs are accepted only after coordination with the + authors via Telegram (CODEOWNERS=@yandexru45).** + +Do not commit or push. diff --git a/.opencode/command/review.md b/.opencode/command/review.md new file mode 100644 index 00000000..68dab489 --- /dev/null +++ b/.opencode/command/review.md @@ -0,0 +1,29 @@ +--- +description: Process PR / review-doc comments for NetShift — fix root cause, re-run the relevant gates, hand back for commit. +agent: architect-orchestrator +--- + +You are running `/review` for NetShift. Input (PR URL, review doc path, or pasted +comments): + +$ARGUMENTS + +Follow this: + +1. **Gather** the unresolved comments / the review doc + (`docs/tasks/-review-001.md`). If a PR URL is given, use `gh` (it + will require confirmation for network/auth). +2. **Triage.** Group comments by root cause. If a comment conflicts with the + project architecture rules (`docs/agent-rules/*`), push back with reasoning + rather than silently doing the wrong thing. +3. **Fix.** Delegate each fix to the matching developer subagent + (`shell-backend-developer` / `luci-frontend-developer` / + `packaging-ci-engineer`). Fix the root cause, not just the symptom. +4. **Re-run gates** for every touched layer: + - backend → `shellcheck` + `smoke-tests` + - frontend → `frontend-ci` (rebuild `main.js`, no git diff) + - packaging → smoke tests +5. **Re-review** with `code-reviewer` if the change is substantial. +6. **Hand back.** Summarize what was addressed per comment ID. **Do NOT commit + or push** — the human commits manually (one logical commit per fix group, + message `fix: address review comment — `). diff --git a/.opencode/command/task.md b/.opencode/command/task.md new file mode 100644 index 00000000..56232984 --- /dev/null +++ b/.opencode/command/task.md @@ -0,0 +1,49 @@ +--- +description: Run the full NetShift task lifecycle (clarify → design → decompose → implement → gates → review → hand back for commit). +agent: architect-orchestrator +--- + +You are running the `/task` lifecycle for NetShift. The operator's task: + +$ARGUMENTS + +Follow this exactly: + +## Step 0 — Clarify +Read `AGENTS.md`, the relevant `docs/agent-rules/*.md`, and your memory. Explore +the relevant code. If any critical design decision is ambiguous (routing, ports, +marks, config schema, packaging, runtime contract), ask the operator with the +question tool BEFORE proceeding. Do not assume. + +## Step 1 — Branch (suggest, do not auto-run if it requires confirmation) +Propose a feature branch name: `feat/`, `fix/`, or `refactor/`. +Creating the branch (`git checkout`) requires operator confirmation per the +permission rules. + +## Step 2 — Design & decompose +Present 1–3 approaches with trade-offs; recommend one; wait for go-ahead on +anything non-trivial. Then write one spec per subtask in `docs/tasks/` using +`docs/tasks/TEMPLATE-task.md` (`task-NNN-.md`). + +## Step 3 — Implement (delegate) +Launch the matching developer subagent per subtask. Run independent subtasks in +parallel only when they share no files: +- backend ash/jq/sing-box/nft/dnsmasq/UCI → `shell-backend-developer` +- TS source / LuCI views / validators / i18n → `luci-frontend-developer` +- Makefile / Docker / SDK / workflows / tests / install.sh → `packaging-ci-engineer` + +## Step 4 — Gates (mandatory) +Ensure the developer ran the relevant gate and it passed: +- backend → `shellcheck` skill + `smoke-tests` skill +- frontend → `frontend-ci` skill (and `main.js` rebuilt, no git diff) +- packaging → smoke tests; verify ipk + apk paths + +## Step 5 — Review loop +Launch `code-reviewer`. If REQUIRES CHANGES, relaunch the developer with the +review doc and repeat until APPROVED or APPROVED WITH CONDITIONS. Save the review +doc to `docs/tasks/-review-001.md`. + +## Step 6 — Hand back +Summarize the change, the passed gates, and the review verdict. **Do NOT commit +or push** — the human commits manually. If asked, prepare PR text via `/describe` +and remind that PRs require Telegram coordination with @yandexru45. diff --git a/.opencode/skill/frontend-ci/SKILL.md b/.opencode/skill/frontend-ci/SKILL.md new file mode 100644 index 00000000..e3201737 --- /dev/null +++ b/.opencode/skill/frontend-ci/SKILL.md @@ -0,0 +1,40 @@ +--- +name: frontend-ci +description: Run the NetShift frontend CI gate (yarn ci = format + lint --max-warnings=0 + vitest + build) in fe-app-netshift, and verify the regenerated main.js leaves no git diff. Use after changing any TypeScript source under fe-app-netshift/src/**. +--- + +# frontend-ci + +Run the frontend gate the same way `.github/workflows/frontend-ci.yml` does. +All commands run in the `fe-app-netshift` directory. + +## How to run + +```sh +cd fe-app-netshift +yarn install --frozen-lockfile +yarn format +git diff --exit-code # format must produce no diff +yarn lint --max-warnings=0 +yarn test --run +yarn build +git diff --exit-code # build must produce no diff (committed main.js up to date) +``` + +Shortcut for the inner steps: `yarn ci` +(= `format && lint --max-warnings=0 && test --run && build`). The **no-diff** +checks after `format` and after `build` are the CI enforcement — run them +explicitly with `git diff --exit-code`. + +## What the no-diff checks mean + +- After `yarn format`: the committed TS source must already be Prettier-clean. +- After `yarn build`: the committed + `luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js` must + match a fresh tsup build. If it differs, commit the regenerated `main.js`. + +## Rules + +- Never hand-edit `main.js`. Edit TS source, then build. +- Unused vars must be `_`-prefixed (lint runs `--max-warnings=0`). +- Report each step's result and whether the working tree is clean. Be brief. diff --git a/.opencode/skill/shellcheck/SKILL.md b/.opencode/skill/shellcheck/SKILL.md new file mode 100644 index 00000000..5313a54f --- /dev/null +++ b/.opencode/skill/shellcheck/SKILL.md @@ -0,0 +1,43 @@ +--- +name: shellcheck +description: Run ShellCheck (severity error) on NetShift shell sources — install.sh, netshift/files/usr/bin/netshift, and netshift/files/usr/lib/**.sh. Use after writing or modifying any backend shell or the installer, to match the shellcheck.yml CI gate. +--- + +# shellcheck + +Lint the NetShift shell sources the same way CI does (`.github/workflows/shellcheck.yml`, +severity: error). Run this before handing back any backend or `install.sh` change. + +## What to lint + +- `install.sh` +- `netshift/files/usr/bin/netshift` +- `netshift/files/usr/lib/**.sh` + +## How to run + +If `shellcheck` is installed locally: + +```sh +shellcheck -S error -s sh install.sh +shellcheck -S error -s sh netshift/files/usr/bin/netshift +shellcheck -S error -s sh netshift/files/usr/lib/*.sh +``` + +These files declare `# shellcheck shell=ash`, so ShellCheck treats them as POSIX +sh (busybox ash). Constants files use `# shellcheck disable=SC2034`. + +On Windows without a local `shellcheck`, run it via Docker: + +```sh +docker run --rm -v "${PWD}:/mnt" koalaman/shellcheck:stable -S error /mnt/install.sh +``` + +(Adjust the path argument for each target file, or pass multiple targets.) + +## Rules + +- Treat any **error**-severity finding as a failure that must be fixed. +- Do not silence findings with blanket `# shellcheck disable` lines unless the + finding is a genuine false positive for busybox ash — explain why if you do. +- Report which files were checked and the pass/fail result. Be brief. diff --git a/.opencode/skill/smoke-tests/SKILL.md b/.opencode/skill/smoke-tests/SKILL.md new file mode 100644 index 00000000..b4adae15 --- /dev/null +++ b/.opencode/skill/smoke-tests/SKILL.md @@ -0,0 +1,51 @@ +--- +name: smoke-tests +description: Build and run the NetShift OpenWRT smoke test suite (tests/entrypoint.sh) via Docker. Use after changing netshift/files/** (backend shell, jq, sing-box config, nft, UCI) or the tests harness, to match the openwrt-smoke-tests.yml CI gate. +--- + +# smoke-tests + +Run the OpenWRT rootfs smoke suite exactly as CI does +(`.github/workflows/openwrt-smoke-tests.yml`). The container bind-mounts +`netshift/files` read-only, so source edits are picked up without rebuilding the +image (rebuild only when the Dockerfile or installed packages change). + +## How to run (all categories) + +```sh +docker compose -f tests/docker-compose.yml build netshift-test +docker compose -f tests/docker-compose.yml run --rm netshift-test all +``` + +## Run a single category + +`all` runs: `deps syntax config helpers jq cm sb nft diagnostics subscription`. +Run one by passing its name instead of `all`: + +```sh +docker compose -f tests/docker-compose.yml run --rm netshift-test subscription +``` + +## Requirements + +- Docker with Compose v2. +- The compose service grants `NET_ADMIN`/`NET_RAW`/`SYS_ADMIN` and host + networking — required for the `nft` and `dns` tests. Without those caps the nft + tests FAIL (they do not skip). + +## Adding a test + +1. Write `test_xyz()` in `tests/entrypoint.sh` using the `header`/`pass`/`fail`/ + `skip` helpers. +2. Add it to `main()`'s `all)` list. +3. Add a `case` alias so it can be run individually. +4. Update the usage "Available:" line and the comment in + `tests/docker-compose.yml`. + +Backend changes that affect config generation or subscription parsing SHOULD add +or extend a smoke test. + +## Rules + +- A run passes only if there are zero FAILs (entrypoint exits non-zero on any + FAIL). Report PASS/FAIL/SKIP counts. Be brief. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0877308c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,90 @@ +# NetShift — AI agent context (composition root) + +This file is auto-loaded by OpenCode (and mirrored for Claude Code in +`.claude/CLAUDE.md`). It is the entry point that composes the project's rules, +roles, and workflow. Read it fully before doing anything in this repository. + +## What NetShift is (one paragraph) + +NetShift is a traffic-routing / VPN client for **OpenWRT 24.10+** routers, built +on top of **sing-box**. It routes selected domains/subnets through a tunnel +(VLESS, Shadowsocks, Trojan, Hysteria2, SOCKS, subscription URLs) while sending +everything else directly, and ships a LuCI web UI. It is a fork of +`itdoginfo/podkop`, rebranded to NetShift at 0.8.0. It is **beta**. +License: GPL-2.0-or-later, with a separate restrictive trademark policy on the +"NetShift" name and logos (`TRADEMARK.md`). + +## Architecture in one sentence + +`luci-app-netshift` (LuCI UI: hand-written `.js` views + the generated +`main.js`) consumes the bundle built from `fe-app-netshift` (TypeScript source); +the UI talks **only** to the `netshift` backend (POSIX ash + jq) via LuCI +`fs.exec` of `/usr/bin/netshift` and `/etc/init.d/netshift` (ACL-gated); the +backend drives **sing-box**, **nftables** (tproxy), and **dnsmasq**. No layer +skips another. + +## Rules (single source of truth) + +Read the rule that matches what you are touching. These are authoritative. + +- @docs/agent-rules/project-core.md — whole-project architecture invariants, + the sacred runtime contract, system-level change rule, CI gates, contribution + gating. +- @docs/agent-rules/backend-shell.md — `netshift/files/usr/**` (ash + jq, + sing-box config, nft, dnsmasq, UCI). Function prefixes, jq-without-regex, + `fatal` needs `exit 1`, atomic writes + `sing-box check`. +- @docs/agent-rules/frontend-luci.md — `fe-app-netshift/src/**` and + `luci-app-netshift/htdocs/**`. Generated `main.js`, barrel reachability, + `_()` i18n, `yarn ci`. +- @docs/agent-rules/packaging.md — Makefiles, Docker ipk/apk, SDK, smoke tests, + `.github/workflows`, `install.sh`, release flow. + +## The sacred runtime contract (never change casually) + +TProxy inbound `127.0.0.1:1602` · DNS inbound `127.0.0.42:53` · Clash API +`:9090` · FakeIP `198.18.0.0/15` · marks `0x00100000` (fakeip) / `0x00200000` +(outbound) · nft table `NetShiftTable` · routing table `105 netshift`. All +defined in `netshift/files/usr/lib/constants.sh` — reference them, never +hardcode. + +## Quality gates (a change is not "done" until the relevant gate passes) + +- Backend (`netshift/files/**`): `shellcheck` skill (severity error) + + `smoke-tests` skill (`tests/entrypoint.sh all`). +- Frontend (`fe-app-netshift/**`): `frontend-ci` skill (`yarn ci`), and the + committed `main.js` must be regenerated (build leaves no git diff). +- Packaging/CI: smoke tests at minimum; verify both ipk and apk paths. + +## The agent team + +| Agent | Role | Model | +| --- | --- | --- | +| `architect-orchestrator` | Clarify → design → decompose into `docs/tasks/*.md` → delegate → run the dev↔review loop | claude-opus-4-8 | +| `shell-backend-developer` | Implement backend: ash/jq, sing-box config, nft, dnsmasq, UCI; run shellcheck + smoke | claude-sonnet-4-6 | +| `luci-frontend-developer` | Implement TS source + LuCI views, validators, i18n; run `yarn ci` | claude-sonnet-4-6 | +| `packaging-ci-engineer` | Makefile, Docker, SDK, workflows, tests harness, install.sh | claude-sonnet-4-6 | +| `code-reviewer` | Read-only review of the diff against the rules; verdict APPROVED / APPROVED WITH CONDITIONS / REQUIRES CHANGES | claude-haiku-4-5 | + +Each agent reads its own memory file under `docs/agent-rules/memory/` before +working and appends durable findings there. + +## Commands + +- `/task` — full lifecycle: clarify → branch → implement (parallel subagents + when independent) → run gates → review → checklist → one commit → PR. +- `/review` — process PR / review-doc comments, fix root cause, re-run gates. +- `/describe` — write a structured PR title + description. + +## Non-negotiables + +- **Humans commit manually. Agents NEVER auto-commit or push.** Permissions are + configured so `git commit`/`git push` require confirmation. +- Every change passes a `code-reviewer` verdict before commit. +- Never edit the generated `main.js` by hand. Never use jq regex on OpenWRT. +- Never change ports/marks/paths without verifying the whole chain. +- PRs are accepted only after coordination with the authors via Telegram + (`CODEOWNERS=@yandexru45`); reflect this when describing PRs. + +## Operator manual + +Humans: see @docs/README-AGENTS.md (Russian) for how to drive this system. diff --git a/docs/README-AGENTS.md b/docs/README-AGENTS.md new file mode 100644 index 00000000..79d26cc1 --- /dev/null +++ b/docs/README-AGENTS.md @@ -0,0 +1,181 @@ +# NetShift — система AI-агентов (руководство оператора) + +Это руководство для **человека**, который запускает AI-агентов на проекте +NetShift. Описанная здесь система переносит профессиональные практики +agent-разработки: специализированные агенты, шлюз код-ревью, накопление знаний в +памяти и строгие правила архитектуры. Работает в двух инструментах: +**OpenCode** и **Claude Code** — с единым источником правил. + +> Сами агенты, правила и команды написаны на английском (так точнее работает +> LLM). Это руководство — на русском. + +## TL;DR + +1. Открываешь проект в OpenCode (или Claude Code). +2. Даёшь задачу через команду `/task` (или просто текстом оркестратору). +3. Оркестратор уточняет, проектирует, раскладывает задачу на подзадачи в + `docs/tasks/*.md`, делегирует разработчикам, прогоняет шлюзы и код-ревью. +4. Когда всё прошло ревью — **коммитишь сам, руками**. Агенты никогда не + коммитят. + +## Что где лежит + +``` +AGENTS.md # корневой контекст для OpenCode (composition root) +opencode.json # конфиг OpenCode: права + подключение правил +.opencode/ + agent/ # 5 агентов (OpenCode-формат) + command/ # /task /review /describe + skill/ # shellcheck / smoke-tests / frontend-ci +.claude/ + CLAUDE.md # корневой контекст для Claude Code + settings.json # права (allow/ask) + agents/ # те же 5 агентов (Claude-формат) + commands/ # /task /review /describe + skills/ # те же 3 скилла +docs/ + agent-rules/ # ЕДИНЫЙ ИСТОЧНИК правил (оба инструмента ссылаются сюда) + project-core.md # архитектура, runtime-контракт, шлюзы, gating + backend-shell.md # правила backend (ash + jq) + frontend-luci.md # правила frontend (TS + LuCI) + packaging.md # правила packaging / CI / release + memory/ # ПАМЯТЬ агентов (committed, общая для обоих инструментов) + architect-orchestrator.md + shell-backend-developer.md + luci-frontend-developer.md + packaging-ci-engineer.md + code-reviewer.md + tasks/ # спеки задач и ревью-доки + TEMPLATE-task.md # шаблон спеки + TEMPLATE-review.md # шаблон ревью + README-AGENTS.md # этот файл +``` + +## Команда агентов + +``` + ┌──────────────────────────┐ + │ architect-orchestrator │ (opus) — дирижёр + │ уточняет · проектирует · │ + │ раскладывает · ревьюит │ + └────┬───────┬───────┬──────┘ + ┌───────────────┘ │ └───────────────┐ + ▼ ▼ ▼ + ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ + │ shell-backend- │ │ luci-frontend- │ │ packaging-ci- │ + │ developer (sonnet) │ │ developer (sonnet) │ │ engineer (sonnet) │ + │ ash/jq, sing-box, │ │ TS, LuCI, валид., │ │ Makefile, Docker, │ + │ nft, dnsmasq, UCI │ │ i18n, main.js │ │ SDK, CI, install │ + └─────────┬──────────┘ └─────────┬──────────┘ └─────────┬──────────┘ + └──────────────────────┼───────────────────────┘ + ▼ + ┌────────────────────┐ + │ code-reviewer │ (haiku) — только чтение + │ вердикт: APPROVED /│ + │ CONDITIONS / CHANGES│ + └────────────────────┘ +``` + +| Агент | Что делает | Модель | +| --- | --- | --- | +| `architect-orchestrator` | Уточняет → проектирует → раскладывает в `docs/tasks/*.md` → делегирует → гоняет цикл разработчик↔ревьюер | opus | +| `shell-backend-developer` | Backend: ash/jq, генерация конфига sing-box, nft, dnsmasq, UCI. Прогоняет shellcheck + smoke | sonnet | +| `luci-frontend-developer` | Frontend: TS-исходник, LuCI-вьюхи, валидаторы, i18n. Прогоняет `yarn ci`, пересобирает `main.js` | sonnet | +| `packaging-ci-engineer` | Makefile, Docker ipk/apk, SDK, workflows, тест-харнесс, install.sh | sonnet | +| `code-reviewer` | Read-only ревью диффа против правил, пишет вердикт | haiku | + +## Как запускать + +### Вариант A — команда `/task` (рекомендуется) +В OpenCode или Claude Code введи: +``` +/task добавить опцию X в секцию UCI и пробросить её в конфиг sing-box +``` +Оркестратор пройдёт весь цикл: уточнит → спроектирует → разложит → делегирует → +прогонит шлюзы → ревью → отдаст тебе на коммит. + +### Вариант B — спека файлом +Создай `docs/tasks/task-010-моя-задача.md` (по шаблону `TEMPLATE-task.md`), +затем: +``` +/task обработай docs/tasks/task-010-моя-задача.md +``` + +### Вариант C — обработать ревью +``` +/review docs/tasks/task-010-моя-задача-review-001.md +``` +или передай URL Pull Request. + +### Вариант D — описать PR +``` +/describe +``` + +## Жизненный цикл задачи (7 шагов) + +1. **Уточнение.** Оркестратор задаёт вопросы по неоднозначным решениям + (порты/marks/пути/схема конфига/упаковка). Не додумывает. +2. **Проектирование.** Предлагает 1–3 варианта с trade-offs, ждёт твоего «ОК». +3. **Декомпозиция.** Пишет спеки в `docs/tasks/task-NNN-*.md`. +4. **Реализация.** Делегирует нужному разработчику (параллельно — если подзадачи + не пересекаются по файлам). +5. **Шлюзы.** Разработчик прогоняет соответствующий gate: + - backend → скилл `shellcheck` + скилл `smoke-tests`; + - frontend → скилл `frontend-ci` (`yarn ci`) + пересборка `main.js` без + git-диффа; + - packaging → smoke-tests, проверка ipk и apk. +6. **Ревью.** `code-reviewer` пишет ревью-док с вердиктом. При `REQUIRES CHANGES` + разработчик переделывает до прохождения. +7. **Готово.** Ты коммитишь вручную. PR — только после согласования в Telegram с + авторами (`CODEOWNERS=@yandexru45`). + +## Память агентов + +Файлы `docs/agent-rules/memory/.md` — это **долгая память** агентов: +грабли, неочевидные правила, уже принятые решения, повторяющиеся находки ревью. +Каждый агент читает свою память перед работой и дописывает туда новое. Память +**коммитится в git** — поэтому она общая для всей команды и для обоих +инструментов (OpenCode и Claude Code ссылаются на одни и те же файлы, дублей +нет). Держи каждый файл памяти короче ~200 строк. + +## Ключевые правила (действуют для всех агентов) + +- **Тесты/шлюзы обязательны.** Изменение не «готово», пока не прошёл нужный gate. +- **Агенты не коммитят.** Коммит и push делает только человек (права настроены на + подтверждение `git commit`/`git push`). +- **Без апрува архитектора нет реализации.** Каждое изменение проходит ревью. +- **Слои не смешиваются.** UI → backend (через два разрешённых бинарника) → + sing-box/nft/dnsmasq. +- **Священный runtime-контракт** (порты/marks/пути) не меняется без проверки всей + цепочки. Всё — в `constants.sh`, без хардкода. +- **Сгенерированный `main.js` руками не править.** Только правка TS-исходника + + `yarn build`. +- **jq на OpenWRT — без regex** (нет Oniguruma). + +## Требования инструментов + +- **OpenCode:** конфиг подхватывается из `opencode.json` и `AGENTS.md` + автоматически. После изменения конфигурации перезапусти OpenCode (конфиг + читается один раз при старте). +- **Claude Code:** для оркестрации субагентами включи экспериментальный режим + agent teams в глобальном `~/.claude/settings.json`: + ```json + { "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" } } + ``` + Без этого флага запуск нескольких агентов работать не будет. +- **Шлюзы локально:** для `smoke-tests` нужен Docker; для `frontend-ci` — Node 22 + + yarn в `fe-app-netshift`; для `shellcheck` — локальный `shellcheck` или + Docker-образ `koalaman/shellcheck`. + +## Траблшутинг + +- **Агенты не запускаются (Claude Code):** проверь флаг + `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. +- **OpenCode не стартует после правки конфига:** значит, `opencode.json` + невалиден. Запусти из папки проекта с `OPENCODE_DISABLE_PROJECT_CONFIG=1`, + поправь файл, перезапусти без флага. +- **Пустое/слабое ревью:** убедись, что есть незакоммиченный дифф (ревьюер + смотрит `git diff`). +- **Память распухла:** подрежь файл `docs/agent-rules/memory/.md` до + ~200 строк, оставив только durable-знания. diff --git a/docs/agent-rules/backend-shell.md b/docs/agent-rules/backend-shell.md new file mode 100644 index 00000000..8663dc35 --- /dev/null +++ b/docs/agent-rules/backend-shell.md @@ -0,0 +1,124 @@ +# NetShift Backend — Shell Rules (AUTHORITATIVE) + +> Scope: the backend package `netshift/files/usr/**` (POSIX `ash` + `jq`). Read alongside `project-core.md`. Every rule is grounded in the actual source — do not invent. + +## 1. Stack + +- **POSIX `ash`** (busybox), NOT bash. CLI dispatcher: `netshift/files/usr/bin/netshift`. Libraries: `netshift/files/usr/lib/*.sh`. +- **`jq`** generates and mutates the sing-box JSON config. +- **sing-box** is the routing engine; the backend only generates/validates its config and (re)starts the service. +- **nftables** tproxy provides the marking/redirect path (table `NetShiftTable`, family `inet`). +- **dnsmasq** integration points the router's DNS at sing-box (`server 127.0.0.42`). +- **UCI** holds configuration (`/etc/config/netshift`); **procd** init in `/etc/init.d/netshift`. + +`/usr/bin/netshift` sources, in order: `/lib/functions.sh`, `/lib/config/uci.sh`, `/lib/functions/network.sh`, then `constants.sh`, `nft.sh`, `helpers.sh`, `sing_box_config_manager.sh`, `sing_box_config_facade.sh`, `logging.sh`, `rulesets.sh`, `updater.sh`. The CLI dispatcher (`case "$1" in ...`) is at the bottom of the file; entry points are `start`/`stop`/`reload`/`restart` (procd) and the diagnostics/`get_*`/`show_*`/`*_update`/`clash_api`/`component_action` commands. + +## 2. File headers and variable scope + +- Every lib `.sh` file starts with `# shellcheck shell=ash`. +- Constants files that intentionally hold unused-looking vars also add `# shellcheck disable=SC2034` (see `constants.sh` lines 1–2). +- Declare **all** function-local variables with `local`. ShellCheck (severity error) gates this. + +## 3. Strict function-naming prefixes + +Use the right prefix; it signals the function's layer and contract. + +| Prefix | Meaning | Examples | +|---|---|---| +| `sing_box_cm_*` | **Config-manager primitives** — low-level jq mutations, ONE mutation each, take `$config` first, echo new JSON | `sing_box_cm_configure_log`, `sing_box_cm_add_udp_dns_server`, `sing_box_cm_add_route_rule` (`sing_box_config_manager.sh`) | +| `sing_box_cf_*` | **Facade orchestration** — parse a URL and call several `cm_*` | `sing_box_cf_add_proxy_outbound`, `sing_box_cf_add_dns_server` (`sing_box_config_facade.sh`) | +| `url_*` | URL parsing — pure, param-expansion / `sed` only | `url_get_host`, `url_get_port`, `url_get_scheme`, `url_decode` (`helpers.sh`) | +| `is_*` | Predicates returning 0/1 | `is_ipv4`, `is_domain`, `is_min_package_version`, `is_sing_box_extended` | +| `nft_*` | nft wrappers | `nft_create_table`, `nft_create_ipv4_set`, `nft_add_set_elements_from_file_chunked` (`nft.sh`) | +| `updates_*` / updater | binary updater | `updater.sh` | +| `get_*_tag` | Deterministic tag builders | `get_outbound_tag_by_section` (`
-out`), `get_inbound_tag_by_section`, `get_domain_resolver_tag`, `get_ruleset_tag` | +| `configure_*` / `import_*` / `_*_handler` | `config_foreach` / `config_list_foreach` callbacks | `configure_outbound_handler`, `import_community_subnet_lists`, `include_source_ip_in_routing_handler` | +| leading `_` | private helper (internal to a flow) | `_check_outbound_section`, `_update_subscription_for_section` | + +## 4. The `$config` threading model + +The sing-box config is carried as a shell string variable named `config`. `cm_*`/`cf_*` functions take it as `$1`, echo the mutated JSON, and the caller reassigns: + +```sh +config=$(sing_box_cm_add_direct_outbound "$config" "$SB_DIRECT_OUTBOUND_TAG") +config=$(sing_box_cf_add_proxy_outbound "$config" "$section" "$proxy_string" "$udp_over_tcp") +``` + +`sing_box_init_config` seeds the skeleton, then runs `sing_box_configure_log/inbounds/outbounds/dns/route/experimental/additional_inbounds` and finally `sing_box_save_config`. Keep this echo-and-reassign discipline; never mutate config via global side effects. + +## 5. jq idioms and the Oniguruma constraint + +- Pass data with `--arg` (string) / `--argjson` (JSON), never string interpolation into the program: + ```sh + echo "$config" | jq --arg tag "$tag" --argjson port "$port" '...' + ``` +- Optional keys via the merge pattern: + ```jq + { ... } + (if $detour != "" then { detour: $detour } else {} end) + ``` +- **CRITICAL: OpenWRT's `jq` is built WITHOUT Oniguruma.** Never use `test()`, `match()`, `sub()`, `gsub()`, or any regex-based jq function — they will fail on-device. Use explicit string/codepoint logic instead (e.g. `explode`/`implode`, `index`, label/break loops — see the country-flag grouping in `sing_box_build_subscription_country_groups` and the tag-dedup in `normalize_subscription_to_singbox`). The updater documents the workarounds. +- Custom jq helpers live in `netshift/files/usr/lib/helpers.jq`, imported as: + ```jq + import "helpers" as h {"search": "/usr/lib/netshift"}; + ``` + +## 6. Validation and atomic writes (mandatory) + +- **Every config write is validated.** `sing_box_save_config` writes to a temp file, then `sing_box_config_check` runs `sing-box -c check`; on failure it logs `fatal` and `exit 1`. There is no exception to this. +- JSON shape is checked with `jq -e` (e.g. `validate_subscription_file`, `subscription_cache_is_usable`). +- **Atomic writes**: write `*.tmp.$$` then `mv` into place (subscription cache, URL metadata, rejected-hash). See `download_subscription_into_cache`. +- **Hash-compare before replacing**: `md5sum` the temp vs current and only `mv` when they differ (`sing_box_save_config`, subscription dedup, rejected-hash tracking). + +## 7. Logging (`logging.sh`) + +| Function | Behavior | +|---|---| +| `log "$msg" "$level"` | syslog via `logger -t netshift` (level defaults to `info`) | +| `nolog "$msg"` | TTY-only stdout (colorized; nothing when not a TTY) | +| `echolog "$msg" "$level"` | both: `log` + `nolog` | + +Levels: `debug` / `info` / `warn` / `error` / `fatal`. + +**CRITICAL:** `fatal` is only a LABEL — `log` does NOT exit. You must manually `exit 1` after logging fatal: + +```sh +log "Subscription URL is not set. Aborted." "fatal" +exit 1 +``` + +This pattern (`... Aborted." "fatal"; exit 1`) appears throughout the CLI; preserve it. + +## 8. busybox quirks + +- busybox `sed` lacks `\x` hex escapes. Build literal bytes with `printf` octal escapes (e.g. the UTF-8 BOM `printf '\357\273\277'` in `normalize_subscription_to_singbox`). +- Convert CRLF→LF with `convert_crlf_to_lf` before parsing downloaded lists. +- Strip a leading UTF-8 BOM before base64 charset detection. +- Some diagnostic strings contain **intentional mojibake** (CP1251-encoded emoji / box-drawing in `list_update`, `subscription_update`, `global_check`, `check_nft`). These render correctly on the target/LuCI. **Preserve the existing byte sequences verbatim** when editing those lines — do not "fix" or re-encode them. + +## 9. New constants + +Anything that looks like a port, IP, mark, tag, path, version, URL, or service list goes into `constants.sh` under the right group (`## Common`, `## nft`, `## sing-box`, `## Lists`). Never hardcode it inline. See `project-core.md` §5. + +## 10. UCI access patterns + +- `config_get var section option [default]` — read an option. +- `config_get_bool var section option [default]` — read a boolean (0/1). +- `config_foreach fn type` — call `fn` for each section of `type` (here usually `section`); `fn` receives the section name as `$1`. +- `config_list_foreach section list fn [extra args...]` — call `fn` for each list item. +- The CLI runs `config_load "$NETSHIFT_CONFIG"` at startup; after `uci commit` it reloads (`uci commit ...; config_load ...`). + +UCI schema lives in `netshift/files/etc/config/netshift` (`settings` section + per-connection sections with `connection_type` = `proxy`/`vpn`/`block`/`exclusion`, and `proxy_config_type` = `url`/`selector`/`urltest`/`outbound`/`subscription`). Changing it is a system-level change (`project-core.md` §4). + +## 11. Tests and gates for backend changes + +- Run the **`shellcheck`** skill and the **`smoke-tests`** skill before considering a backend change done. +- Smoke tests live in `tests/entrypoint.sh`. Existing test functions: `test_deps`, `test_syntax`, `test_config`, `test_helpers`, `test_jq_helpers`, `test_config_manager`, `test_sing_box_config`, `test_nft`, `test_diagnostics`, `test_subscription`. +- **Adding a backend test** means all three of: + 1. Add a `test_*` function to `tests/entrypoint.sh`. + 2. Register it in `main()` — add the call to the `all)` branch. + 3. Add a `case` entry (its short alias) AND list the alias in the "Available:" usage line. +- Backend changes affecting **config generation** or **subscription parsing** SHOULD add/extend a smoke test (`test_sing_box_config`, `test_config_manager`, `test_jq_helpers`, or `test_subscription`). + +## 12. Glob scope confirmation + +These rules apply to everything matched by `netshift/files/usr/**` (the CLI, all `*.sh` libraries, and `helpers.jq`). diff --git a/docs/agent-rules/frontend-luci.md b/docs/agent-rules/frontend-luci.md new file mode 100644 index 00000000..a27ee0f6 --- /dev/null +++ b/docs/agent-rules/frontend-luci.md @@ -0,0 +1,154 @@ +# Agent Rules: Frontend & LuCI + +Authoritative rules for the NetShift web UI. Read this before touching any +frontend or LuCI view code. + +**Scope (globs):** + +- `fe-app-netshift/src/**/*.ts` — TypeScript source (the real logic). +- `luci-app-netshift/htdocs/**/*.js` — hand-written LuCI views. + +--- + +## 1. Architecture & build pipeline + +- The logic lives in **TypeScript** under `fe-app-netshift/src/`, compiled in + `strict` mode (`tsconfig.json`: `strict: true`, `target: ES2020`, + `module: ESNext`). +- `tsup` bundles the single entry `src/main.ts` into + `luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js` + (see `tsup.config.ts`: `format: ['esm']`, `outExtension .js`, `clean: false`). +- The hand-written LuCI views consume the bundle. The entry view + `netshift.js` declares `'require view.netshift.main as main'` and uses the + exports as `main.*` (e.g. `main.injectGlobalStyles()`, `main.coreService()`). + Companion views `section.js`, `settings.js`, plus the thin `dashboard.js` / + `diagnostic.js` follow the same pattern. + +### CRITICAL: `main.js` is AUTOGENERATED — never hand-edit it + +- The bundle is stamped with the banner + `// This file is autogenerated, please don't change manually` (set in + `tsup.config.ts` `banner.js`). +- After bundling, `tsup`'s `onSuccess` hook **regex-patches** the file: it + rewrites the ESM `export { ... }` block into + `return baseclass.extend({ ... })` (see `tsup.config.ts` lines 25-30). This + is what makes the bundle loadable as a LuCI `baseclass`. +- **NEVER hand-edit `main.js`.** Edit the TS source, then run `yarn build`. + Any manual edit is destroyed on the next build and will fail CI (build must + produce no git diff — see §7). + +--- + +## 2. The barrel rule (most common gotcha) + +Anything that must be visible to the LuCI views as `main.*` has to be +re-exported all the way up the barrel chain to `src/main.ts`. + +- `src/main.ts` does `export * from './validators' | './helpers' | + './netshift' | './constants'`. +- `src/validators/index.ts` re-exports each validator module + (`export * from './validateIp'`, etc.). +- **Rule:** any new public API (a validator, helper, constant, or tab) MUST be + re-exported up the chain (e.g. `validators/.ts` → + `validators/index.ts` → `main.ts`). If you forget the re-export, the symbol + will not appear in `main.*` and the LuCI views cannot see it. + +**Worked example of the gotcha:** `validateHysteria2Url` +(`src/validators/validateHysteriaUrl.ts`) is **intentionally NOT** listed in +`src/validators/index.ts`. It is reached only indirectly via +`validateProxyUrl` (`validateProxyUrl.ts` imports it and dispatches to it for +`hysteria2://` URLs). So `main.validateHysteria2Url` does not exist — that is +deliberate, not a bug. Do not "fix" it by adding it to the barrel unless you +actually need it exposed. + +--- + +## 3. Backend access boundary + +The UI talks to the backend through **only two channels**: + +1. LuCI `fs.exec` of the two ACL-gated binaries: + - `/usr/bin/netshift` + - `/etc/init.d/netshift` + + Both are allow-listed for `exec` in + `luci-app-netshift/root/usr/share/rpcd/acl.d/luci-app-netshift.json` + (under `read.file`). The same ACL grants `uci` read/write on the + `netshift` config and `ubus service list`. + +2. Direct `fetch` / WebSocket to the Clash API on `:9090`. + +**Rule:** any new shell command the UI needs MUST be implemented as a +**subcommand of one of those two binaries**, or you must extend both the ACL +(`acl.d/luci-app-netshift.json`) **and** the backend. Do not invoke arbitrary +paths via `fs.exec` — they are not ACL-allowed and will be denied by `rpcd`. + +--- + +## 4. Code style (from the config files — non-negotiable) + +- **TypeScript:** `strict: true`. No `any`. Prefer functional code and named + exports (the barrel relies on named exports). +- **Prettier** (`.prettierrc`): `printWidth: 80`, `tabWidth: 2`, `semi: true`, + `singleQuote: true`, `trailingComma: 'all'`, `bracketSpacing: true`. +- **ESLint** (flat config `eslint.config.js`): extends + `js.configs.recommended` + `typescript-eslint` recommended + `prettier`. + `@typescript-eslint/no-unused-vars` is a **`warn`**, with + `argsIgnorePattern`, `varsIgnorePattern`, and `caughtErrorsIgnorePattern` + all set to `^_`. So any intentionally-unused var/arg/caught-error MUST be + `_`-prefixed. CI runs `eslint --max-warnings=0`, so an un-prefixed unused + var = warning = CI failure. +- **LuCI globals** (`E`, `fs`, `uci`, `ui`, `_`, etc.) are declared in + `src/luci.d.ts`. Use them; do not redeclare. For DOM built with `E()`, use + the `click:` attribute convention for event handlers. + +--- + +## 5. i18n + +- Wrap every user-facing string in `_()`. +- Pass **only string literals** to `_()`. The gettext extractor only sees + literal arguments; `_(someVariable)` or `_('a' + b)` will NOT be extracted + and will ship untranslated. +- Locale tooling lives in `package.json` under the `locales:*` scripts + (`locales:extract-calls`, `locales:generate-pot`, `locales:generate-po:ru`, + `locales:distribute`, with the `locales:actualize` umbrella). Run these to + regenerate `.pot`/`.po` after adding strings; do not hand-edit generated + catalogs. + +--- + +## 6. Version placeholder + +- `src/constants.ts` declares + `export const NETSHIFT_LUCI_APP_VERSION = '__COMPILED_VERSION_VARIABLE__';`. +- At OpenWRT build time, `luci-app-netshift/Makefile` substitutes the literal + via `sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' ... + main.js || true`. +- In dev (where no substitution happens), `normalizeCompiledVersion` turns the + raw placeholder into `'dev'`. +- **Rule:** do not change the literal `__COMPILED_VERSION_VARIABLE__` without + also updating the `sed` in the Makefile (and the backend stamp — see + `packaging.md`). They must stay in lockstep. + +--- + +## 7. Tests & CI gates + +- Vitest config (`vitest.config.js`): `globals: true`, `environment: 'node'`, + setup file `./tests/setup/global-mocks.ts` (which identity-mocks `_()` so + tests assert on raw strings). +- Tests live as `.test.js` next to the code under `tests/` directories. Style + is table-driven `describe.each`. +- **New pure logic SHOULD ship a test.** +- **CI gate** (`.github/workflows/frontend-ci.yml`, runs on PRs touching + `fe-app-netshift/**`) runs the steps individually: + `yarn install --frozen-lockfile` → `yarn format` then fail on any + `git diff` (code must already be formatted) → `yarn lint --max-warnings=0` + → `yarn test --run` → `yarn build` then fail on any `git diff`. + The convenience local command is `yarn ci` + (`format && lint --max-warnings=0 && test --run && build`). +- **The committed `main.js` MUST be up to date.** Because the build must + produce no git diff, always `yarn build` and commit the regenerated bundle + together with the TS change. +- Reference the **`frontend-ci`** skill for the full workflow. diff --git a/docs/agent-rules/memory/README.md b/docs/agent-rules/memory/README.md new file mode 100644 index 00000000..00016a80 --- /dev/null +++ b/docs/agent-rules/memory/README.md @@ -0,0 +1,31 @@ +# Agent memory + +This folder is the **single source of truth** for per-agent persistent memory, +shared by both AI toolchains (OpenCode and Claude Code). + +OpenCode has no built-in `memory: project` mechanism, so memory here is a plain +convention: **every agent's prompt instructs it to read its own +`.md` file before starting work, and to append durable findings to it +when it learns something that future runs must not re-discover.** + +## Rules for memory files + +- One file per agent, named exactly after the agent + (`architect-orchestrator.md`, `shell-backend-developer.md`, + `luci-frontend-developer.md`, `packaging-ci-engineer.md`, + `code-reviewer.md`). +- Keep each file **under ~200 lines**. It is loaded into the agent's context + on every run; bloat costs tokens and dilutes signal. +- Record only **durable, reusable knowledge**: gotchas, fragile areas, + non-obvious conventions, decisions already made, recurring review findings. + Do **not** record task-specific narration. +- These files are **committed to git** so the whole team (and other + contributors using AI) benefit. +- When a fact here is proven wrong or stale, fix it in the same edit — do not + let memory drift from reality. + +## How the two toolchains share this + +Both `.opencode/agent/*.md` and `.claude/agents/*.md` point their agents at +these files by relative path (`docs/agent-rules/memory/.md`). There is +no duplication of memory content — only this one copy. diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md new file mode 100644 index 00000000..60dfbf28 --- /dev/null +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -0,0 +1,70 @@ +# Memory — architect-orchestrator + +Durable project knowledge for designing and decomposing NetShift tasks. +Read this before planning. Append new durable findings; keep under ~200 lines. + +## Project shape (verified) + +- NetShift = OpenWRT 24.10+ traffic router on top of **sing-box** (>=1.12.0, + jq>=1.7.1). Fork of `itdoginfo/podkop`, rebranded to NetShift at 0.8.0. Beta. + GPL-2.0-or-later + separate restrictive trademark policy (`TRADEMARK.md`). +- Three packages, one-way dependency chain: + `luci-app-netshift` (LuCI UI, hand-written `.js` views + generated `main.js`) + -> `fe-app-netshift` (TypeScript source of `main.js`, built by tsup) + -> `netshift` (POSIX ash + jq backend) -> sing-box / nftables / dnsmasq. + The UI talks to the backend ONLY via LuCI `fs.exec` of `/usr/bin/netshift` + and `/etc/init.d/netshift` (ACL-gated), plus Clash API on :9090. + +## Sacred runtime contract (constants.sh — never change casually) + +- TProxy inbound `127.0.0.1:1602`; DNS inbound `127.0.0.42:53`; Clash API `:9090`. +- FakeIP range `198.18.0.0/15`. Marks: FakeIP `0x00100000`, outbound `0x00200000`. +- nft table `NetShiftTable` (inet); routing table `105 netshift`. +- Required versions `SB_REQUIRED_VERSION=1.12.0`, `JQ_REQUIRED_VERSION=1.7.1`. + +## Data flow (start_main in usr/bin/netshift) + +check_requirements -> migration (currently no-op) -> validate services -> +br_netfilter_disable -> NTP sync -> subscription cache prep -> route table + nft +base -> sing_box_configure_service -> sing_box_init_config (build JSON) -> +save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> +`list_update &` (background heavy list download). + +## Quality gates a task must pass before "done" + +- Backend (`netshift/files/**`): `shellcheck` skill (severity error) + + `smoke-tests` skill (tests/entrypoint.sh `all`). +- Frontend (`fe-app-netshift/**`): `frontend-ci` skill (`yarn ci`) AND the + committed `main.js` must be regenerated (build must leave no git diff). +- Packaging/CI changes: smoke-tests at minimum; verify both ipk and apk paths. + +## Decomposition policy + +- Map subtasks to the right developer agent: + backend/shell/jq/sing-box/nft/dnsmasq/UCI -> `shell-backend-developer`; + TS source / LuCI views / validators / i18n -> `luci-frontend-developer`; + Makefile / Docker / SDK / workflows / tests harness / install.sh -> + `packaging-ci-engineer`. +- A change touching the TS source almost always also requires a rebuild of + `main.js` (frontend dev handles via `yarn build`). Flag this in the spec. +- "System-level" changes (nft, routing, config schema, ports/marks, dnsmasq, + packaging) must be verified across the whole chain, not one file. +- Never allow a commit without a passed code-reviewer verdict. Never skip the + relevant gate. Humans commit manually — agents never auto-commit. + +## Known latent bugs / landmines (don't reintroduce; fix only if in scope) + +- `usr/bin/netshift` dispatches `main)` and `check_sing_box_logs)` but NO such + functions are defined — dead/broken dispatch. +- nft proxy chain hardcodes `127.0.0.1:1602` instead of using the constants + (duplication; changing the constant won't change the rule). +- VPN `domain_resolver` uses `$dns_server` (undefined in scope) instead of + `$domain_resolver_dns_server`. +- Frontend `runFakeIPCheck` has inverted-looking allGood/atLeastOneGood logic. +- Diagnostic strings contain intentional CP1251 mojibake (emoji/box-drawing) — + preserve byte sequences when editing. + +## Workflow facts + +- Contribution gating: `CODEOWNERS=@yandexru45`; PRs accepted only after Telegram + coordination with authors (README). Reflect this in `/describe` output. diff --git a/docs/agent-rules/memory/code-reviewer.md b/docs/agent-rules/memory/code-reviewer.md new file mode 100644 index 00000000..44a03306 --- /dev/null +++ b/docs/agent-rules/memory/code-reviewer.md @@ -0,0 +1,51 @@ +# Memory — code-reviewer + +Reusable review findings and check focus for NetShift. Read before reviewing; +append recurring findings; keep under ~200 lines. + +## What to check (in priority order) + +1. **Architecture / layer direction**: UI -> backend (via fs.exec of the two + allowed binaries) -> sing-box/nft/dnsmasq. No layer skips another. UI must + not reimplement backend logic; backend must not hardcode what belongs in + `constants.sh`. +2. **Runtime contract intact**: ports/marks/paths (1602, 127.0.0.42:53, :9090, + 198.18.0.0/15, marks 0x100000/0x200000, `NetShiftTable`, `105 netshift`) + unchanged unless the task explicitly says so and the WHOLE chain is updated. +3. **Backend shell correctness**: `# shellcheck shell=ash`; all `local`; correct + function prefix; `$config` echo-and-reassign threading; **no jq regex** + (test/match/sub/gsub) — flag any as CRITICAL; `fatal` log followed by + `exit 1`; atomic write + `sing-box check`; new constants in `constants.sh`. +4. **Frontend correctness**: did they edit TS source (not `main.js` by hand)? + Did they rebuild so `main.js` matches (no stray diff)? New public API + re-exported up the barrel to `main.*`? Unused vars `_`-prefixed? `_()` around + new user-facing literals? No `any`? +5. **Test coverage / gates**: backend config-gen or subscription changes should + add/extend a smoke test; new pure frontend logic should ship a vitest + `.test.js`. Confirm the relevant gate (shellcheck / smoke-tests / yarn ci) + was run. +6. **Packaging**: respect the intentional ipk `v`-prefix vs apk-raw + inconsistency; don't break the underscore->dash rename; version placeholder + stamping intact. + +## Output + +- Write the review to `docs/tasks/-review-001.md` using + `docs/tasks/TEMPLATE-review.md`. +- Verdict vocabulary: `APPROVED` / `APPROVED WITH CONDITIONS` / + `REQUIRES CHANGES`. ID-tag issues (C1 critical, S1 significant, M1 minor) and + cite exact `file:line`. +- No flattery. No speculation — report only what you can verify. Every problem + gets a concrete recommendation. + +## Recurring findings to watch for + +- jq regex functions sneaking in (CRITICAL on OpenWRT jq). +- `fatal` log without a following `exit 1`. +- Hand-edited `main.js` or a `main.js` that doesn't match a fresh build. +- New validator/helper not re-exported -> invisible to `main.*`. +- Hardcoded ports/IPs/paths instead of `constants.sh` references. +- Routing code that ignores `subscription_outbound_is_unavailable` (traffic + leak when a subscription is down). +- Scope creep: unrelated file churn (e.g. lockfile churn) flagged as Minor. +- Corrupted mojibake bytes in diagnostic strings (should be byte-preserved). diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md new file mode 100644 index 00000000..b95b221f --- /dev/null +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -0,0 +1,76 @@ +# Memory — luci-frontend-developer + +Durable frontend (TypeScript / LuCI) knowledge. Read before implementing; +append findings; keep under ~200 lines. + +## The generated bundle (most important) + +- `luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js` is + **autogenerated by tsup** from `fe-app-netshift/src/**`. Banner: "This file is + autogenerated, please don't change manually." NEVER hand-edit it. Edit TS + source, then `yarn build`. CI fails if the committed `main.js` differs from a + fresh build (`git diff --exit-code` after build). +- tsup `onSuccess` regex-patches the ESM `export { ... }` block into + `return baseclass.extend({ ... })` so LuCI can load it as a baseclass module. + An unusual export shape could break that regex. + +## Barrel / export reachability (recurring gotcha) + +- Hand-written views consume the bundle as `main.*` via + `'require view.netshift.main as main'`. A new public API + (validator/helper/constant/tab) reaches the views ONLY if it is re-exported + up the barrel chain to `src/main.ts`. +- `validateHysteria2Url` (in `validateHysteriaUrl.ts`) is intentionally NOT in + `validators/index.ts` — only `validateProxyUrl` is exported; it dispatches to + the per-scheme validators internally. Mirror that pattern: export the + dispatcher, not necessarily every leaf. +- Other helpers (prettyBytes, showToast, normalizeCompiledVersion, etc.) are + imported by direct path, not via barrel. Check before assuming barrel. + +## Backend access + +- Only `/usr/bin/netshift` and `/etc/init.d/netshift` are ACL-allowed for + `fs.exec` (acl.d/luci-app-netshift.json). A new shell command must be a + subcommand of one of those, else extend the ACL + backend. Plus Clash API on + `ws://:9090` and `http://:9090/ui`. + +## Style / CI + +- `yarn ci` = `format && lint --max-warnings=0 && test --run && build`. + No-diff enforcement (format + build) lives in `frontend-ci.yml` via + `git diff --exit-code`. Run the `frontend-ci` skill before handing back. +- Prettier: 2-space, single quotes, semicolons, trailing-comma all, width 80. +- ESLint: `@typescript-eslint/no-unused-vars` is a WARNING with `^_` ignore, and + CI is `--max-warnings=0` => unused vars MUST be `_`-prefixed (`_e`, `_err`). +- strict TS, no `any`, functional components, named exports. +- LuCI globals (E, fs, uci, ui, _) are in `src/luci.d.ts` (no import). E() + handlers use the `click:` attribute (not `onclick`) even though luci.d.ts only + declares `onclick` — follow the existing `click:` convention. Extend + `luci.d.ts` when you need a new LuCI global. + +## i18n + +- Wrap user-facing strings in `_()`, and only STRING LITERALS — the gettext + extractor (`yarn locales:actualize`) only sees literal args. Some validator + messages (vless/trojan) are currently NOT wrapped — wrap new ones. + +## Version placeholder + +- `constants.ts` `NETSHIFT_LUCI_APP_VERSION='__COMPILED_VERSION_VARIABLE__'` is + substituted at OpenWRT build by `luci-app-netshift/Makefile` sed. In dev, + `normalizeCompiledVersion` turns anything containing `COMPILED` into `'dev'`. + Don't change the literal without updating the Makefile sed. + +## Tests + +- vitest `.test.js` files next to code under `tests/`, table-driven + `describe.each`, `_()` identity-mocked in `tests/setup/global-mocks.ts`, node + env (no DOM). New pure logic SHOULD ship a test. DOM/service/render code is + untested (no DOM mocks) — verify those by reasoning + build. + +## Landmines + +- `runFakeIPCheck` allGood/atLeastOneGood logic looks inverted vs other checks — + don't "fix" without understanding intent. +- Filename typo `checks/contstants.ts` is imported with the typo everywhere — + don't "correct" it and break imports. diff --git a/docs/agent-rules/memory/packaging-ci-engineer.md b/docs/agent-rules/memory/packaging-ci-engineer.md new file mode 100644 index 00000000..3f2462bf --- /dev/null +++ b/docs/agent-rules/memory/packaging-ci-engineer.md @@ -0,0 +1,75 @@ +# Memory — packaging-ci-engineer + +Durable packaging / CI / release knowledge. Read before working; append +findings; keep under ~200 lines. + +## Packages + +- `netshift` (backend) and `luci-app-netshift` (UI; also yields + `luci-i18n-netshift-ru` via `LUCI_LANGUAGES=en ru`). Both `PKGARCH=all`. +- `netshift/Makefile`: DEPENDS `+sing-box +curl +jq +kmod-nft-tproxy + +coreutils-base64 +bind-dig`; CONFLICTS `https-dns-proxy nextdns + luci-app-passwall luci-app-passwall2`; version + `PKG_VERSION = $(if $(NETSHIFT_VERSION),$(NETSHIFT_VERSION),0.$(date +%d%m%Y))`; + `prerm` removes `105 netshift` from `/etc/iproute2/rt_tables` and stops the + service; conffile `/etc/config/netshift`; stamps + `__COMPILED_VERSION_VARIABLE__` into `constants.sh` via sed (NO `|| true`, so a + missing file fails the build). +- `luci-app-netshift/Makefile`: uses `luci.mk`, `LUCI_DEPENDS=+luci-base + +netshift`; stamps the same placeholder into `main.js` (WITH `|| true`, so a + missing main.js silently won't stamp). Asymmetric on purpose — note it. + +## Docker build images + +- `Dockerfile-ipk` FROM `itdoginfo/openwrt-sdk-ipk:24.10.6`; + `Dockerfile-apk` FROM `itdoginfo/openwrt-sdk-apk:25.12.3`. +- KNOWN INCONSISTENCY (intentional, do NOT "fix" blindly): ipk Dockerfile + exports `NETSHIFT_VERSION="v${NETSHIFT_VERSION}"` (adds a `v`); apk sets it + raw (no `v`). Embedded version vs artifact filenames can differ across types. +- `sdk/Dockerfile-sdk-*` are the base SDK images (feeds update + luci-base); + apk SDK requires running `./setup.sh` first. + +## Release flow (build.yml, on tag push) + +smoke-tests gate -> `preparation` derives version (`git describe --tags +--exact-match`, fallback `0.`) -> matrix build ipk+apk -> `docker cp` +artifacts out of the container -> **ipk underscore->dash rename** +(`sed 's/_/-/g'`) -> filter to the 3 packages -> GitHub Release. + +- The underscore->dash rename is LOAD-BEARING: `install.sh` scrapes the + latest-release API and matches assets by package-name prefix + (`netshift*`, `luci-app-netshift*`, `luci-i18n-netshift-ru*`). Breaking the + rename breaks install. + +## Smoke tests (tests/) + +- Image = OpenWRT 24.10.6 rootfs; source is **bind-mounted at runtime** + (`../netshift/files -> /netshift/files:ro`), so editing `netshift/files` is + picked up without rebuilding the image. +- Needs `NET_ADMIN`/`NET_RAW`/`SYS_ADMIN` + `network_mode: host` for nft/dns; + nft tests FAIL (not skip) without caps. +- `all` runs: deps syntax config helpers jq cm sb nft diagnostics subscription. +- Add a test: `test_xyz()` (header/pass/fail/skip), add to `main()` `all)` list, + add `case` alias, update usage line + docker-compose comment. Keep the two + compose invocations (build.yml smoke vs openwrt-smoke-tests.yml) in sync. + +## CI gates by path + +- `frontend-ci.yml`: PRs touching `fe-app-netshift/**` -> yarn install / + format(diff) / lint(--max-warnings=0) / test / build(diff). +- `shellcheck.yml`: `install.sh` + `usr/bin/netshift` + `usr/lib/**.sh`, + severity error (Differential ShellCheck). +- `openwrt-smoke-tests.yml`: `netshift/**`, `luci-app-netshift/**`, `tests/**`, + `install.sh`, Dockerfiles -> entrypoint `all`. +- `.gitlab-ci.yml` declares a `test` stage but runs no tests — only builds and + deploys Docker on master. Quality is enforced by the AI review workflow + the + GitHub Actions above, not GitLab. + +## install.sh + +- POSIX; apk/opkg abstraction; podkop->netshift migration STOPS the old service + first (that restores dnsmasq keys + removes PodkopTable + `105 podkop`), + backs up config to `/etc/config/podkop.bak.pre-netshift`. OpenWRT 23.05 + unsupported; needs >=15 MB on `/overlay`; NO uninstall path (removal lives in + package `prerm`). GitHub API rate-limit is a known fragility (wget path has no + guard). diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md new file mode 100644 index 00000000..18cbbac9 --- /dev/null +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -0,0 +1,65 @@ +# Memory — shell-backend-developer + +Durable backend (ash + jq) knowledge. Read before implementing; append +findings; keep under ~200 lines. + +## Hard constraints (proven) + +- **OpenWRT jq has NO Oniguruma** — `test()`, `match()`, `sub()`, `gsub()` and + any regex are unavailable. The updater (`updater.sh`) documents workarounds. + Build string logic with `split`/`startswith`/`endswith`/`contains`/`ascii` + instead. +- **`fatal` is only a log label** — `log "..." "fatal"` does NOT exit. You must + follow it with `exit 1` yourself. Missing the `exit 1` continues with a + half-built config. +- **busybox sed lacks `\x` escapes** — use printf-octal workarounds (see + `helpers.sh` `convert_crlf_to_lf` and BOM stripping). Don't assume GNU sed. +- **Intentional mojibake**: some diagnostic strings (list_update / + subscription_update / global_check) store emoji/box-drawing as corrupted + CP1251-ish bytes (e.g. `рџ”„`, `вњ…`, `в”Ѓ`). Preserve the exact existing bytes + when editing those lines or rendered output changes. + +## Conventions (follow exactly) + +- File header: `# shellcheck shell=ash`; constants files add + `# shellcheck disable=SC2034`. Declare every variable `local`. +- Function prefixes: `sing_box_cm_*` = one jq mutation each (dumb primitive); + `sing_box_cf_*` = facade (parse + several cm_* calls); `url_*` = pure URL + parsing; `is_*` = predicate returning 0/1; `nft_*` = nft wrapper; `updates_*` + = updater; `get_*_tag` = deterministic tag builder; `configure_*`/`import_*`/ + `_*_handler` = config_foreach callbacks; leading `_` = private helper. +- Config threading: `$config` is a shell STRING; cm/cf take it as `$1`, echo + mutated JSON; caller does `config=$(sing_box_cm_... "$config" ...)`. +- jq optional keys: `+ (if $x != "" then {k:$x} else {} end)`. Custom helpers + in `helpers.jq`, imported `import "helpers" as h {"search":"/usr/lib/netshift"}`. +- Validation is mandatory: write to `*.tmp.$$`, run `sing-box -c check` + (fatal on fail), `jq -e` for shape, md5sum-compare, then `mv`. Atomic only. +- New constants -> `constants.sh` (grouped Common/nft/sing-box/Lists). Never + hardcode ports/IPs/marks/paths. +- The service-tag pattern: cm_* functions stamp a transient `__service_tag` + (`SERVICE_TAG`) on rules; `sing_box_cm_save_config_to_file` strips every + `__service_tag` via `walk(...)` before writing. Don't leave tags in output. + +## Subscription / unavailable-outbound flow (don't leak traffic) + +- Many code paths branch on `subscription_outbound_is_unavailable` to emit + **reject** route rules instead of routes when a subscription is down. Any new + routing code MUST respect this or it leaks traffic when a sub is unavailable. + +## Testing + +- Smoke suite is `tests/entrypoint.sh` (run via `smoke-tests` skill). Categories: + deps syntax config helpers jq cm sb nft diagnostics subscription. +- To add a test: write `test_xyz()` using the `header`/`pass`/`fail`/`skip` + helpers; add it to `main()`'s `all)` list; add a `case` alias; update the + usage line and the docker-compose comment. Config-gen and subscription + parsing changes SHOULD get a smoke test. +- Pre-commit-equivalent: always run the `shellcheck` skill (severity error) on + touched shell files before handing back. + +## Known landmines + +- nft proxy chain hardcodes `127.0.0.1:1602` (duplicates the constants). +- VPN `domain_resolver` uses wrong variable `$dns_server`. +- `check_nft` references stale set names (`netshift_domains`) / UCI options that + don't exist elsewhere — likely copied diagnostic cruft. diff --git a/docs/agent-rules/packaging.md b/docs/agent-rules/packaging.md new file mode 100644 index 00000000..9814bda5 --- /dev/null +++ b/docs/agent-rules/packaging.md @@ -0,0 +1,188 @@ +# Agent Rules: Packaging, CI & Release + +Authoritative rules for building, testing, and releasing NetShift. Read this +before touching anything below. + +**Scope:** `netshift/Makefile`, `luci-app-netshift/Makefile`, +`Dockerfile-ipk`, `Dockerfile-apk`, `sdk/`, `tests/`, `.github/workflows/`, +`install.sh`. + +--- + +## 1. The packages + +Two source packages produce three published artifacts: + +- **`netshift`** — the backend (init script, UCI config, `/usr/bin/netshift`, + shell + jq libs under `/usr/lib/netshift`). +- **`luci-app-netshift`** — the web UI. Its Makefile sets + `LUCI_LANGUAGES := en ru`, so the build also emits + **`luci-i18n-netshift-ru`** (the Russian translation) as a third package. + +Both packages are `PKGARCH := all` / `LUCI_PKGARCH := all` (architecture- +independent). + +--- + +## 2. `netshift/Makefile` (backend) + +- `DEPENDS := +sing-box +curl +jq +kmod-nft-tproxy +coreutils-base64 + +bind-dig` +- `CONFLICTS := https-dns-proxy nextdns luci-app-passwall luci-app-passwall2` +- Version: + `PKG_VERSION := $(if $(NETSHIFT_VERSION),$(NETSHIFT_VERSION),0.$(shell date +%d%m%Y))` + — i.e. use `NETSHIFT_VERSION` when set, otherwise a date-stamped fallback. +- `Package/netshift/prerm` removes the `105 netshift` line from + `/etc/iproute2/rt_tables` (only if present) and runs + `/etc/init.d/netshift stop`. **Service/system-state teardown lives in the + package `prerm`, not in `install.sh`.** +- `Package/netshift/conffiles` declares `/etc/config/netshift` (preserved + across upgrades). +- Version stamp: `Package/netshift/install` runs + `sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' + $(1)/usr/lib/netshift/constants.sh` — **no `|| true`** (this stamp must + succeed). + +`luci-app-netshift/Makefile` stamps the **same** placeholder into the bundled +UI: `sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(PKG_VERSION)/g' +.../view/netshift/main.js || true` — note the **`|| true`** here (UI stamp is +best-effort). It uses the same `PKG_VERSION` expression and +`LUCI_DEPENDS := +luci-base +netshift`. + +> If you ever rename `__COMPILED_VERSION_VARIABLE__`, update **both** Makefile +> `sed`s and `fe-app-netshift/src/constants.ts` together. See +> `frontend-luci.md` §6. + +--- + +## 3. Docker build images + +- `Dockerfile-ipk` — `FROM itdoginfo/openwrt-sdk-ipk:24.10.6`. +- `Dockerfile-apk` — `FROM itdoginfo/openwrt-sdk-apk:25.12.3`. +- Both copy `./netshift` → `feeds/utilities/netshift` and + `./luci-app-netshift` → `feeds/luci/luci-app-netshift`, then run + `make defconfig` + `make package//compile`. +- `sdk/Dockerfile-sdk-ipk` (`FROM openwrt/sdk:x86_64-v24.10.6`) and + `sdk/Dockerfile-sdk-apk` (`FROM openwrt/sdk:x86_64-v25.12.3`) are the **base + SDK images** that the `itdoginfo/openwrt-sdk-*` images derive from (feeds + updated, `luci-base` installed, feed dirs created; the apk one also runs + `./setup.sh`). + +### KNOWN INCONSISTENCY — respect it, do not "fix" blindly + +The two release Dockerfiles pass the version differently: + +- `Dockerfile-ipk`: `RUN export NETSHIFT_VERSION="v${NETSHIFT_VERSION}" && ...` + — it **prepends `v`**. +- `Dockerfile-apk`: `ENV NETSHIFT_VERSION=${NETSHIFT_VERSION}` — **raw, no + `v`**. + +This asymmetry is intentional/load-bearing for the current artifact names. Do +not normalize one to match the other without verifying the whole release flow +(§4) and `install.sh` matching (§6). + +--- + +## 4. Release flow (`.github/workflows/build.yml`) + +Triggered on **tag push** (`tags: ['*']`). Jobs: + +1. **`smoke-tests`** — builds and runs the OpenWRT rootfs smoke suite + (`docker compose -f tests/docker-compose.yml run --rm netshift-test all`). + This is a **gate**: `build` `needs` it. +2. **`preparation`** — derives the version: + `git describe --tags --exact-match || "0.$(date +%d%m%Y)"`. +3. **`build`** (matrix `ipk` + `apk`) — builds via + `Dockerfile-`, passing `NETSHIFT_VERSION` from `preparation`; then + `docker create` + `docker cp` the built packages out of + `/builder/bin/packages/x86_64/{utilities,luci}/`. +4. **ipk-only rename** — for `ipk`, every `*.ipk` filename is rewritten with + `sed 's/_/-/g'` (underscore → dash). +5. **Filter** — copies exactly the **three** packages into `filtered-bin/`: + `luci-i18n-netshift-ru-*`, `netshift-*`, `luci-app-netshift-*` (the i18n + one is renamed to carry `${VERSION}`). +6. **`release`** — downloads both matrices' artifacts and publishes a + **GitHub Release** (`softprops/action-gh-release`) named/tagged + `github.ref_name`. + +The underscore→dash rename (step 4) is **load-bearing**: `install.sh` matches +release assets by **package-name prefix** (see §6), and the dashed names are +what it expects. + +--- + +## 5. Smoke tests (`tests/`) + +- Runs in an **OpenWRT 24.10.6 rootfs** container (`tests/Dockerfile`: pulls + the official `openwrt-24.10.6-x86-64-rootfs.tar.gz`, `opkg install`s + `sing-box curl jq coreutils-base64 bind-dig nftables`). +- Source is **bind-mounted read-only**: `../netshift/files` → + `/netshift/files:ro` (see `tests/docker-compose.yml`). The container has no + copy of the scripts — it tests the live source tree. +- Requires kernel caps **`NET_ADMIN` + `NET_RAW` + `SYS_ADMIN`** and + `network_mode: host` (for real nft / DNS operations). +- `entrypoint.sh` `main()` dispatches by category. The `all` target runs, in + order: `test_deps test_syntax test_config test_helpers test_jq_helpers + test_config_manager test_sing_box_config test_nft test_diagnostics + test_subscription`. The usage line lists: + `all deps syntax config helpers jq cm sb nft diagnostics subscription`. + +### How to ADD a smoke test + +1. Write a `test_xyz()` function using the existing helpers: + `header`, `pass`, `fail`, `skip` (they drive the `PASS`/`FAIL`/`SKIP` + counters and `summary`). For sub-shells, emit `name:OK` / `name:FAIL` / + `name:SKIP` lines and let the `case` parser pick them up (see + `test_helpers` / `test_subscription`). +2. Add it to the `all)` list in `main()`. +3. Add a short **case alias** (e.g. `xyz) test_xyz ;;`). +4. Update the **usage line** in `main()` (the `Available: ...` echo). +5. Update the **`docker-compose.yml` comment** that documents test names. + +--- + +## 6. CI gates by path + +| Workflow | Triggers (paths) | What it does | +|---|---|---| +| `frontend-ci.yml` | `fe-app-netshift/**` (PR) | `yarn install --frozen-lockfile`, `yarn format` (fail on diff), `yarn lint --max-warnings=0`, `yarn test --run`, `yarn build` (fail on diff). See `frontend-luci.md`. | +| `shellcheck.yml` | `install.sh`, `netshift/files/usr/bin/**`, `netshift/files/usr/lib/**` (push/PR to `main`/`rc/**`) | Differential ShellCheck, `severity: error`, include-paths `netshift/files/usr/bin/netshift`, `netshift/files/usr/lib/**.sh`, `install.sh`. | +| `openwrt-smoke-tests.yml` | `netshift/**`, `luci-app-netshift/**`, `tests/**`, `install.sh`, `Dockerfile-ipk`, `Dockerfile-apk`, `.dockerignore` (push/PR to `main`/`rc/**`) | Builds the smoke image and runs `netshift-test all`. | + +> **Keep the two smoke invocations in sync.** `build.yml`'s `smoke-tests` job +> runs the suite **only on tag push**; PR/branch coverage comes from the +> separate `openwrt-smoke-tests.yml`. Both call +> `docker compose -f tests/docker-compose.yml ... netshift-test all` — if you +> change one compose command (image name, target, flags), change the other. + +--- + +## 7. `install.sh` + +- **POSIX `sh`** (BusyBox `ash` compatible); shellcheck-gated at `error`. +- **Package-manager abstraction:** detects `apk` (`PKG_IS_APK=1`) vs `opkg` + and wraps install/remove/update/list (`pkg_install`, `pkg_remove`, + `pkg_is_installed`, etc.). apk install uses `--allow-untrusted`; opkg remove + uses `--force-depends`. +- **podkop → netshift migration** (`migrate_from_podkop`, triggered by + `podkop_is_installed` since podkop never reached 0.8.0): **stop the old + service first** (`/etc/init.d/podkop stop` then `disable`) so dnsmasq/nft + teardown happens, **back up config** to + `/etc/config/podkop.bak.pre-netshift`, copy config to + `/etc/config/netshift`, remove the original `/etc/config/podkop`, clean the + old `105 podkop` rt_tables line and podkop cron entries, and remove the old + `luci-i18n-podkop*` / `luci-app-podkop` / `podkop` packages. +- **OpenWRT 23.05 is unsupported** (since NetShift 0.8.0): `check_system` + exits if `DISTRIB_RELEASE` major == `23`. +- **Space requirement:** needs **≥ 15 MB** free in `/overlay` + (`REQUIRED_SPACE=15360` KB). +- **Asset matching:** scrapes the latest-release API + (`https://api.github.com/repos/yandexru45/netshift/releases/latest`), + greps `.apk`/`.ipk` URLs, then installs by **package-name prefix** + (loops `for pkg in netshift luci-app-netshift`, plus + `luci-i18n-netshift-ru*`). This is why the ipk underscore→dash rename in + `build.yml` (§4) is load-bearing. +- **NO uninstall path.** `install.sh` only installs/migrates; removal lives in + the package `prerm` (see §2). Do not add an uninstaller here. +- **Known fragility:** GitHub API **rate limiting** — the script detects + `API rate limit` and exits with a "repeat in five minutes" message. diff --git a/docs/agent-rules/project-core.md b/docs/agent-rules/project-core.md new file mode 100644 index 00000000..39b8495e --- /dev/null +++ b/docs/agent-rules/project-core.md @@ -0,0 +1,112 @@ +# NetShift — Project Core Rules (AUTHORITATIVE) + +> Single source of truth for AI agents working anywhere in this repo. Read this before touching code. Every rule below is grounded in the actual source — do not invent values. + +## 1. Project identity + +NetShift is an OpenWRT traffic router built on top of [sing-box](https://github.com/SagerNet/sing-box): it selectively routes chosen domains/subnets through a tunnel and sends everything else directly. It is a fork of [itdoginfo/podkop](https://github.com/itdoginfo/podkop), rebranded to NetShift at version `0.8.0`. The project is **beta** (expect breaking changes). License: **GPL-2.0-or-later** (`LICENSE`), with a **separate trademark policy** — the NetShift name and logos are protected; see `TRADEMARK.md`. Code is GPL-licensed; the brand is not. + +Hard requirements (target device): +- OpenWRT **24.10+** +- `sing-box >= 1.12.0` (`SB_REQUIRED_VERSION` in `constants.sh`) +- `jq >= 1.7.1` (`JQ_REQUIRED_VERSION`) +- `coreutils-base64 >= 9.7` (`COREUTILS_BASE64_REQUIRED_VERSION`) +- `>= 25 MB` free space (16 MB flash devices unsupported) + +## 2. The three packages and strict dependency direction + +Layers point in ONE direction. **No layer skips another.** + +``` +luci-app-netshift (TS/LuCI UI, hand-written views + generated main.js) + │ consumes the generated main.js produced from + ▼ +fe-app-netshift (TypeScript source, built with tsup) + │ UI talks ONLY to the backend, never to sing-box/nft/dnsmasq directly + ▼ +netshift backend via LuCI fs.exec of /usr/bin/netshift and /etc/init.d/netshift (ACL-gated) + │ + ▼ +sing-box / nftables / dnsmasq +``` + +- `luci-app-netshift` — LuCI web UI. Its `htdocs/.../view/netshift/main.js` is **generated** from `fe-app-netshift`. Hand-written views live alongside it. +- `fe-app-netshift` — the TypeScript source of `main.js` (fetchers, methods, services, tabs). Edit UI logic **here**, not in the generated bundle. +- `netshift` — the backend package (POSIX ash + jq): CLI dispatcher `/usr/bin/netshift`, procd init `/etc/init.d/netshift`, libraries in `/usr/lib/netshift/`, UCI config `/etc/config/netshift`. + +The UI never reimplements backend logic; it invokes backend commands. The backend never depends on the UI. + +## 3. Runtime contract (sacred — do not change casually) + +These values are wired across `constants.sh`, `nft.sh`, the CLI, and the generated sing-box config. Changing one without the rest breaks the whole chain. + +| Concept | Value | Source constant | +|---|---|---| +| tproxy inbound | `127.0.0.1:1602` | `SB_TPROXY_INBOUND_ADDRESS` / `SB_TPROXY_INBOUND_PORT` | +| DNS inbound | `127.0.0.42:53` | `SB_DNS_INBOUND_ADDRESS` / `SB_DNS_INBOUND_PORT` | +| Service mixed inbound | `127.0.0.1:4534` | `SB_SERVICE_MIXED_INBOUND_*` | +| Clash API controller | `:9090` | `SB_CLASH_API_CONTROLLER_PORT` | +| FakeIP range | `198.18.0.0/15` | `SB_FAKEIP_INET4_RANGE` | +| nft FakeIP mark | `0x00100000` | `NFT_FAKEIP_MARK` | +| nft outbound mark | `0x00200000` | `NFT_OUTBOUND_MARK` | +| nft table | `NetShiftTable` (family `inet`) | `NFT_TABLE_NAME` | +| routing table | `105 netshift` | `RT_TABLE_NAME` + `/etc/iproute2/rt_tables` | +| state dir | `/etc/netshift` | `NETSHIFT_STATE_DIR` | +| sing-box config | `/etc/sing-box/config.json` (UCI `settings.config_path`) | UCI | + +These ports, marks, addresses, the nft table name, and the routing table id are **sacred**. They are referenced in `nft.sh` rules, `route_table_rule_mark`, dnsmasq integration (`127.0.0.42`), diagnostics (`check_sing_box`, `check_nft_rules`, `check_dns_available`), and the generated config. Treat any change to them as a system-level change (§4). + +## 4. System-level change rule + +A change is **system-level** if it touches any of: +- nft rules / sets / chains, routing rules or tables, fwmarks +- sing-box config schema or generation +- dnsmasq integration (server `127.0.0.42`, `noresolv`, `cachesize`, backup/restore) +- UCI schema (`/etc/config/netshift`) +- ports / marks / tags in `constants.sh` +- packaging (`Makefile`, install, conffiles, dependencies) +- the payment-free subscription flow (download → validate → cache → generate outbounds) + +For system-level changes you MUST verify the **whole chain**, not a single file: + +``` +UCI (config_get) → config generation (sing_box_*) → sing-box -c check → nft rules → running service +``` + +Validate by running the smoke tests and, where relevant, confirming the generated config still passes `sing-box check` and the nft table/routing still install (see `start_main` in `/usr/bin/netshift`). + +## 5. Repo-wide conventions + +- **LF line endings everywhere.** `.gitattributes` enforces `* text=auto eol=lf`. Never introduce CRLF. +- **No magic strings.** All ports, IPs, marks, tags, paths, versions, and service lists live in `netshift/files/usr/lib/constants.sh`, grouped as `## Common`, `## nft`, `## sing-box`, `## Lists`. New constants go there. +- Community service list is `COMMUNITY_SERVICES` in `constants.sh`; the UI and `validate_service` both depend on it. + +## 6. Mandatory quality gates (CI a contributor must pass) + +These gate every PR. Use the matching skills. + +1. **ShellCheck** (`.github/workflows/shellcheck.yml`) — severity `error`, over: + - `install.sh` + - `netshift/files/usr/bin/netshift` + - `netshift/files/usr/lib/**.sh` + - Skill: `shellcheck`. +2. **OpenWRT smoke tests** (`.github/workflows/openwrt-smoke-tests.yml`) — runs `tests/entrypoint.sh` in an OpenWRT rootfs via `tests/docker-compose.yml` (`run --rm netshift-test all`). + - Skill: `smoke-tests`. +3. **Frontend `yarn ci`** (in `fe-app-netshift`) — defined as: + `yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build` + i.e. prettier (no diff), ESLint with `--max-warnings=0`, vitest, and a build that must produce no diff in the committed `main.js`. + - Skill: `frontend-ci`. + +## 7. Contribution gating + +- `CODEOWNERS = @yandexru45`. +- PRs are accepted **only after coordination with the authors via Telegram** (per README; see `t.me/netshift_chat`). +- **Agents NEVER auto-commit.** A human reviews and commits manually. Do not run `git commit`, `git push`, amend, or open PRs unless the human explicitly asks. + +## 8. Anti-patterns (do NOT do these) + +- Hardcoding ports / IPs / marks / paths instead of using `constants.sh`. +- Duplicating routing/marking logic instead of reusing `nft.sh` / `route_table_rule_mark` / `create_nft_rules`. +- Editing the generated `main.js` by hand — edit `fe-app-netshift` TS source and rebuild. +- Reimplementing backend logic in the UI — the UI must call `/usr/bin/netshift` / `/etc/init.d/netshift`. +- Changing a sacred runtime value in one place while leaving the rest of the chain stale. diff --git a/docs/tasks/TEMPLATE-review.md b/docs/tasks/TEMPLATE-review.md new file mode 100644 index 00000000..5c1d44fd --- /dev/null +++ b/docs/tasks/TEMPLATE-review.md @@ -0,0 +1,55 @@ +# Code Review — (task-NNN) + +> Authored by `code-reviewer`. File name: `<task-name>-review-001.md` +> (use `-002`, `-003`, ... or append a "Re-review" section for later rounds). + +**Review ID:** review-001 +**Date:** <YYYY-MM-DD> +**Scope:** <uncommitted working-tree changes | branch X vs base> +**Reviewer:** code-reviewer agent + +**Files reviewed:** +- `path/...` + +--- + +## Summary + +<Brief, neutral assessment of what the change does and its overall quality. No +flattery.> + +## Critical Issues + +> Must fix before merge. Architecture violations, broken runtime contract, jq +> regex on OpenWRT, missing `exit 1` after `fatal`, hand-edited `main.js`, traffic +> leaks, etc. + +### [C1] <Issue title> +- **File:** `path/to/file` (line X) +- **Problem:** <what is wrong and why it matters> +- **Recommendation:** <concrete fix> + +## Significant Issues + +### [S1] <Issue title> +- **File:** `path/to/file` (line X) +- **Problem:** ... +- **Recommendation:** ... + +## Minor Observations + +- **[M1]** `path/to/file`: <short note> + +## Test Coverage + +<Were the required tests/gates added and run? Backend config-gen/subscription → +smoke test? New pure frontend logic → vitest? Was the relevant gate green? If +frontend tests were explicitly not required, say so and state what was verified +by reasoning + build.> + +## Verdict + +**APPROVED** | **APPROVED WITH CONDITIONS** | **REQUIRES CHANGES** + +<One sentence overall. If conditional, list "Conditions before merge" by issue +ID. If changes required, list "Required before merge" by issue ID.> diff --git a/docs/tasks/TEMPLATE-task.md b/docs/tasks/TEMPLATE-task.md new file mode 100644 index 00000000..9c109c4f --- /dev/null +++ b/docs/tasks/TEMPLATE-task.md @@ -0,0 +1,62 @@ +# Task: <imperative title> + +> Authored by `architect-orchestrator`. One self-contained spec per subtask. +> File name: `task-NNN-<kebab-slug>.md`. + +## Context + +<Narrative: what the user wants and why. Link any relevant prior tasks.> + +### Root cause / research basis (authoritative) + +<What investigation established. Cite `file:line`. State facts, not guesses.> + +### Operator decisions (already made — do NOT re-ask) + +- <Design choice the operator already approved, e.g. "Variant 2".> + +## Goal + +<One paragraph describing the desired end state.> + +## Scope + +- Layer(s): <backend ash/jq | TS/LuCI frontend | packaging/CI>. +- Files to modify (exact paths): + - `path/one` + - `path/two` +- Do NOT touch: <explicit out-of-scope files/areas>. + +## Requirements + +### 1. <numbered requirement> + +<Be specific. Include exact target `file:line` and fenced code blocks of the +intended change where helpful.> + +### 2. <numbered requirement> + +## Architecture Notes + +- Applicable rules: <e.g. `docs/agent-rules/backend-shell.md` (no jq regex; + `fatal` needs `exit 1`)>. +- Runtime-contract impact: <none | which ports/marks/paths and the whole-chain + verification required>. +- Single-source-of-truth constraints: <constants.sh; barrel→main.*; etc.>. + +## Tests Required + +- Backend: <which `test_*` to add/extend in `tests/entrypoint.sh`, or "covered + by existing X">. Run the `shellcheck` and `smoke-tests` skills. +- Frontend: <vitest `.test.js` to add, or "verify by reasoning + build">. Run + the `frontend-ci` skill; ensure `main.js` rebuild leaves no git diff. +- Packaging: <smoke tests; verify ipk + apk paths>. + +## Definition of Done + +- [ ] All requirements implemented in scope; nothing out-of-scope changed. +- [ ] Relevant gate(s) pass (shellcheck / smoke-tests / yarn ci). +- [ ] (Frontend) `main.js` regenerated; `git diff` clean after build. +- [ ] Runtime contract intact (or whole chain verified if changed). +- [ ] New user-facing strings wrapped in `_()` (frontend). +- [ ] `code-reviewer` verdict: APPROVED or APPROVED WITH CONDITIONS. diff --git a/opencode.json b/opencode.json new file mode 100644 index 00000000..2ecf11d4 --- /dev/null +++ b/opencode.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencode.ai/config.json", + "instructions": [ + "AGENTS.md", + "docs/agent-rules/project-core.md", + "docs/agent-rules/backend-shell.md", + "docs/agent-rules/frontend-luci.md", + "docs/agent-rules/packaging.md" + ] +} From df7b67781e14bca884e6cc7826f090b11ef1c9ed Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Thu, 4 Jun 2026 18:53:10 +0300 Subject: [PATCH 41/75] =?UTF-8?q?=D1=84=D0=BE=D0=BB=D0=BB=D0=B1=D0=B5?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BE=D0=BA=20xray=20json=20+=20=D1=84=D0=BE=D0=BB?= =?UTF-8?q?=D0=BB=D0=B1=D0=B5=D0=BA=D0=B8=20=D1=8E=D0=B7=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../memory/architect-orchestrator.md | 22 ++ netshift/files/usr/bin/netshift | 87 ++++- netshift/files/usr/lib/constants.sh | 8 + netshift/files/usr/lib/helpers.sh | 291 ++++++++++++++++- tests/entrypoint.sh | 297 ++++++++++++++++++ 6 files changed, 680 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 678c6f4a..5a6b8f9a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ fe-app-netshift/.env .DS_Store *.txt tests/test-results/ +docs/tasks \ No newline at end of file diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 60dfbf28..a51ffdae 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -63,6 +63,28 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> - Frontend `runFakeIPCheck` has inverted-looking allGood/atLeastOneGood logic. - Diagnostic strings contain intentional CP1251 mojibake (emoji/box-drawing) — preserve byte sequences when editing. +- `validate_subscription_file` (helpers.sh) only checks `.type` is NOT in + {selector,urltest,direct,dns,block}. A body whose outbounds lack `.type` + entirely (e.g. a single Xray-config OBJECT using `.protocol`) passes as + "valid" → bypasses the fallback normalizer and later fails `sing-box check`. + An Xray ARRAY is `type=="array"` and correctly falls through to normalize. + Watch this when adding any pre-normalize validate gate. + +## Subscription pipeline facts (verified 2026-06) + +- Fallback chain in `download_subscription_into_cache` (usr/bin/netshift): + validate raw body FIRST, only then `normalize_subscription_to_singbox` + (base64 / plaintext URI list / Xray-JSON). UA fallback wraps the whole loop: + it probes `SUBSCRIPTION_USER_AGENT_CANDIDATES` (constants.sh) when no UA is + configured, caches the winner in `<section>.user_agent` (atomic .tmp.$$+mv). +- New per-section UCI option `subscription_user_agent` is read but NOT yet in + the UCI schema / LuCI / ACL. Degrades gracefully (empty ⇒ auto). Treat any + promotion to a real UI knob as a system-level change (schema + LuCI + i18n). +- `xray_json_to_uri_lines` converts Xray client configs (object|array) to share + URIs; emits ONLY keys the facade reads (type/path/host/mode/serviceName/ + security/sni/alpn/fp/pbk/sid/flow); drops vmess (counted by + `xray_json_count_unsupported`) and dialerProxy-chained outbounds; dedups on + the connection part. No-regex jq + busybox-safe sed pre-gate. ## Workflow facts diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index ad18b70c..1a9f5685 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -167,6 +167,15 @@ get_subscription_rejected_cache_path() { echo "$SUBSCRIPTION_CACHE_FOLDER/${section}.rejected" } +# Path of the cached "winning" User-Agent for a subscription source: the first +# candidate that previously produced valid outbounds. Tried first on the next +# refresh so we don't re-probe the whole whitelist every time. +get_subscription_user_agent_cache_path() { + local section="$1" + + echo "$SUBSCRIPTION_CACHE_FOLDER/${section}.user_agent" +} + ensure_subscription_cache_dir() { local state_dir_created=0 cache_dir_created=0 local mkdir_errfile mkdir_rc @@ -345,6 +354,8 @@ download_subscription_into_cache() { local subscription_url_cache_path="$4" local service_proxy_address="$5" local tmpfile persist_tmpfile url_tmpfile rejected_cache_path tmp_hash rejected_hash validation_reason file_size fallback_tmp + local configured_user_agent user_agent_cache_path cached_user_agent candidates_file + local effective_user_agent download_ok winning_user_agent ua_tmpfile ensure_subscription_cache_dir || { log "Failed to prepare persistent subscription cache directory '$SUBSCRIPTION_CACHE_FOLDER' for section '$section'" "error" @@ -359,27 +370,73 @@ download_subscription_into_cache() { return 11 } - if ! download_subscription "$subscription_url" "$tmpfile" "$service_proxy_address" 3 2 10; then - log "Subscription body download failed for section '$section' after retries" "error" - rm -f "$tmpfile" - return 12 + # User-Agent fallback. Panels often key the returned body format off the + # client User-Agent, so when none is configured we probe a whitelist of + # well-known clients and keep the first that yields valid outbounds. The + # previously successful UA (cached) is tried first to avoid re-probing. + user_agent_cache_path="$(get_subscription_user_agent_cache_path "$section")" + configured_user_agent="$(uci -q get "netshift.${section}.subscription_user_agent" 2>/dev/null)" + cached_user_agent="$(cat "$user_agent_cache_path" 2>/dev/null)" + candidates_file="${tmpfile}.ua" + if ! build_subscription_user_agent_candidates "$configured_user_agent" "$cached_user_agent" > "$candidates_file"; then + log "Failed to build subscription User-Agent candidate list for section '$section'" "error" + rm -f "$tmpfile" "$candidates_file" + return 11 fi - file_size="$(wc -c < "$tmpfile" 2>/dev/null | tr -d ' ')" - log "Downloaded subscription body for section '$section': bytes=${file_size:-unknown}" "debug" + download_ok=0 + winning_user_agent="" + fallback_tmp="${tmpfile}.fb" + # Each candidate gets the full download (with its own retries) + validate + + # fallback-normalize pipeline. First success wins. + while IFS= read -r effective_user_agent || [ -n "$effective_user_agent" ]; do + [ -n "$effective_user_agent" ] || continue + + log "Trying subscription User-Agent for section '$section': $effective_user_agent" "info" + + if ! download_subscription "$subscription_url" "$tmpfile" "$service_proxy_address" 3 2 10 "$effective_user_agent"; then + log "Subscription download failed for section '$section' with User-Agent '$effective_user_agent'; trying next candidate" "warn" + continue + fi + + file_size="$(wc -c < "$tmpfile" 2>/dev/null | tr -d ' ')" + log "Downloaded subscription body for section '$section': bytes=${file_size:-unknown}, User-Agent='$effective_user_agent'" "debug" - if ! validate_subscription_file "$tmpfile"; then - # Fallback: provider returned base64 / plaintext key list instead of sing-box JSON - fallback_tmp="${tmpfile}.fb" + if validate_subscription_file "$tmpfile"; then + download_ok=1 + winning_user_agent="$effective_user_agent" + break + fi + + # Fallback: provider returned base64 / plaintext key list or Xray JSON + # instead of a sing-box config. if normalize_subscription_to_singbox "$tmpfile" "$fallback_tmp" "$section" && validate_subscription_file "$fallback_tmp"; then mv -f "$fallback_tmp" "$tmpfile" - log "Subscription for section '$section' parsed via fallback (base64/plaintext key list)" "info" + log "Subscription for section '$section' parsed via fallback with User-Agent '$effective_user_agent'" "info" + download_ok=1 + winning_user_agent="$effective_user_agent" + break + fi + rm -f "$fallback_tmp" + + validation_reason="$(describe_subscription_validation_failure "$tmpfile")" + log "Downloaded subscription for section '$section' is invalid with User-Agent '$effective_user_agent': ${validation_reason:-unknown validation error}; trying next candidate" "warn" + done < "$candidates_file" + rm -f "$candidates_file" + + if [ "$download_ok" -ne 1 ]; then + log "No subscription User-Agent candidate produced valid outbounds for section '$section'" "error" + rm -f "$tmpfile" "$fallback_tmp" + return 13 + fi + + # Persist the winning User-Agent so the next refresh tries it first. + if [ -n "$winning_user_agent" ]; then + ua_tmpfile="${user_agent_cache_path}.tmp.$$" + if printf '%s' "$winning_user_agent" > "$ua_tmpfile" && mv "$ua_tmpfile" "$user_agent_cache_path"; then + chmod 600 "$user_agent_cache_path" 2>/dev/null else - rm -f "$fallback_tmp" - validation_reason="$(describe_subscription_validation_failure "$tmpfile")" - log "Downloaded subscription for section '$section' is invalid: ${validation_reason:-unknown validation error}" "error" - rm -f "$tmpfile" - return 13 + rm -f "$ua_tmpfile" fi fi diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 5f699cbe..243f232d 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -14,6 +14,14 @@ TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets" TMP_SUBSCRIPTION_FOLDER="$TMP_SING_BOX_FOLDER/subscriptions" SUBSCRIPTION_CACHE_FOLDER="$NETSHIFT_STATE_DIR/subscriptions" TMP_SUBSCRIPTION_DOWNLOAD_FOLDER="$TMP_SING_BOX_FOLDER/subscription-downloads" +# Subscription User-Agent fallback. Many panels return a DIFFERENT body format +# depending on the client User-Agent (sing-box JSON vs base64 URI list vs Clash +# vs Xray JSON, or an HTML/403 stub for unknown clients). When no User-Agent is +# configured for a source, the backend tries these candidates in order and +# keeps the first one that yields valid sing-box outbounds. The default +# "singbox/<version>" candidate is prepended at runtime (it depends on the +# installed sing-box). Order matters: most-likely-to-work first. +SUBSCRIPTION_USER_AGENT_CANDIDATES="v2rayN Happ Hiddify Clash.Meta ClashMetaForAndroid" CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker JQ_REQUIRED_VERSION="1.7.1" COREUTILS_BASE64_REQUIRED_VERSION="9.7" diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index 829fe4f9..9d437eb9 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -660,13 +660,71 @@ generate_hwid() { "$(echo "$raw_hash" | cut -c13-16)" } -# Downloads a subscription JSON from the given URL with custom headers +# Resolves the effective subscription User-Agent: the explicit value when one +# is given, otherwise the default "singbox/<version>" string. Centralizes the +# default so download_subscription and the candidate builder agree. +get_subscription_user_agent() { + local custom_user_agent="${1:-}" + + if [ -n "$custom_user_agent" ]; then + printf '%s' "$custom_user_agent" + return 0 + fi + + printf 'singbox/%s' "$(get_sing_box_version)" +} + +# Emits the ordered, de-duplicated list of User-Agent candidates (one per line) +# to try for a subscription source when no User-Agent is explicitly configured. +# Different panels key the returned body format off the User-Agent, so we probe +# a whitelist of well-known clients and let the caller keep the first that +# yields valid outbounds. +# +# Arguments: +# $1 - configured User-Agent (empty for auto mode) +# $2 - preferred User-Agent (e.g. the previously cached winner; tried early) +# Behavior: +# - configured non-empty: emit ONLY that value (respect the user's choice). +# - auto: emit "singbox/<ver>", then the preferred one, then the whitelist +# from constants (SUBSCRIPTION_USER_AGENT_CANDIDATES), skipping duplicates. +build_subscription_user_agent_candidates() { + local configured_user_agent="${1:-}" + local preferred_user_agent="${2:-}" + local default_user_agent candidate seen + + if [ -n "$configured_user_agent" ]; then + printf '%s\n' "$configured_user_agent" + return 0 + fi + + default_user_agent="$(get_subscription_user_agent)" + seen="" + # shellcheck disable=SC2086 # word-splitting of the candidate list is intentional + for candidate in "$default_user_agent" "$preferred_user_agent" $SUBSCRIPTION_USER_AGENT_CANDIDATES; do + [ -n "$candidate" ] || continue + # Skip a candidate already emitted. Wrap stored names in newlines so the + # substring test matches whole entries only. + case "$seen" in + *" +$candidate +"*) continue ;; + esac + seen="${seen} +$candidate +" + printf '%s\n' "$candidate" + done +} + +# Downloads a subscription body from the given URL with client-mimicking headers # Arguments: # $1 - subscription URL # $2 - output file path # $3 - http proxy address (optional) # $4 - retries (optional, default 3) # $5 - wait between retries (optional, default 2) +# $6 - timeout seconds (optional, default 10) +# $7 - User-Agent (optional; default "singbox/<version>") download_subscription() { local url="$1" local filepath="$2" @@ -674,12 +732,14 @@ download_subscription() { local retries="${4:-3}" local wait="${5:-2}" local timeout="${6:-10}" + local user_agent="${7:-}" local sb_version device_model kernel_version hwid sb_version="$(get_sing_box_version)" device_model="$(get_device_model)" kernel_version="$(get_kernel_version)" hwid="$(generate_hwid)" + [ -n "$user_agent" ] || user_agent="$(get_subscription_user_agent)" local tmpfile errfile rc family tmpfile="${filepath}.part.$$" @@ -693,7 +753,7 @@ download_subscription() { if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ wget -4 -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: singbox/$sb_version" \ + --header "User-Agent: $user_agent" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ --header "X-Device-Model: $device_model" \ @@ -703,7 +763,7 @@ download_subscription() { "$url" 2>"$errfile" else wget -4 -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: singbox/$sb_version" \ + --header "User-Agent: $user_agent" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ --header "X-Device-Model: $device_model" \ @@ -716,7 +776,7 @@ download_subscription() { if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ wget -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: singbox/$sb_version" \ + --header "User-Agent: $user_agent" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ --header "X-Device-Model: $device_model" \ @@ -726,7 +786,7 @@ download_subscription() { "$url" 2>"$errfile" else wget -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: singbox/$sb_version" \ + --header "User-Agent: $user_agent" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ --header "X-Device-Model: $device_model" \ @@ -761,7 +821,7 @@ download_subscription() { if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ wget -4 -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: singbox/$sb_version" \ + --header "User-Agent: $user_agent" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ --header "X-Device-Model: $device_model" \ @@ -771,7 +831,7 @@ download_subscription() { "$url" 2>"$errfile" else wget -4 -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: singbox/$sb_version" \ + --header "User-Agent: $user_agent" \ --header "X-HWID: $hwid" \ --header "X-Device-OS: OpenWrt Linux" \ --header "X-Device-Model: $device_model" \ @@ -972,14 +1032,197 @@ describe_subscription_validation_failure() { echo "subscription contains no usable proxy outbounds: total=${total:-unknown}, usable=${usable:-unknown}" } +# Convert an "Xray JSON" subscription body into a newline-separated list of +# proxy share URIs (one per line) that the fallback parser's URI loop can +# consume. +# +# An "Xray JSON" body is what several panels (e.g. the Xray/v2rayN ecosystem) +# hand out instead of a sing-box config: either a single Xray client config +# object or, more commonly, a JSON ARRAY of such objects. Each object carries +# an `outbounds` array whose proxy members use the Xray schema +# (`protocol` + `settings.vnext`/`settings.servers` + `streamSettings`), which +# is NOT the sing-box outbound schema. validate_subscription_file() rejects it +# (its outbounds have no sing-box `type`), so without this converter the whole +# subscription is unusable. +# +# Strategy: for every config object we emit one `vless://` / `trojan://` / +# `ss://` share URI per *directly usable* proxy outbound, i.e. one that does +# NOT declare `streamSettings.sockopt.dialerProxy` (a chained / multi-hop +# upstream that cannot be expressed as a single share link). The resulting URIs +# carry the standard query params the facade already understands +# (security/sni/fp/pbk/sid/flow/type/path/host/mode/alpn), so they flow through +# the existing sing_box_cf_add_proxy_outbound path unchanged. The outbound tag +# (or the config `remarks`) becomes the URI fragment so the node keeps a +# human-readable name. +# +# CRITICAL: OpenWRT's jq has no Oniguruma, so the program below uses only +# explicit string operations (no test/match/sub/gsub). It also keeps every +# query VALUE free of '& ? # %' and whitespace, because url_get_query_param() +# (helpers.sh) stops a value at the first such delimiter. +# +# Arguments: +# src_file: path to the raw downloaded subscription body +# Returns: +# 0 and prints the URI lines to stdout when at least one outbound converted; +# 1 (and prints nothing) otherwise. +xray_json_to_uri_lines() { + local src_file="$1" + + [ -s "$src_file" ] || return 1 + + # Quick structural gate before invoking jq: the body must be valid JSON + # whose (array element | object) carries Xray-style proxy outbounds. We let + # jq make the authoritative decision and emit the URIs in one pass. + jq -er ' + # Normalize the document to an array of Xray config objects. + (if type == "array" then . else [.] end) as $configs + + # A query value is only safe for url_get_query_param when it is present + # (not JSON null) and carries none of these delimiters/whitespace; + # otherwise drop the param entirely. NB: a missing Xray field reads as + # JSON null, and (null | tostring) == "null" — we must treat that as + # absent, never emit a literal "null" value (e.g. sid=null). + | def safe($v): + if $v == null then "" + else + ($v | tostring) as $s + | if ($s == "") then "" + elif ($s | (index("&") // index("?") // index("#") + // index(" ") // index("%") + // index("\t") // index("\n"))) != null then "" + else $s end + end; + + # Build "key=value" only when value is present and delimiter-safe. + def kv($k; $v): + safe($v) as $s + | if $s == "" then empty else ($k + "=" + $s) end; + + [ $configs[] + | (.remarks // "") as $cfg_name + | (.outbounds // [])[] + | select(type == "object") + | select(.protocol == "vless" or .protocol == "trojan" + or .protocol == "shadowsocks") + # Skip chained / multi-hop outbounds: not representable as one URI. + | select((.streamSettings.sockopt.dialerProxy // "") == "") + | . as $ob + | (.streamSettings // {}) as $ss + | ($ss.network // "tcp") as $net + | ($ss.security // "") as $sec + | ($ss.realitySettings // {}) as $reality + | ($ss.tlsSettings // $ss.realitySettings // {}) as $tls + # vnext (vless/vmess) vs servers (trojan/shadowsocks) addressing. + | ($ob.settings.vnext[0] // $ob.settings.servers[0] // {}) as $peer + | ($peer.users[0] // {}) as $user + | ($peer.address // "") as $host + | ($peer.port // "") as $port + | select($host != "" and ($port | tostring) != "") + | ($ob.tag // $cfg_name) as $name + # Build the query param list per protocol, dropping empties. + | ( + if $ob.protocol == "vless" then + ([ "encryption=none", + ("type=" + $net), + kv("flow"; $user.flow), + (if $sec != "" then ("security=" + $sec) else empty end), + kv("sni"; ($tls.serverName // "")) ]) + + (if $sec == "reality" then + [ kv("pbk"; $reality.publicKey), + kv("sid"; $reality.shortId), + kv("fp"; ($reality.fingerprint // "chrome")) ] + else + [ kv("fp"; ($tls.fingerprint // "")) ] + end) + elif $ob.protocol == "trojan" then + [ ("type=" + $net), + (if $sec != "" then ("security=" + ($sec)) else "security=tls" end), + kv("sni"; ($tls.serverName // "")), + kv("fp"; ($tls.fingerprint // "")) ] + else + [ ("type=" + $net) ] + end + ) as $base + # Transport-specific params (ws / xhttp / grpc). + | ( + if $net == "ws" then + [ kv("path"; ($ss.wsSettings.path // "")), + kv("host"; ($ss.wsSettings.headers.Host // "")) ] + elif $net == "xhttp" then + [ kv("path"; ($ss.xhttpSettings.path // "")), + kv("host"; ($ss.xhttpSettings.host // "")), + kv("mode"; ($ss.xhttpSettings.mode // "")) ] + elif $net == "grpc" then + [ kv("serviceName"; ($ss.grpcSettings.serviceName // "")) ] + else [] end + ) as $transport + # alpn is a JSON array in Xray; flatten to a comma string (no spaces). + | ([ ($tls.alpn // [])[] | tostring ] | join(",")) as $alpn_str + | ($base + $transport + + (if $alpn_str != "" then [ kv("alpn"; $alpn_str) ] else [] end) + | map(select(. != null and . != ""))) as $query + # Credential: uuid for vless, password for trojan/shadowsocks. + | (if $ob.protocol == "vless" then ($user.id // "") + else ($peer.password // $ob.settings.password // "") end) as $cred + | select($cred != "") + | ($ob.protocol + | if . == "shadowsocks" then "ss" else . end) as $scheme + # The connection part (no #fragment) is the dedup key: providers that + # ship one server set across many "profiles" repeat identical nodes + # with only the display name differing, which would otherwise inflate + # the list into thousands of duplicates. + | ($scheme + "://" + $cred + "@" + $host + ":" + ($port | tostring) + + (if ($query | length) > 0 then "?" + ($query | join("&")) else "" end) + ) as $conn + | { conn: $conn, + uri: ($conn + (if $name != "" then "#" + $name else "" end)) } + ] + # Deduplicate on $conn, preserving first-seen order (no sort): a + # label/break reduce over already-seen keys. Avoids unique_by (which + # reorders) and stays within the no-regex jq subset on OpenWRT. + | reduce .[] as $e ({ seen: [], out: [] }; + if (.seen | index($e.conn)) != null then . + else .seen += [$e.conn] | .out += [$e.uri] end) + | .out + | select(length > 0) + | .[] + ' "$src_file" 2>/dev/null +} + +# Count the Xray-JSON proxy outbounds that look like real nodes but use a +# protocol the NetShift facade cannot build (today: vmess — the facade has no +# vmess outbound). These are silently dropped by xray_json_to_uri_lines, so we +# count them separately to surface an explicit warning to the user instead of +# leaving them to wonder why a node count came up short. Chained (dialerProxy) +# outbounds are NOT counted here — those are deliberately collapsed, not +# "unsupported". Prints a single integer (0 when none / on any error). +xray_json_count_unsupported() { + local src_file="$1" + + [ -s "$src_file" ] || { + echo 0 + return 0 + } + + jq -er ' + [ (if type == "array" then . else [.] end)[] + | (.outbounds // [])[] + | select(type == "object") + | select((.streamSettings.sockopt.dialerProxy // "") == "") + | select(.protocol == "vmess") + ] | length + ' "$src_file" 2>/dev/null || echo 0 +} + # Fallback subscription parser. # # Many providers do not return a sing-box JSON config. Instead they return # either (a) a base64-encoded list of proxy URIs, or (b) a plaintext list of # proxy URIs (one per line), possibly interspersed with '#comment' metadata -# lines. This function decodes/parses such a body into a minimal sing-box -# configuration ({"outbounds":[...]}) so the normal persist + merge path can -# consume it unchanged. +# lines, or (c) an "Xray JSON" config (object or array of objects, handled via +# xray_json_to_uri_lines above). This function decodes/parses such a body into +# a minimal sing-box configuration ({"outbounds":[...]}) so the normal persist +# + merge path can consume it unchanged. # # It lives in helpers.sh (alongside validate_subscription_file). It calls # sing_box_cf_add_proxy_outbound, which is defined later in @@ -1002,7 +1245,7 @@ normalize_subscription_to_singbox() { local raw stripped candidate pad_len decoded bom local udp_over_tcp config new_config lines_file local line scheme idx kept skipped before_count after_count final_count - local fragment display_name + local fragment display_name first_char xray_uris xray_unsupported [ -s "$src_file" ] || return 1 # Strip a leading UTF-8 BOM (EF BB BF) if present; it would otherwise break @@ -1013,6 +1256,32 @@ normalize_subscription_to_singbox() { [ -n "$raw" ] || raw="$(cat "$src_file" 2>/dev/null)" [ -n "$raw" ] || return 1 + # Xray-JSON detection (before base64/URI handling). When the body is a JSON + # object/array of Xray client configs, convert its proxy outbounds to share + # URIs and feed those through the URI loop below. Only attempt this when the + # first non-whitespace byte is '{' or '[' (cheap pre-gate) so plaintext URI + # lists never pay the jq cost. + first_char="$(printf '%s' "$raw" | sed -n '1{s/^[[:space:]]*//;s/\(.\).*/\1/p;};1q' 2>/dev/null)" + case "$first_char" in + '{' | '[') + xray_uris="$(xray_json_to_uri_lines "$src_file" 2>/dev/null)" + if [ -n "$xray_uris" ]; then + log "Detected Xray JSON subscription for '$section'; converting proxy outbounds to share URIs" "debug" + raw="$xray_uris" + # Surface unsupported protocols (vmess) explicitly: they are dropped + # by the converter because the facade cannot build them, and a silent + # drop looks like a bug to the user. + xray_unsupported="$(xray_json_count_unsupported "$src_file")" + case "$xray_unsupported" in + '' | *[!0-9]*) xray_unsupported=0 ;; + esac + if [ "$xray_unsupported" -gt 0 ]; then + log "Xray JSON subscription for '$section' has $xray_unsupported VMess node(s); VMess is not supported and they were skipped" "warn" + fi + fi + ;; + esac + # Decide whether the body is a base64 blob or already plaintext URIs. # Be conservative: only treat as base64 when the raw body has NO '://' # substring (a plaintext URI list always contains '://') but the decoded diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index d062a7d0..e7fa54b6 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -746,6 +746,303 @@ else fi rm -f "$caseD_in" "$caseD_out" +# ── CASE E: Xray JSON subscription (array of Xray client configs) ─── +# A provider that returns an "Xray JSON" body instead of a sing-box config: +# an array of Xray configs whose proxy outbounds use the Xray schema +# (protocol + settings.vnext + streamSettings). The normalizer must detect +# this, convert the directly-usable (non-dialerProxy) outbounds to share URIs +# and produce a valid sing-box config. The chained (sockopt.dialerProxy) +# outbound must be skipped. +caseE_in="/tmp/netshift-fb-caseE-$$.json" +caseE_out="/tmp/netshift-fb-caseE-out-$$.json" +cat > "$caseE_in" << 'XRAYJSON' +[ + { + "remarks": "Reality TCP", + "outbounds": [ + { + "protocol": "vless", + "tag": "proxy-reality", + "settings": {"vnext": [{"address": "uk.example.com", "port": 8443, + "users": [{"id": "59e308c0-071d-4214-bb4a-64a2409d9e3b", + "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "dY9SNEllJMW63xo-JdXufhmjAxB", + "shortId": "c20b1035d72d7793", "serverName": "storage.yandex.net", + "fingerprint": "firefox"}} + } + ] + }, + { + "remarks": "WS TLS", + "outbounds": [ + { + "protocol": "vless", + "tag": "proxy-ws", + "settings": {"vnext": [{"address": "ws.example.com", "port": 443, + "users": [{"id": "dea6c6da-3903-4dbc-b98c-e79364764f9f", + "flow": "", "encryption": "none"}]}]}, + "streamSettings": {"network": "ws", "security": "tls", + "tlsSettings": {"serverName": "ws.example.com"}, + "wsSettings": {"path": "/livestreamcontent/", + "headers": {"Host": "ws.example.com"}}} + }, + { + "protocol": "vless", + "tag": "proxy-chained", + "settings": {"vnext": [{"address": "bypass.example.com", "port": 8443, + "users": [{"id": "8c459cd3-f3b0-496c-9d87-138d292ecdf6", + "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "sockopt": {"dialerProxy": "upstream-0"}, + "realitySettings": {"publicKey": "abc", "shortId": "def", + "serverName": "storage.yandex.net", "fingerprint": "firefox"}} + } + ] + } +] +XRAYJSON + +if normalize_subscription_to_singbox "$caseE_in" "$caseE_out" "testsub"; then + echo 'fb-caseE-rc:OK' +else + echo 'fb-caseE-rc:FAIL' +fi +# Exactly two usable outbounds: reality-tcp + ws-tls; the dialerProxy one skipped. +e_len="$(jq -r '.outbounds | length' "$caseE_out" 2>/dev/null)" +[ -n "$e_len" ] || e_len=0 +if [ "$e_len" -eq 2 ]; then + echo "fb-caseE-count(==2 got $e_len):OK" +else + echo "fb-caseE-count(==2 got $e_len):FAIL" +fi +if validate_subscription_file "$caseE_out"; then + echo 'fb-caseE-validate:OK' +else + echo 'fb-caseE-validate:FAIL' +fi +# The reality outbound must carry the converted reality block + flow. +if jq -e '[.outbounds[] | select(.type == "vless" + and .tls.reality.public_key == "dY9SNEllJMW63xo-JdXufhmjAxB" + and .flow == "xtls-rprx-vision")] | length == 1' "$caseE_out" \ + > /dev/null 2>&1; then + echo 'fb-caseE-reality-fields:OK' +else + echo 'fb-caseE-reality-fields:FAIL' +fi +rm -f "$caseE_in" "$caseE_out" + +# ── CASE F: Xray JSON reality node WITHOUT shortId ────────────────── +# Regression guard: a missing Xray field reads as JSON null, and a naive +# (null | tostring) would emit a literal "sid=null" query param, which +# sing-box would then store as short_id:"null". The converter must drop the +# absent param entirely, so the produced reality block carries NO short_id. +caseF_in="/tmp/netshift-fb-caseF-$$.json" +caseF_out="/tmp/netshift-fb-caseF-out-$$.json" +cat > "$caseF_in" << 'XRAYJSON' +[ + { + "remarks": "no-sid", + "outbounds": [ + { + "protocol": "vless", + "tag": "proxy-no-sid", + "settings": {"vnext": [{"address": "ru.example.com", "port": 443, + "users": [{"id": "1dff23f6-b2f1-4242-9746-b586808ed302", + "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "G2i-nsQgWiVf52tdCUV", + "serverName": "cloudrynth.com", "fingerprint": "firefox"}} + } + ] + } +] +XRAYJSON + +if normalize_subscription_to_singbox "$caseF_in" "$caseF_out" "testsub"; then + echo 'fb-caseF-rc:OK' +else + echo 'fb-caseF-rc:FAIL' +fi +if validate_subscription_file "$caseF_out"; then + echo 'fb-caseF-validate:OK' +else + echo 'fb-caseF-validate:FAIL' +fi +# No outbound may carry a literal "null" short_id, and the public_key must be set. +if jq -e '([.outbounds[].tls.reality.short_id // empty] | map(select(. == "null")) | length) == 0 + and ([.outbounds[] | select(.tls.reality.public_key == "G2i-nsQgWiVf52tdCUV")] | length == 1)' \ + "$caseF_out" > /dev/null 2>&1; then + echo 'fb-caseF-no-null-sid:OK' +else + echo 'fb-caseF-no-null-sid:FAIL' +fi +rm -f "$caseF_in" "$caseF_out" + +# ── CASE G: Xray JSON duplicate-node dedup ────────────────────────── +# Providers commonly ship one server set across many "profiles"/balancers, +# repeating identical nodes with only the display name differing. The +# converter must dedup on the connection part (ignoring the #name), so N +# copies of the same server collapse to one. Here three configs reference the +# same two servers (A, B) plus one extra (C) -> exactly 3 unique outbounds. +caseG_in="/tmp/netshift-fb-caseG-$$.json" +caseG_out="/tmp/netshift-fb-caseG-out-$$.json" +cat > "$caseG_in" << 'XRAYJSON' +[ + {"remarks": "profile-1", "outbounds": [ + {"protocol": "vless", "tag": "A", "settings": {"vnext": [{"address": "a.example.com", "port": 443, + "users": [{"id": "11111111-1111-1111-1111-111111111111", "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "PK", "shortId": "ab", "serverName": "a.example.com", "fingerprint": "firefox"}}}, + {"protocol": "vless", "tag": "B", "settings": {"vnext": [{"address": "b.example.com", "port": 443, + "users": [{"id": "22222222-2222-2222-2222-222222222222", "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "PK", "shortId": "cd", "serverName": "b.example.com", "fingerprint": "firefox"}}} + ]}, + {"remarks": "profile-2", "outbounds": [ + {"protocol": "vless", "tag": "A-copy", "settings": {"vnext": [{"address": "a.example.com", "port": 443, + "users": [{"id": "11111111-1111-1111-1111-111111111111", "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "PK", "shortId": "ab", "serverName": "a.example.com", "fingerprint": "firefox"}}}, + {"protocol": "vless", "tag": "B-copy", "settings": {"vnext": [{"address": "b.example.com", "port": 443, + "users": [{"id": "22222222-2222-2222-2222-222222222222", "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "PK", "shortId": "cd", "serverName": "b.example.com", "fingerprint": "firefox"}}} + ]}, + {"remarks": "profile-3", "outbounds": [ + {"protocol": "vless", "tag": "C", "settings": {"vnext": [{"address": "c.example.com", "port": 443, + "users": [{"id": "33333333-3333-3333-3333-333333333333", "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "PK", "shortId": "ef", "serverName": "c.example.com", "fingerprint": "firefox"}}} + ]} +] +XRAYJSON + +if normalize_subscription_to_singbox "$caseG_in" "$caseG_out" "testsub"; then + echo 'fb-caseG-rc:OK' +else + echo 'fb-caseG-rc:FAIL' +fi +# 5 raw nodes (A,B,A-copy,B-copy,C) must dedup to 3 unique servers (A,B,C). +g_len="$(jq -r '.outbounds | length' "$caseG_out" 2>/dev/null)" +[ -n "$g_len" ] || g_len=0 +if [ "$g_len" -eq 3 ]; then + echo "fb-caseG-dedup(==3 got $g_len):OK" +else + echo "fb-caseG-dedup(==3 got $g_len):FAIL" +fi +# All three distinct servers must survive (a, b, c). +if jq -e '([.outbounds[].server] | sort) == ["a.example.com","b.example.com","c.example.com"]' \ + "$caseG_out" > /dev/null 2>&1; then + echo 'fb-caseG-servers:OK' +else + echo 'fb-caseG-servers:FAIL' +fi +rm -f "$caseG_in" "$caseG_out" + +# ── CASE H: Xray JSON with unsupported VMess alongside VLESS ──────── +# The facade cannot build VMess. The converter must skip vmess but keep the +# vless node, and xray_json_count_unsupported must report the dropped vmess so +# the backend can warn the user instead of silently losing it. +caseH_in="/tmp/netshift-fb-caseH-$$.json" +caseH_out="/tmp/netshift-fb-caseH-out-$$.json" +cat > "$caseH_in" << 'XRAYJSON' +[ + { + "remarks": "mixed", + "outbounds": [ + { + "protocol": "vless", + "tag": "ok-vless", + "settings": {"vnext": [{"address": "vl.example.com", "port": 443, + "users": [{"id": "11111111-1111-1111-1111-111111111111", + "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "PK", "shortId": "ab", + "serverName": "vl.example.com", "fingerprint": "firefox"}} + }, + { + "protocol": "vmess", + "tag": "drop-vmess", + "settings": {"vnext": [{"address": "vm.example.com", "port": 443, + "users": [{"id": "22222222-2222-2222-2222-222222222222", + "alterId": 0, "security": "auto"}]}]}, + "streamSettings": {"network": "tcp", "security": "tls", + "tlsSettings": {"serverName": "vm.example.com"}} + } + ] + } +] +XRAYJSON + +if normalize_subscription_to_singbox "$caseH_in" "$caseH_out" "testsub"; then + echo 'fb-caseH-rc:OK' +else + echo 'fb-caseH-rc:FAIL' +fi +# Exactly one usable outbound (vless); vmess dropped. +h_len="$(jq -r '.outbounds | length' "$caseH_out" 2>/dev/null)" +[ -n "$h_len" ] || h_len=0 +if [ "$h_len" -eq 1 ] \ + && jq -e '.outbounds[0].server == "vl.example.com"' "$caseH_out" >/dev/null 2>&1; then + echo "fb-caseH-vless-kept(==1 got $h_len):OK" +else + echo "fb-caseH-vless-kept(==1 got $h_len):FAIL" +fi +# The unsupported-protocol counter must report exactly one vmess. +h_unsup="$(xray_json_count_unsupported "$caseH_in")" +if [ "$h_unsup" = "1" ]; then + echo 'fb-caseH-vmess-counted:OK' +else + echo "fb-caseH-vmess-counted(==1 got $h_unsup):FAIL" +fi +rm -f "$caseH_in" "$caseH_out" + +# ── CASE I: subscription User-Agent candidate building ────────────── +# Auto mode (no configured UA) must emit, in order and without duplicates: +# the default singbox/<ver> first, then the cached/preferred UA, then the +# constants whitelist. A configured UA must short-circuit to exactly itself. +caseI_default="$(get_subscription_user_agent)" + +# (a) Auto mode, no preferred: first line is the default; v2rayN present; no dup default. +caseI_auto="$(build_subscription_user_agent_candidates "" "")" +caseI_first="$(printf '%s\n' "$caseI_auto" | sed -n '1p')" +if [ "$caseI_first" = "$caseI_default" ]; then + echo 'fb-caseI-auto-default-first:OK' +else + echo "fb-caseI-auto-default-first(got '$caseI_first'):FAIL" +fi +if printf '%s\n' "$caseI_auto" | grep -Fxq 'v2rayN'; then + echo 'fb-caseI-auto-has-v2rayN:OK' +else + echo 'fb-caseI-auto-has-v2rayN:FAIL' +fi +caseI_default_count="$(printf '%s\n' "$caseI_auto" | grep -Fxc "$caseI_default")" +if [ "$caseI_default_count" = "1" ]; then + echo 'fb-caseI-auto-default-unique:OK' +else + echo "fb-caseI-auto-default-unique(got $caseI_default_count):FAIL" +fi + +# (b) Preferred UA is emitted right after the default and only once. +caseI_pref="$(build_subscription_user_agent_candidates "" "Hiddify")" +caseI_second="$(printf '%s\n' "$caseI_pref" | sed -n '2p')" +caseI_hid_count="$(printf '%s\n' "$caseI_pref" | grep -Fxc 'Hiddify')" +if [ "$caseI_second" = "Hiddify" ] && [ "$caseI_hid_count" = "1" ]; then + echo 'fb-caseI-preferred-second-unique:OK' +else + echo "fb-caseI-preferred-second-unique(2nd='$caseI_second' count=$caseI_hid_count):FAIL" +fi + +# (c) Configured UA short-circuits to exactly one line = itself. +caseI_conf="$(build_subscription_user_agent_candidates "MyClient/1.0" "Hiddify")" +caseI_conf_lines="$(printf '%s\n' "$caseI_conf" | grep -c .)" +if [ "$caseI_conf" = "MyClient/1.0" ] && [ "$caseI_conf_lines" = "1" ]; then + echo 'fb-caseI-configured-only:OK' +else + echo "fb-caseI-configured-only(got '$caseI_conf' lines=$caseI_conf_lines):FAIL" +fi + echo 'DONE' FBEOF From fe4dbb978f351335c116d2ca804000e8cc393ef9 Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Thu, 4 Jun 2026 19:58:49 +0300 Subject: [PATCH 42/75] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20include/exclude-=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B2=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=D1=85=20=D0=BF=D0=BE=20=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B5=D0=B2=D1=8B=D0=BC=20=D1=81=D0=BB=D0=BE=D0=B2=D0=B0=D0=BC?= =?UTF-8?q?=20=D0=B2=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 11 +- .../memory/architect-orchestrator.md | 32 ++++ .../memory/luci-frontend-developer.md | 16 ++ .../memory/shell-backend-developer.md | 20 +++ fe-app-netshift/locales/calls.json | 168 ++++++++++-------- fe-app-netshift/locales/netshift.pot | 160 +++++++++-------- fe-app-netshift/locales/netshift.ru.po | 16 +- fe-app-netshift/src/netshift/types.ts | 2 + .../resources/view/netshift/section.js | 18 ++ luci-app-netshift/po/ru/netshift.po | 16 +- luci-app-netshift/po/templates/netshift.pot | 160 +++++++++-------- netshift/files/etc/config/netshift | 7 + netshift/files/usr/bin/netshift | 52 +++++- .../files/usr/lib/sing_box_config_facade.sh | 73 +++++++- tests/entrypoint.sh | 97 ++++++++++ 15 files changed, 623 insertions(+), 225 deletions(-) diff --git a/.gitignore b/.gitignore index 5a6b8f9a..f68151c7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,13 @@ fe-app-netshift/.env .DS_Store *.txt tests/test-results/ -docs/tasks \ No newline at end of file +docs/tasks +fe-app-netshift/coverage/ # vitest --coverage +fe-app-netshift/dist/ # на случай tsup dist (бандл идёт в luci-app, но dist может появиться) +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +fe-app-netshift/.yarn/ +fe-app-netshift/.yarnrc.yml +fe-app-netshift/.pnp.* \ No newline at end of file diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index a51ffdae..addf8622 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -90,3 +90,35 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> - Contribution gating: `CODEOWNERS=@yandexru45`; PRs accepted only after Telegram coordination with authors (README). Reflect this in `/describe` output. +- **Frontend yarn trap (verified 2026-06):** repo `fe-app-netshift/yarn.lock` is + CLASSIC yarn v1 format; there is NO `packageManager` pin and NO `.yarnrc.yml`. + A local corepack yarn 4.x will try to MIGRATE on `yarn install`, polluting the + tree with a 3000+ line `yarn.lock` rewrite + untracked `.yarn/` and + `.yarnrc.yml`. These are NOT deliverables — discard before commit + (`git checkout -- fe-app-netshift/yarn.lock`; rm `.yarn/`/`.yarnrc.yml`). To + verify the gate independently without polluting, run the tools directly from + `node_modules/.bin` (prettier/eslint/vitest/tsup) instead of `yarn install`. + Tell frontend devs to leave yarn.lock alone. +- The frontend-ci `main.js` no-diff check: a TYPE-ONLY change in TS source + (e.g. adding optional fields to a `types.ts` interface) produces NO main.js + diff — that is expected/correct, not a missed rebuild. + +## Subscription keyword filter (issue #5, task-002/003 — done 2026-06) + +- Backend filter lives in `sing_box_cf_prepare_subscription_batch` + (sing_box_config_facade.sh): one jq pass between candidate-select and the + static-unsupported filter, BEFORE tag dedup + sing-box check. Covers native + + all fallback (base64/URI/Xray) bodies and both selector branches automatically. +- UCI options (cross-layer contract, verbatim): `subscription_filter_include_keywords` + (whitelist) / `subscription_filter_exclude_keywords` (blacklist), both UCI + `list`. Read in the `subscription)` branch via `config_list_foreach`. +- Semantics: include=OR (empty⇒keep all), exclude=OR(drop), SUBSTRING, + ASCII-case-insensitive (`ascii_downcase`), byte-exact for emoji/Cyrillic. + jq: NOTE `include`/`exclude` are RESERVED jq words — devs used `$inc`/`$exc`; + matching must use `. as $kw` inside any/all to avoid the `.`-after-pipe rebind. +- Empty-after-filter ⇒ existing fail-safe `mark_subscription_outbound_unavailable` + + warn (NO exit 1). `skipped` stays "statically unsupported" (compute `$total` + AFTER the keyword filter, not before). +- UI: two `form.DynamicList` in `section.js` after `subscription_group_by_countries`, + rmempty=true, NO validator (keep emoji/space verbatim); `string[]?` fields on + `ConfigProxySubscriptionSection` in types.ts; ru/en via locale tooling. diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index b95b221f..50a3379b 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -53,6 +53,22 @@ append findings; keep under ~200 lines. - Wrap user-facing strings in `_()`, and only STRING LITERALS — the gettext extractor (`yarn locales:actualize`) only sees literal args. Some validator messages (vless/trojan) are currently NOT wrapped — wrap new ones. +- i18n pipeline (`yarn locales:actualize` = extract-calls → generate-pot → + generate-po:ru → distribute). `extract-calls.js` scans BOTH `src/**/*.ts` AND + the hand-written `luci-app-netshift/.../view/netshift/**/*.js` (excludes + `main.js`), so `_()` strings added in `section.js` ARE extracted. Source of + truth for ru text is `fe-app-netshift/locales/netshift.ru.po` — fill the new + empty `msgstr` there (preserves existing translations on regen), then re-run + `yarn locales:distribute` to copy into the generated catalogs + `luci-app-netshift/po/{templates/netshift.pot, ru/netshift.po}`. Touched + catalog files: `locales/calls.json`, `locales/netshift.pot`, + `locales/netshift.ru.po`, `po/templates/netshift.pot`, `po/ru/netshift.po`. +- TYPE-ONLY changes to `src/**` (e.g. adding interface fields in `types.ts`, + inside the `NetShift` namespace) erase at build → `main.js` has NO diff after + `yarn build`. Expect a clean `git diff` on `main.js` for pure-type edits. +- `section.js` is a hand-written LuCI view, NOT bundled — `yarn format` only + touches `src/`, so format leaves section.js alone; keep its existing + 2-space/double-quote style manually. ## Version placeholder diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 18cbbac9..224937dd 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -57,6 +57,26 @@ findings; keep under ~200 lines. - Pre-commit-equivalent: always run the `shellcheck` skill (severity error) on touched shell files before handing back. +## jq gotchas (proven by task-002) + +- **`include` / `exclude` are RESERVED jq keywords** — you cannot name a jq + variable `$include` (jq tries to parse the `include` directive). Use `$inc`/ + `$exc` etc. for keyword-filter lists. +- **`any(gen; cond)` / `all(gen; cond)` binding trap**: inside the condition, + `.` is the generator element ONLY at the top of `cond`. If you write + `($name | index(.))` the `.` becomes `$name` (the pipe rebinds `.`), so the + match silently always succeeds. Bind first: `any($kw[]; . as $k | ($name | + index($k)) != null)`. +- Subscription keyword filter lives in `sing_box_cf_prepare_subscription_batch` + (facade), runs BEFORE static-unsupported filter + tag dedup, threaded from the + `subscription)` branch via two UCI **list** options + `subscription_filter_include_keywords` / `subscription_filter_exclude_keywords` + (the cross-layer contract names for task-003 — do NOT rename). Keywords are + opaque user text: collect with a `config_list_foreach` handler that jq + `--arg`-appends each item into a JSON array (commas/emoji survive; never use + `comma_string_to_json_array` for them). Empty result reuses the existing + `mark_subscription_outbound_unavailable` fail-safe (no `exit 1`). + ## Known landmines - nft proxy chain hardcodes `127.0.0.1:1602` (duplicates the constants). diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index 8a57d517..4e491ab6 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -66,21 +66,21 @@ "call": "Applicable for SOCKS and Shadowsocks proxy", "key": "Applicable for SOCKS and Shadowsocks proxy", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:251" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:269" ] }, { "call": "At least one valid domain must be specified. Comments-only content is not allowed.", "key": "At least one valid domain must be specified. Comments-only content is not allowed.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:496" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:514" ] }, { "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:577" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:595" ] }, { @@ -192,7 +192,7 @@ "call": "Community Lists", "key": "Community Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:351" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:369" ] }, { @@ -311,8 +311,8 @@ "call": "Disabled", "key": "Disabled", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:442", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:522" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:460", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:540" ] }, { @@ -326,7 +326,7 @@ "call": "DNS over HTTPS (DoH)", "key": "DNS over HTTPS (DoH)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:319", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:337", "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:15" ] }, @@ -334,7 +334,7 @@ "call": "DNS over TLS (DoT)", "key": "DNS over TLS (DoT)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:320", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:338", "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:16" ] }, @@ -342,7 +342,7 @@ "call": "DNS Protocol Type", "key": "DNS Protocol Type", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:316", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:334", "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:12" ] }, @@ -357,7 +357,7 @@ "call": "DNS Server", "key": "DNS Server", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:329", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:347", "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:24" ] }, @@ -379,7 +379,7 @@ "call": "Domain Resolver", "key": "Domain Resolver", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:306" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:324" ] }, { @@ -426,12 +426,19 @@ "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:298" ] }, + { + "call": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", + "key": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:147" + ] + }, { "call": "Dynamic List", "key": "Dynamic List", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:443", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:523" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:461", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:541" ] }, { @@ -445,21 +452,21 @@ "call": "Enable built-in DNS resolver for domains handled by this section", "key": "Enable built-in DNS resolver for domains handled by this section", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:307" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:325" ] }, { "call": "Enable DNS resolve to get real IP when routing", "key": "Enable DNS resolve to get real IP when routing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:746" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:764" ] }, { "call": "Enable Mixed Proxy", "key": "Enable Mixed Proxy", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:717" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:735" ] }, { @@ -473,7 +480,7 @@ "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:718" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:736" ] }, { @@ -501,21 +508,21 @@ "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:478" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:496" ] }, { "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:452" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:470" ] }, { "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:532" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:550" ] }, { @@ -529,7 +536,7 @@ "call": "Every 1 minute", "key": "Every 1 minute", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:187" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:205" ] }, { @@ -550,7 +557,7 @@ "call": "Every 3 minutes", "key": "Every 3 minutes", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:188" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:206" ] }, { @@ -564,14 +571,14 @@ "call": "Every 30 seconds", "key": "Every 30 seconds", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:186" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:204" ] }, { "call": "Every 5 minutes", "key": "Every 5 minutes", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:189" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:207" ] }, { @@ -609,6 +616,13 @@ "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:403" ] }, + { + "call": "Exclude servers by keyword", + "key": "Exclude servers by keyword", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:146" + ] + }, { "call": "Failed to copy!", "key": "Failed to copy!", @@ -644,7 +658,7 @@ "call": "Fully Routed IPs", "key": "Fully Routed IPs", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:690" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:708" ] }, { @@ -675,6 +689,13 @@ "src\\netshift\\api.ts:27" ] }, + { + "call": "Include servers by keyword", + "key": "Include servers by keyword", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:137" + ] + }, { "call": "Install extended", "key": "Install extended", @@ -1027,6 +1048,13 @@ "src\\netshift\\tabs\\diagnostic\\helpers\\getMeta.ts:20" ] }, + { + "call": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", + "key": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:138" + ] + }, { "call": "Latest", "key": "Latest", @@ -1045,14 +1073,14 @@ "call": "Local Domain Lists", "key": "Local Domain Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:598" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:616" ] }, { "call": "Local Subnet Lists", "key": "Local Subnet Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:621" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:639" ] }, { @@ -1080,7 +1108,7 @@ "call": "Mixed Proxy Port", "key": "Mixed Proxy Port", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:730" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:748" ] }, { @@ -1094,7 +1122,7 @@ "call": "Must be a number in the range of 50 - 1000", "key": "Must be a number in the range of 50 - 1000", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:215" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:233" ] }, { @@ -1122,7 +1150,7 @@ "call": "Network Interface", "key": "Network Interface", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:260" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:278" ] }, { @@ -1258,28 +1286,28 @@ "call": "Regional options cannot be used together", "key": "Regional options cannot be used together", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:385" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:403" ] }, { "call": "Remote Domain Lists", "key": "Remote Domain Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:644" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:662" ] }, { "call": "Remote Subnet Lists", "key": "Remote Subnet Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:667" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:685" ] }, { "call": "Resolve real IP for routing", "key": "Resolve real IP for routing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:745" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:763" ] }, { @@ -1363,7 +1391,7 @@ "call": "Russia inside restrictions", "key": "Russia inside restrictions", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:404" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:422" ] }, { @@ -1384,7 +1412,7 @@ "call": "Select a predefined list for routing", "key": "Select a predefined list for routing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:352" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:370" ] }, { @@ -1419,14 +1447,14 @@ "call": "Select network interface for VPN connection", "key": "Select network interface for VPN connection", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:261" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:279" ] }, { "call": "Select or enter DNS server address", "key": "Select or enter DNS server address", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:330", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:348", "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:25" ] }, @@ -1448,21 +1476,21 @@ "call": "Select the DNS protocol type for the domain resolver", "key": "Select the DNS protocol type for the domain resolver", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:317" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:335" ] }, { "call": "Select the list type for adding custom domains", "key": "Select the list type for adding custom domains", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:440" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:458" ] }, { "call": "Select the list type for adding custom subnets", "key": "Select the list type for adding custom subnets", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:520" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:538" ] }, { @@ -1504,7 +1532,7 @@ "call": "Selector Proxy Links", "key": "Selector Proxy Links", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:137" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:155" ] }, { @@ -1603,29 +1631,29 @@ "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:691" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:709" ] }, { "call": "Specify remote URLs to download and use domain lists", "key": "Specify remote URLs to download and use domain lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:645" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:663" ] }, { "call": "Specify remote URLs to download and use subnet lists", "key": "Specify remote URLs to download and use subnet lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:668" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:686" ] }, { "call": "Specify the path to the list file located on the router filesystem", "key": "Specify the path to the list file located on the router filesystem", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:599", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:622" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:617", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:640" ] }, { @@ -1702,8 +1730,8 @@ "call": "Text List", "key": "Text List", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:444", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:524" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:462", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:542" ] }, { @@ -1717,21 +1745,21 @@ "call": "The interval between connectivity tests", "key": "The interval between connectivity tests", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:184" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:202" ] }, { "call": "The maximum difference in response times (ms) allowed when comparing servers", "key": "The maximum difference in response times (ms) allowed when comparing servers", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:198" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:216" ] }, { "call": "The URL used to test server connectivity", "key": "The URL used to test server connectivity", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:222" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:240" ] }, { @@ -1780,7 +1808,7 @@ "call": "UDP (Unprotected DNS)", "key": "UDP (Unprotected DNS)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:321", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:339", "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:17" ] }, @@ -1788,7 +1816,7 @@ "call": "UDP over TCP", "key": "UDP over TCP", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:250" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:268" ] }, { @@ -1844,70 +1872,70 @@ "call": "URLTest Check Interval", "key": "URLTest Check Interval", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:183" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:201" ] }, { "call": "URLTest Proxy Links", "key": "URLTest Proxy Links", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:160" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:178" ] }, { "call": "URLTest Testing URL", "key": "URLTest Testing URL", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:221" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:239" ] }, { "call": "URLTest Tolerance", "key": "URLTest Tolerance", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:197" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:215" ] }, { "call": "User Domain List Type", "key": "User Domain List Type", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:439" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:457" ] }, { "call": "User Domains", "key": "User Domains", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:451" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:469" ] }, { "call": "User Domains List", "key": "User Domains List", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:477" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:495" ] }, { "call": "User Subnet List Type", "key": "User Subnet List Type", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:519" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:537" ] }, { "call": "User Subnets", "key": "User Subnets", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:531" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:549" ] }, { "call": "User Subnets List", "key": "User Subnets List", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:557" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:575" ] }, { @@ -1934,8 +1962,8 @@ "call": "Validation errors:", "key": "Validation errors:", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:510", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:589" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:528", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:607" ] }, { @@ -1958,22 +1986,22 @@ "key": "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "places": [ "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:38", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:138", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:161" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:156", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:179" ] }, { "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:387" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:405" ] }, { "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:406" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:424" ] }, { diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index 6e8ed43a..5ba7a1b9 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-02 14:15+0300\n" -"PO-Revision-Date: 2026-06-02 14:15+0300\n" +"POT-Creation-Date: 2026-06-04 16:41+0300\n" +"PO-Revision-Date: 2026-06-04 16:41+0300\n" "Last-Translator: yandexru45 <sukadark228@gmail.com>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -52,15 +52,15 @@ msgstr "" msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:251 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:269 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:496 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:514 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:577 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:595 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -127,7 +127,7 @@ msgstr "" msgid "Close" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:351 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:369 msgid "Community Lists" msgstr "" @@ -195,8 +195,8 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:442 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:522 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:460 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:540 msgid "Disabled" msgstr "" @@ -204,17 +204,17 @@ msgstr "" msgid "DNS on router" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:319 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:337 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:320 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:338 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:316 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:334 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:12 msgid "DNS Protocol Type" msgstr "" @@ -223,7 +223,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:329 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:347 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:24 msgid "DNS Server" msgstr "" @@ -236,7 +236,7 @@ msgstr "" msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:306 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:324 msgid "Domain Resolver" msgstr "" @@ -266,8 +266,12 @@ msgstr "" msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:443 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:523 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:147 +msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:461 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:541 msgid "Dynamic List" msgstr "" @@ -275,15 +279,15 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:307 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:325 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:746 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:764 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:717 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:735 msgid "Enable Mixed Proxy" msgstr "" @@ -291,7 +295,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:718 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:736 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -307,15 +311,15 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:478 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:496 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:452 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:470 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:532 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:550 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" @@ -323,7 +327,7 @@ msgstr "" msgid "Enter the subscription URL to fetch proxy configurations from your provider" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:187 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:205 msgid "Every 1 minute" msgstr "" @@ -335,7 +339,7 @@ msgstr "" msgid "Every 3 hours" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:188 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:206 msgid "Every 3 minutes" msgstr "" @@ -343,11 +347,11 @@ msgstr "" msgid "Every 30 minutes" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:186 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:204 msgid "Every 30 seconds" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:189 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:207 msgid "Every 5 minutes" msgstr "" @@ -371,6 +375,10 @@ msgstr "" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:146 +msgid "Exclude servers by keyword" +msgstr "" + #: src\helpers\copyToClipboard.ts:12 msgid "Failed to copy!" msgstr "" @@ -393,7 +401,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:690 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:708 msgid "Fully Routed IPs" msgstr "" @@ -413,6 +421,10 @@ msgstr "" msgid "HTTP error" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:137 +msgid "Include servers by keyword" +msgstr "" + #: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 msgid "Install extended" msgstr "" @@ -615,6 +627,10 @@ msgstr "" msgid "Issues detected" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:138 +msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." +msgstr "" + #: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:48 msgid "Latest" msgstr "" @@ -623,11 +639,11 @@ msgstr "" msgid "List Update Frequency" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:598 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:616 msgid "Local Domain Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:621 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:639 msgid "Local Subnet Lists" msgstr "" @@ -643,7 +659,7 @@ msgstr "" msgid "Memory Usage" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:730 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:748 msgid "Mixed Proxy Port" msgstr "" @@ -651,7 +667,7 @@ msgstr "" msgid "Monitored Interfaces" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:215 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:233 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -667,7 +683,7 @@ msgstr "" msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:260 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:278 msgid "Network Interface" msgstr "" @@ -749,19 +765,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:385 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:403 msgid "Regional options cannot be used together" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:644 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:662 msgid "Remote Domain Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:667 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:685 msgid "Remote Subnet Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:745 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:763 msgid "Resolve real IP for routing" msgstr "" @@ -809,7 +825,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:404 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:422 msgid "Russia inside restrictions" msgstr "" @@ -821,7 +837,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:352 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:370 msgid "Select a predefined list for routing" msgstr "" @@ -841,11 +857,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:261 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:279 msgid "Select network interface for VPN connection" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:330 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:348 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:25 msgid "Select or enter DNS server address" msgstr "" @@ -858,15 +874,15 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:317 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:335 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:440 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:458 msgid "Select the list type for adding custom domains" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:520 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:538 msgid "Select the list type for adding custom subnets" msgstr "" @@ -890,7 +906,7 @@ msgstr "" msgid "Selector" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:137 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:155 msgid "Selector Proxy Links" msgstr "" @@ -947,20 +963,20 @@ msgstr "" msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:691 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:709 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:645 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:663 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:668 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:686 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:599 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:622 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:617 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:640 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1004,8 +1020,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:444 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:524 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:462 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:542 msgid "Text List" msgstr "" @@ -1013,15 +1029,15 @@ msgstr "" msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:184 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:202 msgid "The interval between connectivity tests" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:198 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:216 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:222 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:240 msgid "The URL used to test server connectivity" msgstr "" @@ -1049,12 +1065,12 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:321 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:339 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:250 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:268 msgid "UDP over TCP" msgstr "" @@ -1089,43 +1105,43 @@ msgstr "" msgid "URLTest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:183 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:201 msgid "URLTest Check Interval" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:160 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:178 msgid "URLTest Proxy Links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:221 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:239 msgid "URLTest Testing URL" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:197 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:215 msgid "URLTest Tolerance" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:439 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:457 msgid "User Domain List Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:451 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:469 msgid "User Domains" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:477 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:495 msgid "User Domains List" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:519 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:537 msgid "User Subnet List Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:531 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:549 msgid "User Subnets" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:557 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:575 msgid "User Subnets List" msgstr "" @@ -1146,8 +1162,8 @@ msgstr "" msgid "Valid" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:510 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:589 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:528 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:607 msgid "Validation errors:" msgstr "" @@ -1161,16 +1177,16 @@ msgid "Visit Wiki" msgstr "" #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:38 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:138 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:161 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:156 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:179 msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:387 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:405 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:406 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:424 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 2b4db66d..3c7cc5c8 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-02 17:15+0300\n" -"PO-Revision-Date: 2026-06-02 17:15+0300\n" +"POT-Creation-Date: 2026-06-04 19:41+0300\n" +"PO-Revision-Date: 2026-06-04 19:41+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -194,6 +194,9 @@ msgstr "Скачивать списки через выбранную секци msgid "Downloading all lists via specific Proxy/VPN" msgstr "Загрузка всех списков через указанный прокси/VPN" +msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." +msgstr "Исключать серверы подписки, имя которых содержит любое из этих ключевых слов (без учёта регистра)." + msgid "Dynamic List" msgstr "Динамический список" @@ -272,6 +275,9 @@ msgstr "Исключить NTP" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "Исключите трафик протокола NTP из туннеля, чтобы предотвратить его маршрутизацию через прокси-сервер или VPN." +msgid "Exclude servers by keyword" +msgstr "Исключать серверы по ключевому слову" + msgid "Failed to copy!" msgstr "Не удалось скопировать!" @@ -296,6 +302,9 @@ msgstr "" msgid "HTTP error" msgstr "Ошибка HTTP" +msgid "Include servers by keyword" +msgstr "Включать серверы по ключевому слову" + msgid "Install extended" msgstr "Установить extended" @@ -446,6 +455,9 @@ msgstr "IP-адрес 0.0.0.0 не допускается" msgid "Issues detected" msgstr "Обнаружены проблемы" +msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." +msgstr "Оставлять только серверы подписки, имя которых содержит хотя бы одно из этих ключевых слов (без учёта регистра). Оставьте пустым, чтобы оставить все." + msgid "Latest" msgstr "Последняя" diff --git a/fe-app-netshift/src/netshift/types.ts b/fe-app-netshift/src/netshift/types.ts index 2994034c..21efd368 100644 --- a/fe-app-netshift/src/netshift/types.ts +++ b/fe-app-netshift/src/netshift/types.ts @@ -120,6 +120,8 @@ export namespace NetShift { subscription_url: string; subscription_update_interval?: string; subscription_group_by_countries?: '0' | '1'; + subscription_filter_include_keywords?: string[]; + subscription_filter_exclude_keywords?: string[]; } export interface ConfigVpnSection { diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index e15eb010..271c6ab6 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -131,6 +131,24 @@ function createSectionContent(section) { o.rmempty = false; o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o = section.option( + form.DynamicList, + "subscription_filter_include_keywords", + _("Include servers by keyword"), + _("Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all."), + ); + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o.rmempty = true; + + o = section.option( + form.DynamicList, + "subscription_filter_exclude_keywords", + _("Exclude servers by keyword"), + _("Drop subscription servers whose name contains any of these keywords (case-insensitive)."), + ); + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o.rmempty = true; + o = section.option( form.DynamicList, "selector_proxy_links", diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 2b4db66d..3c7cc5c8 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-02 17:15+0300\n" -"PO-Revision-Date: 2026-06-02 17:15+0300\n" +"POT-Creation-Date: 2026-06-04 19:41+0300\n" +"PO-Revision-Date: 2026-06-04 19:41+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -194,6 +194,9 @@ msgstr "Скачивать списки через выбранную секци msgid "Downloading all lists via specific Proxy/VPN" msgstr "Загрузка всех списков через указанный прокси/VPN" +msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." +msgstr "Исключать серверы подписки, имя которых содержит любое из этих ключевых слов (без учёта регистра)." + msgid "Dynamic List" msgstr "Динамический список" @@ -272,6 +275,9 @@ msgstr "Исключить NTP" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "Исключите трафик протокола NTP из туннеля, чтобы предотвратить его маршрутизацию через прокси-сервер или VPN." +msgid "Exclude servers by keyword" +msgstr "Исключать серверы по ключевому слову" + msgid "Failed to copy!" msgstr "Не удалось скопировать!" @@ -296,6 +302,9 @@ msgstr "" msgid "HTTP error" msgstr "Ошибка HTTP" +msgid "Include servers by keyword" +msgstr "Включать серверы по ключевому слову" + msgid "Install extended" msgstr "Установить extended" @@ -446,6 +455,9 @@ msgstr "IP-адрес 0.0.0.0 не допускается" msgid "Issues detected" msgstr "Обнаружены проблемы" +msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." +msgstr "Оставлять только серверы подписки, имя которых содержит хотя бы одно из этих ключевых слов (без учёта регистра). Оставьте пустым, чтобы оставить все." + msgid "Latest" msgstr "Последняя" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index 6e8ed43a..5ba7a1b9 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-02 14:15+0300\n" -"PO-Revision-Date: 2026-06-02 14:15+0300\n" +"POT-Creation-Date: 2026-06-04 16:41+0300\n" +"PO-Revision-Date: 2026-06-04 16:41+0300\n" "Last-Translator: yandexru45 <sukadark228@gmail.com>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -52,15 +52,15 @@ msgstr "" msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:251 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:269 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:496 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:514 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:577 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:595 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -127,7 +127,7 @@ msgstr "" msgid "Close" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:351 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:369 msgid "Community Lists" msgstr "" @@ -195,8 +195,8 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:442 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:522 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:460 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:540 msgid "Disabled" msgstr "" @@ -204,17 +204,17 @@ msgstr "" msgid "DNS on router" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:319 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:337 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:320 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:338 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:316 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:334 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:12 msgid "DNS Protocol Type" msgstr "" @@ -223,7 +223,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:329 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:347 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:24 msgid "DNS Server" msgstr "" @@ -236,7 +236,7 @@ msgstr "" msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:306 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:324 msgid "Domain Resolver" msgstr "" @@ -266,8 +266,12 @@ msgstr "" msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:443 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:523 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:147 +msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:461 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:541 msgid "Dynamic List" msgstr "" @@ -275,15 +279,15 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:307 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:325 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:746 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:764 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:717 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:735 msgid "Enable Mixed Proxy" msgstr "" @@ -291,7 +295,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:718 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:736 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -307,15 +311,15 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:478 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:496 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:452 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:470 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:532 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:550 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" @@ -323,7 +327,7 @@ msgstr "" msgid "Enter the subscription URL to fetch proxy configurations from your provider" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:187 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:205 msgid "Every 1 minute" msgstr "" @@ -335,7 +339,7 @@ msgstr "" msgid "Every 3 hours" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:188 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:206 msgid "Every 3 minutes" msgstr "" @@ -343,11 +347,11 @@ msgstr "" msgid "Every 30 minutes" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:186 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:204 msgid "Every 30 seconds" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:189 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:207 msgid "Every 5 minutes" msgstr "" @@ -371,6 +375,10 @@ msgstr "" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:146 +msgid "Exclude servers by keyword" +msgstr "" + #: src\helpers\copyToClipboard.ts:12 msgid "Failed to copy!" msgstr "" @@ -393,7 +401,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:690 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:708 msgid "Fully Routed IPs" msgstr "" @@ -413,6 +421,10 @@ msgstr "" msgid "HTTP error" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:137 +msgid "Include servers by keyword" +msgstr "" + #: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 msgid "Install extended" msgstr "" @@ -615,6 +627,10 @@ msgstr "" msgid "Issues detected" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:138 +msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." +msgstr "" + #: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:48 msgid "Latest" msgstr "" @@ -623,11 +639,11 @@ msgstr "" msgid "List Update Frequency" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:598 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:616 msgid "Local Domain Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:621 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:639 msgid "Local Subnet Lists" msgstr "" @@ -643,7 +659,7 @@ msgstr "" msgid "Memory Usage" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:730 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:748 msgid "Mixed Proxy Port" msgstr "" @@ -651,7 +667,7 @@ msgstr "" msgid "Monitored Interfaces" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:215 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:233 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -667,7 +683,7 @@ msgstr "" msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:260 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:278 msgid "Network Interface" msgstr "" @@ -749,19 +765,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:385 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:403 msgid "Regional options cannot be used together" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:644 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:662 msgid "Remote Domain Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:667 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:685 msgid "Remote Subnet Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:745 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:763 msgid "Resolve real IP for routing" msgstr "" @@ -809,7 +825,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:404 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:422 msgid "Russia inside restrictions" msgstr "" @@ -821,7 +837,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:352 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:370 msgid "Select a predefined list for routing" msgstr "" @@ -841,11 +857,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:261 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:279 msgid "Select network interface for VPN connection" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:330 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:348 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:25 msgid "Select or enter DNS server address" msgstr "" @@ -858,15 +874,15 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:317 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:335 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:440 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:458 msgid "Select the list type for adding custom domains" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:520 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:538 msgid "Select the list type for adding custom subnets" msgstr "" @@ -890,7 +906,7 @@ msgstr "" msgid "Selector" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:137 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:155 msgid "Selector Proxy Links" msgstr "" @@ -947,20 +963,20 @@ msgstr "" msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:691 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:709 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:645 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:663 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:668 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:686 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:599 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:622 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:617 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:640 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1004,8 +1020,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:444 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:524 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:462 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:542 msgid "Text List" msgstr "" @@ -1013,15 +1029,15 @@ msgstr "" msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:184 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:202 msgid "The interval between connectivity tests" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:198 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:216 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:222 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:240 msgid "The URL used to test server connectivity" msgstr "" @@ -1049,12 +1065,12 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:321 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:339 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:250 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:268 msgid "UDP over TCP" msgstr "" @@ -1089,43 +1105,43 @@ msgstr "" msgid "URLTest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:183 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:201 msgid "URLTest Check Interval" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:160 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:178 msgid "URLTest Proxy Links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:221 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:239 msgid "URLTest Testing URL" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:197 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:215 msgid "URLTest Tolerance" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:439 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:457 msgid "User Domain List Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:451 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:469 msgid "User Domains" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:477 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:495 msgid "User Domains List" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:519 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:537 msgid "User Subnet List Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:531 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:549 msgid "User Subnets" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:557 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:575 msgid "User Subnets List" msgstr "" @@ -1146,8 +1162,8 @@ msgstr "" msgid "Valid" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:510 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:589 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:528 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:607 msgid "Validation errors:" msgstr "" @@ -1161,16 +1177,16 @@ msgid "Visit Wiki" msgstr "" #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:38 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:138 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:161 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:156 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:179 msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:387 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:405 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:406 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:424 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" diff --git a/netshift/files/etc/config/netshift b/netshift/files/etc/config/netshift index 53e2ed95..b5cef360 100644 --- a/netshift/files/etc/config/netshift +++ b/netshift/files/etc/config/netshift @@ -48,4 +48,11 @@ config section 'main' # #option urltest_check_interval '3m' # #option urltest_tolerance '50' # #option urltest_testing_url 'https://www.gstatic.com/generate_204' +# # Keyword whitelist: keep only nodes whose display name contains any of +# # these (OR). Empty/absent = keep all. Substring, ASCII case-insensitive. +# #list subscription_filter_include_keywords '🤖' +# #list subscription_filter_include_keywords 'grpc' +# # Keyword blacklist: drop any node whose display name contains any of +# # these (OR). Empty/absent = no exclusion. +# #list subscription_filter_exclude_keywords 'expired' # list community_lists 'russia_inside' diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 1a9f5685..c8f5a55e 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -1536,7 +1536,9 @@ configure_outbound_handler() { local subscription_url subscription_json_path urltest_tag selector_tag \ urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance \ urltest_testing_url subscription_group_by_countries subscription_group_by_countries_raw \ - subscription_outbound_tags_json service_proxy_address subscription_ready + subscription_outbound_tags_json service_proxy_address subscription_ready \ + subscription_filter_include_keywords_json subscription_filter_exclude_keywords_json \ + subscription_keyword_filter_active config_get subscription_url "$section" "subscription_url" config_get urltest_check_interval "$section" "urltest_check_interval" "3m" @@ -1556,6 +1558,16 @@ configure_outbound_handler() { log "Subscription country grouping for section '$section': raw='${subscription_group_by_countries_raw:-<empty>}', enabled=$subscription_group_by_countries" "debug" + # Keyword whitelist/blacklist filtering of subscription nodes by + # display name. Both are optional UCI lists of opaque match strings. + subscription_filter_include_keywords_json="$(build_subscription_filter_keywords_json "$section" "subscription_filter_include_keywords")" + subscription_filter_exclude_keywords_json="$(build_subscription_filter_keywords_json "$section" "subscription_filter_exclude_keywords")" + subscription_keyword_filter_active=0 + if [ "$subscription_filter_include_keywords_json" != "[]" ] || [ "$subscription_filter_exclude_keywords_json" != "[]" ]; then + subscription_keyword_filter_active=1 + log "Subscription keyword filter enabled for section '$section': include=$subscription_filter_include_keywords_json, exclude=$subscription_filter_exclude_keywords_json" "debug" + fi + if [ -z "$subscription_url" ]; then log "Subscription URL is not set. Aborted." "fatal" exit 1 @@ -1611,7 +1623,8 @@ configure_outbound_handler() { subscription_ready=0 if subscription_cache_is_usable "$subscription_json_path"; then - if sing_box_cf_add_subscription_outbounds "$config" "$section" "$subscription_json_path" > /dev/null; then + if sing_box_cf_add_subscription_outbounds "$config" "$section" "$subscription_json_path" \ + "$subscription_filter_include_keywords_json" "$subscription_filter_exclude_keywords_json" > /dev/null; then if [ -n "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then config="$SING_BOX_CF_LAST_CONFIG" subscription_ready=1 @@ -1620,6 +1633,10 @@ configure_outbound_handler() { fi if [ "$subscription_ready" -eq 0 ]; then + # When the keyword filter empties the set, the precise cause is + # already logged facade-side (only on the cache-usable branch + # where add_subscription_outbounds actually ran the filter), so we + # do not re-warn here to avoid misattribution / double-warning. log "Subscription cache for section '$section' is unavailable or empty; using a temporary blocked outbound" "warn" mark_subscription_outbound_unavailable "$section" else @@ -1912,6 +1929,37 @@ exclude_source_ip_from_routing_handler() { config=$(sing_box_cm_patch_route_rule "$config" "$rule_tag" "source_ip_cidr" "$source_ip") } +# config_list_foreach callback that appends a raw keyword (opaque user text: +# may contain spaces, emoji, Cyrillic, &?#%) onto the JSON array carried in the +# global SUBSCRIPTION_FILTER_KEYWORDS_JSON. Items are appended verbatim; the +# facade's jq filter later drops empty ("") items via select(length > 0). A +# whitespace-only item is NOT trimmed and stays a literal match string +# (intentional opaque match semantics per the spec). Uses jq --arg so every byte +# survives intact (no URL/query parsing). +append_subscription_filter_keyword_handler() { + local keyword="$1" + local next + + next=$(printf '%s' "$SUBSCRIPTION_FILTER_KEYWORDS_JSON" | + jq -c --arg kw "$keyword" '. + [$kw]' 2>/dev/null) + [ -n "$next" ] && SUBSCRIPTION_FILTER_KEYWORDS_JSON="$next" +} + +# Read a per-section UCI list of keyword filter values into a JSON array string. +# Arguments: +# section: string, the UCI section name +# list_name: string, the UCI list option name +# Outputs: +# Writes the JSON array (e.g. ["grpc","\ud83e\udd16"]) to stdout; "[]" if empty. +build_subscription_filter_keywords_json() { + local section="$1" + local list_name="$2" + + SUBSCRIPTION_FILTER_KEYWORDS_JSON="[]" + config_list_foreach "$section" "$list_name" append_subscription_filter_keyword_handler + printf '%s' "$SUBSCRIPTION_FILTER_KEYWORDS_JSON" +} + configure_routing_for_section_lists() { local section="$1" diff --git a/netshift/files/usr/lib/sing_box_config_facade.sh b/netshift/files/usr/lib/sing_box_config_facade.sh index 06d74979..e35c03f0 100644 --- a/netshift/files/usr/lib/sing_box_config_facade.sh +++ b/netshift/files/usr/lib/sing_box_config_facade.sh @@ -365,6 +365,10 @@ sing_box_cf_add_single_key_reject_rule() { # Arguments: # config: string (JSON), sing-box configuration the batch will be merged into # subscription_json_path: string, path to the downloaded subscription JSON file +# include_keywords_json: string (JSON array), keep only nodes whose display name +# contains at least one of these (OR). Empty array ([]) = keep all. +# exclude_keywords_json: string (JSON array), drop any node whose display name +# contains at least one of these (OR). Empty array ([]) = no exclusion. # Outputs: # Writes a JSON object to stdout: # { outbounds: [ {type,...,tag} ... ], tags: [..], names: [..], @@ -373,8 +377,13 @@ sing_box_cf_add_single_key_reject_rule() { sing_box_cf_prepare_subscription_batch() { local config="$1" local subscription_json_path="$2" + local include_keywords_json="${3:-[]}" + local exclude_keywords_json="${4:-[]}" local sing_box_extended="false" + [ -n "$include_keywords_json" ] || include_keywords_json="[]" + [ -n "$exclude_keywords_json" ] || exclude_keywords_json="[]" + if is_sing_box_extended; then sing_box_extended="true" fi @@ -383,7 +392,21 @@ sing_box_cf_prepare_subscription_batch() { # the subscription JSON is slurped from its file path. printf '%s' "$config" | jq -c \ --slurpfile sub "$subscription_json_path" \ - --argjson extended "$sing_box_extended" ' + --argjson extended "$sing_box_extended" \ + --argjson include_keywords "$include_keywords_json" \ + --argjson exclude_keywords "$exclude_keywords_json" ' + # Normalise the keyword lists: drop empty items and precompute the + # ASCII-lowercased form once. ascii_downcase only touches ASCII, so + # emoji/Cyrillic keywords are matched as exact byte-substrings. + # NB: "include"/"exclude" are reserved jq keywords, hence $inc/$exc. + ([$include_keywords[]? | tostring | select(length > 0) | ascii_downcase]) as $inc + | ([$exclude_keywords[]? | tostring | select(length > 0) | ascii_downcase]) as $exc + # A node "matches" a normalised keyword list when its lowercased name + # contains any of the keywords (substring via index, NO regex/Oniguruma). + # Bind each keyword to $kw so index() receives the keyword, not the name. + | def name_passes_keywords($lc): + (($inc | length) == 0 or any($inc[]; . as $kw | ($lc | index($kw)) != null)) + and (($exc | length) == 0 or all($exc[]; . as $kw | ($lc | index($kw)) == null)); # Reserved tags already used by the working config (stdin is the config). ([.outbounds[]?.tag // empty]) as $existing # Candidate proxy outbounds from the subscription (preserve order). @@ -393,7 +416,16 @@ sing_box_cf_prepare_subscription_batch() { .type != "direct" and .type != "dns" and .type != "block" - )] as $candidates + )] as $all_candidates + # Keyword whitelist/blacklist filter on the display name. Runs BEFORE the + # static-unsupported filter and tag dedup, so dropped nodes never get + # tags and never reach sing-box check. Covers native + fallback-parsed + # subscriptions (both consume this batch). + | [$all_candidates[] + | . as $ob + | (($ob.remark // $ob.tag // "") | tostring) as $name + | select(name_passes_keywords($name | ascii_downcase)) + ] as $candidates | ($candidates | length) as $total # Statically reject outbounds the current sing-box build cannot load. | [ $candidates[] @@ -548,6 +580,10 @@ sing_box_cf_apply_subscription_range() { # config: string (JSON), sing-box configuration to modify # section: string, the UCI section name # subscription_json_path: string, path to the downloaded subscription JSON file +# include_keywords_json: string (JSON array, optional), keyword whitelist (OR); +# empty/[] keeps all nodes. Forwarded to the prepare batch. +# exclude_keywords_json: string (JSON array, optional), keyword blacklist (OR); +# empty/[] excludes nothing. Forwarded to the prepare batch. # Outputs: # Writes updated JSON configuration to stdout # Sets global variable SUBSCRIPTION_OUTBOUND_TAGS (comma-separated list of tags) @@ -558,6 +594,11 @@ sing_box_cf_add_subscription_outbounds() { local config="$1" local section="$2" local subscription_json_path="$3" + local include_keywords_json="${4:-[]}" + local exclude_keywords_json="${5:-[]}" + + [ -n "$include_keywords_json" ] || include_keywords_json="[]" + [ -n "$exclude_keywords_json" ] || exclude_keywords_json="[]" SUBSCRIPTION_OUTBOUND_TAGS="" SUBSCRIPTION_OUTBOUND_TAGS_JSON="[]" @@ -570,9 +611,17 @@ sing_box_cf_add_subscription_outbounds() { return 1 fi - # Build the entire batch (filter + dedup tags) in one jq pass. + # Whether keyword filtering is active (for distinct empty-result logging). + local keyword_filter_active=0 + if [ "$include_keywords_json" != "[]" ] || [ "$exclude_keywords_json" != "[]" ]; then + keyword_filter_active=1 + fi + + # Build the entire batch (keyword filter + static filter + dedup tags) in one + # jq pass. local prepared - prepared=$(sing_box_cf_prepare_subscription_batch "$config" "$subscription_json_path") + prepared=$(sing_box_cf_prepare_subscription_batch "$config" "$subscription_json_path" \ + "$include_keywords_json" "$exclude_keywords_json") if [ -z "$prepared" ]; then log "Failed to parse subscription outbounds JSON" "error" echo "$config" @@ -584,7 +633,23 @@ sing_box_cf_add_subscription_outbounds() { kept_count=$(printf '%s' "$prepared" | jq -r '.count // 0' 2>/dev/null) statically_skipped=$(printf '%s' "$prepared" | jq -r '.skipped // 0' 2>/dev/null) + if [ "$keyword_filter_active" -eq 1 ]; then + # candidate_total here is the post-keyword-filter candidate count; report + # kept vs. filtered_out so an over-strict filter is diagnosable. + local raw_candidate_total filtered_out + raw_candidate_total=$(printf '%s' "$config" | jq -c \ + --slurpfile sub "$subscription_json_path" \ + '[$sub[0].outbounds[]? | select(.type != "selector" and .type != "urltest" and .type != "direct" and .type != "dns" and .type != "block")] | length' 2>/dev/null) + [ -n "$raw_candidate_total" ] || raw_candidate_total=0 + filtered_out=$((raw_candidate_total - candidate_total)) + [ "$filtered_out" -ge 0 ] || filtered_out=0 + log "Subscription keyword filter for section '$section': kept=$candidate_total, filtered_out=$filtered_out" "info" + fi + if [ -z "$candidate_total" ] || [ "$candidate_total" -eq 0 ]; then + if [ "$keyword_filter_active" -eq 1 ]; then + log "Subscription keyword filter for section '$section' removed all nodes; using a temporary blocked outbound" "warn" + fi log "No proxy outbounds found in subscription JSON" "error" echo "$config" return 1 diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index e7fa54b6..09c263a8 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1043,6 +1043,103 @@ else echo "fb-caseI-configured-only(got '$caseI_conf' lines=$caseI_conf_lines):FAIL" fi +# ── CASE J: subscription keyword whitelist/blacklist filter ───────── +# Drive sing_box_cf_prepare_subscription_batch directly with a synthetic +# subscription JSON and assert kept counts/names for include/exclude lists. +# Matching: substring, OR across keywords, ASCII case-insensitive, byte-exact +# for non-ASCII (emoji/Cyrillic). No jq regex (index + ascii_downcase only). +caseJ_cfg='{"outbounds":[]}' +caseJ_sub="/tmp/netshift-fb-caseJ-$$.json" +cat > "$caseJ_sub" << 'JSUB' +{ + "outbounds": [ + {"type": "shadowsocks", "tag": "US grpc", "server": "a.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"}, + {"type": "shadowsocks", "tag": "US ws", "server": "b.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"}, + {"type": "shadowsocks", "tag": "DE grpc", "server": "c.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"} + ] +} +JSUB + +# Helper: emit the JSON `count` for given include/exclude arrays. +caseJ_count() { + sing_box_cf_prepare_subscription_batch "$caseJ_cfg" "$caseJ_sub" "$1" "$2" | + jq -r '.count // -1' +} +# Helper: emit a comma-joined sorted names list for given include/exclude arrays. +caseJ_names() { + sing_box_cf_prepare_subscription_batch "$caseJ_cfg" "$caseJ_sub" "$1" "$2" | + jq -r '(.names // []) | sort | join(",")' +} + +# (1) include-only: ["grpc"] keeps exactly the 2 grpc nodes. +caseJ_inc_count="$(caseJ_count '["grpc"]' '[]')" +caseJ_inc_names="$(caseJ_names '["grpc"]' '[]')" +if [ "$caseJ_inc_count" = "2" ] && [ "$caseJ_inc_names" = "DE grpc,US grpc" ]; then + echo 'fb-caseJ-include-only:OK' +else + echo "fb-caseJ-include-only(count=$caseJ_inc_count names='$caseJ_inc_names'):FAIL" +fi + +# (2) exclude-only: ["ws"] drops the ws node, keeps the other 2. +caseJ_exc_count="$(caseJ_count '[]' '["ws"]')" +caseJ_exc_names="$(caseJ_names '[]' '["ws"]')" +if [ "$caseJ_exc_count" = "2" ] && [ "$caseJ_exc_names" = "DE grpc,US grpc" ]; then + echo 'fb-caseJ-exclude-only:OK' +else + echo "fb-caseJ-exclude-only(count=$caseJ_exc_count names='$caseJ_exc_names'):FAIL" +fi + +# (3) include + exclude OR: include=["US"], exclude=["ws"] => "US grpc" only. +caseJ_both_count="$(caseJ_count '["US"]' '["ws"]')" +caseJ_both_names="$(caseJ_names '["US"]' '["ws"]')" +if [ "$caseJ_both_count" = "1" ] && [ "$caseJ_both_names" = "US grpc" ]; then + echo 'fb-caseJ-include-exclude:OK' +else + echo "fb-caseJ-include-exclude(count=$caseJ_both_count names='$caseJ_both_names'):FAIL" +fi + +# (4) case-insensitive ASCII: include=["GRPC"] matches "US grpc"/"DE grpc". +caseJ_ci_count="$(caseJ_count '["GRPC"]' '[]')" +if [ "$caseJ_ci_count" = "2" ]; then + echo 'fb-caseJ-ascii-ci:OK' +else + echo "fb-caseJ-ascii-ci(count=$caseJ_ci_count):FAIL" +fi + +# (5) emoji/unicode substring: a robot-emoji node kept, a plain node dropped. +caseJ_emoji_sub="/tmp/netshift-fb-caseJ-emoji-$$.json" +cat > "$caseJ_emoji_sub" << 'JEMOJI' +{ + "outbounds": [ + {"type": "shadowsocks", "tag": "🤖 Gemini", "server": "a.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"}, + {"type": "shadowsocks", "tag": "Plain Node", "server": "b.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"} + ] +} +JEMOJI +caseJ_emoji_count="$(sing_box_cf_prepare_subscription_batch "$caseJ_cfg" "$caseJ_emoji_sub" '["🤖"]' '[]' | jq -r '.count // -1')" +caseJ_emoji_names="$(sing_box_cf_prepare_subscription_batch "$caseJ_cfg" "$caseJ_emoji_sub" '["🤖"]' '[]' | jq -r '(.names // []) | join(",")')" +if [ "$caseJ_emoji_count" = "1" ] && [ "$caseJ_emoji_names" = "🤖 Gemini" ]; then + echo 'fb-caseJ-emoji-substring:OK' +else + echo "fb-caseJ-emoji-substring(count=$caseJ_emoji_count names='$caseJ_emoji_names'):FAIL" +fi +rm -f "$caseJ_emoji_sub" + +# (6) empty include keeps all; over-strict filter removes everything (count 0). +caseJ_all_count="$(caseJ_count '[]' '[]')" +if [ "$caseJ_all_count" = "3" ]; then + echo 'fb-caseJ-empty-include-keeps-all:OK' +else + echo "fb-caseJ-empty-include-keeps-all(count=$caseJ_all_count):FAIL" +fi +caseJ_none_count="$(caseJ_count '["nomatch-zzz"]' '[]')" +if [ "$caseJ_none_count" = "0" ]; then + echo 'fb-caseJ-filter-removes-all-zero-kept:OK' +else + echo "fb-caseJ-filter-removes-all-zero-kept(count=$caseJ_none_count):FAIL" +fi +rm -f "$caseJ_sub" + echo 'DONE' FBEOF From 7d6b6f81ab811a2c23d033d895c36812b75de7af Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Thu, 4 Jun 2026 20:35:21 +0300 Subject: [PATCH 43/75] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=B8=20=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D0=BA=D0=BE=D0=B7=D1=8F=D0=B1=D1=80=20=D0=B2=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/agent-rules/backend-shell.md | 3 +- docs/agent-rules/memory/code-reviewer.md | 7 +- .../memory/shell-backend-developer.md | 28 ++- netshift/files/usr/bin/netshift | 228 +++++++++--------- tests/entrypoint.sh | 29 +++ 5 files changed, 175 insertions(+), 120 deletions(-) diff --git a/docs/agent-rules/backend-shell.md b/docs/agent-rules/backend-shell.md index 8663dc35..5670b858 100644 --- a/docs/agent-rules/backend-shell.md +++ b/docs/agent-rules/backend-shell.md @@ -93,7 +93,8 @@ This pattern (`... Aborted." "fatal"; exit 1`) appears throughout the CLI; prese - busybox `sed` lacks `\x` hex escapes. Build literal bytes with `printf` octal escapes (e.g. the UTF-8 BOM `printf '\357\273\277'` in `normalize_subscription_to_singbox`). - Convert CRLF→LF with `convert_crlf_to_lf` before parsing downloaded lists. - Strip a leading UTF-8 BOM before base64 charset detection. -- Some diagnostic strings contain **intentional mojibake** (CP1251-encoded emoji / box-drawing in `list_update`, `subscription_update`, `global_check`, `check_nft`). These render correctly on the target/LuCI. **Preserve the existing byte sequences verbatim** when editing those lines — do not "fix" or re-encode them. +- The diagnostic strings in `usr/bin/netshift` (emoji / box-drawing in `list_update`, `subscription_update`, `global_check`, `check_nft`, e.g. `📡 🛠️ ✅ ❌ ⚠️ ➡️ 🧱 🥸 📄` and `━` separators) are **valid UTF-8** and must stay valid UTF-8 — they render correctly on the device (SSH/UTF-8 terminal) and LuCI. They are **not** intentional mojibake. +- These were once corrupted by a UTF-8→CP1251 double-encode (real UTF-8 bytes read as CP1251 and re-saved as UTF-8), which made them print as `рџ…`/`в”…` garbage; task-004 repaired them. **Never open/save `usr/bin/netshift` in a non-UTF-8 editor or run it through a CP1251 codepage** — doing so reintroduces the `рџ…`/`в”…`/` ` mojibake. Edit it as UTF-8 only. ## 9. New constants diff --git a/docs/agent-rules/memory/code-reviewer.md b/docs/agent-rules/memory/code-reviewer.md index 44a03306..cac99aa7 100644 --- a/docs/agent-rules/memory/code-reviewer.md +++ b/docs/agent-rules/memory/code-reviewer.md @@ -48,4 +48,9 @@ append recurring findings; keep under ~200 lines. - Routing code that ignores `subscription_outbound_is_unavailable` (traffic leak when a subscription is down). - Scope creep: unrelated file churn (e.g. lockfile churn) flagged as Minor. -- Corrupted mojibake bytes in diagnostic strings (should be byte-preserved). +- Diagnostic strings in `usr/bin/netshift` are valid UTF-8 emoji/box-drawing — + must stay UTF-8, never CP1251 (task-004 fixed a double-encode). For + mojibake-repair reviews, prove ASCII-byte preservation byte-safely (Python: + decode HEAD blob vs working tree, strip `[^\x00-\x7F]` per line, expect 0 + mismatched lines); beware PowerShell text pipelines which produce false UTF-16 + diffs. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 224937dd..e308c7b4 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -14,10 +14,14 @@ findings; keep under ~200 lines. half-built config. - **busybox sed lacks `\x` escapes** — use printf-octal workarounds (see `helpers.sh` `convert_crlf_to_lf` and BOM stripping). Don't assume GNU sed. -- **Intentional mojibake**: some diagnostic strings (list_update / - subscription_update / global_check) store emoji/box-drawing as corrupted - CP1251-ish bytes (e.g. `рџ”„`, `вњ…`, `в”Ѓ`). Preserve the exact existing bytes - when editing those lines or rendered output changes. +- **Diagnostic strings are UTF-8, NOT mojibake** (corrected by task-004). The + emoji/box-drawing in `usr/bin/netshift` (`global_check`, `list_update`, + `subscription_update`, `check_nft`: `📡 🛠️ ✅ ❌ ⚠️ ➡️ 🧱 🥸 📄 ━`) are valid + UTF-8 and must STAY valid UTF-8. They were once double-encoded (UTF-8 read as + CP1251, re-saved as UTF-8 → printed `рџ…`/`в”…`/` `). Never open/save that file + in a non-UTF-8 editor or pass it through CP1251 — it re-corrupts. The earlier + "preserve the corrupted bytes" note here was the WRONG guidance that protected + the bug. ## Conventions (follow exactly) @@ -83,3 +87,19 @@ findings; keep under ~200 lines. - VPN `domain_resolver` uses wrong variable `$dns_server`. - `check_nft` references stale set names (`netshift_domains`) / UCI options that don't exist elsewhere — likely copied diagnostic cruft. + +## task-004: double-encode repair recipe (reusable) + +- To reverse a UTF-8→CP1251 double-encode losslessly: `text = + bytes.decode("utf-8"); fixed = text.encode("cp1251").decode("utf-8")` then + write `fixed.encode("utf-8")`. ASCII bytes pass through; verify 0 + cp1251-unmappable chars and that ASCII-stripped lines are byte-identical + before/after (proves no code moved). Result was exactly 114 lines, all + non-ASCII-only. LF/no-BOM preserved. +- On Windows here, `python3.exe` is the MS Store stub — use `python` (Python + 3.11 at `...\Programs\Python\Python311`). Don't `print()` emoji to the + PowerShell console (cp1251 codepage mangles it / raises); write results to a + UTF-8 file and read it back. +- `test_syntax` in `tests/entrypoint.sh` now also `ash -n`'s `usr/bin/netshift` + and asserts no residual `рџ`/`в”`/`вЂ` markers (built via `printf` octal, since + busybox grep lacks `\x`). Guards against re-introducing the mojibake. diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index c8f5a55e..ac16aecf 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -1080,7 +1080,7 @@ ensure_nft_ready_for_list_update() { list_update() { - echolog "рџ”„ Starting lists update..." + echolog "🔄 Starting lists update..." local nslookup_timeout=3 local nslookup_attempts=10 @@ -1093,7 +1093,7 @@ list_update() { # DNS Check for i in $(seq 1 $nslookup_attempts); do if nslookup -timeout=$nslookup_timeout openwrt.org > /dev/null 2>&1; then - echolog "вњ… DNS check passed" + echolog "✅ DNS check passed" break fi echolog "DNS is unavailable [$i/$nslookup_attempts]" @@ -1101,7 +1101,7 @@ list_update() { done if [ "$i" -eq $nslookup_attempts ]; then - echolog "вќЊ DNS check failed after $nslookup_attempts attempts" + echolog "❌ DNS check failed after $nslookup_attempts attempts" return 1 fi @@ -1112,12 +1112,12 @@ list_update() { if [ -n "$service_proxy_address" ]; then if curl -s -x "http://$service_proxy_address" -m $curl_timeout https://github.com > /dev/null; then - echolog "вњ… GitHub connection check passed (via proxy)" + echolog "✅ GitHub connection check passed (via proxy)" break fi else if curl -s -m $curl_timeout https://github.com > /dev/null; then - echolog "вњ… GitHub connection check passed" + echolog "✅ GitHub connection check passed" break fi fi @@ -1130,16 +1130,16 @@ list_update() { done if [ "$i" -eq $curl_attempts ]; then - echolog "вќЊ GitHub connection check failed after $curl_attempts attempts" + echolog "❌ GitHub connection check failed after $curl_attempts attempts" return 1 fi if ! ensure_nft_ready_for_list_update; then - echolog "вќЊ NFT table is unavailable, cannot update lists" + echolog "❌ NFT table is unavailable, cannot update lists" return 1 fi - echolog "рџ“Ґ Downloading and processing lists..." + echolog "📥 Downloading and processing lists..." local update_failed=0 config_foreach import_community_subnet_lists "section" || update_failed=1 @@ -1147,15 +1147,15 @@ list_update() { config_foreach import_subnets_from_remote_subnet_lists "section" || update_failed=1 if [ "$update_failed" -eq 0 ]; then - echolog "вњ… Lists update completed successfully" + echolog "✅ Lists update completed successfully" else - echolog "вќЊ Lists update failed" + echolog "❌ Lists update failed" return 1 fi } subscription_update() { - echolog "рџ”„ Starting subscription update..." + echolog "🔄 Starting subscription update..." local has_subscription=0 local updated_sections=0 @@ -1178,7 +1178,7 @@ subscription_update() { config_foreach _check_subscription_section "section" if [ "$has_subscription" -eq 0 ]; then - echolog "в„№пёЏ No subscription sections found, nothing to update" + echolog "ℹ️ No subscription sections found, nothing to update" return 0 fi @@ -1200,21 +1200,21 @@ subscription_update() { config_get subscription_url "$section" "subscription_url" if [ -z "$subscription_url" ]; then - echolog "вќЊ Subscription URL not set for section '$section'" + echolog "❌ Subscription URL not set for section '$section'" failed_sections=$((failed_sections + 1)) return fi mkdir -p "$TMP_SUBSCRIPTION_FOLDER" if ! ensure_subscription_cache_dir; then - echolog "вќЊ Subscription cache directory is unavailable for section '$section'" + echolog "❌ Subscription cache directory is unavailable for section '$section'" failed_sections=$((failed_sections + 1)) return fi subscription_json_path="$(get_subscription_json_path "$section")" subscription_url_cache_path="$(get_subscription_url_cache_path "$section")" - echolog "рџ“Ґ Updating subscription for section '$section'..." + echolog "📥 Updating subscription for section '$section'..." service_proxy_address="$(get_subscription_download_proxy_address "$section" "runtime" || echo '')" if [ -n "$service_proxy_address" ]; then @@ -1224,7 +1224,7 @@ subscription_update() { fi if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 6 5 5; then - echolog "вќЊ Subscription source is not reachable for section '$section'" + echolog "❌ Subscription source is not reachable for section '$section'" failed_sections=$((failed_sections + 1)) return fi @@ -1244,32 +1244,32 @@ subscription_update() { .type != "block" )] | length' "$subscription_json_path" 2>/dev/null) - echolog "вњ… Subscription updated for section '$section': $outbounds_count outbounds" + echolog "✅ Subscription updated for section '$section': $outbounds_count outbounds" ;; 2) - echolog "в„№пёЏ Subscription for section '$section' is unchanged" + echolog "ℹ️ Subscription for section '$section' is unchanged" return ;; 10) - echolog "вќЊ Failed to prepare subscription cache for section '$section'" + echolog "❌ Failed to prepare subscription cache for section '$section'" ;; 11) - echolog "вќЊ Failed to create temporary subscription download file for section '$section'" + echolog "❌ Failed to create temporary subscription download file for section '$section'" ;; 12) - echolog "вќЊ Failed to download subscription body for section '$section'" + echolog "❌ Failed to download subscription body for section '$section'" ;; 13) - echolog "вќЊ Downloaded subscription for section '$section' is invalid" + echolog "❌ Downloaded subscription for section '$section' is invalid" ;; 14) - echolog "вќЊ Downloaded subscription for section '$section' is unchanged and still has no usable outbounds" + echolog "❌ Downloaded subscription for section '$section' is unchanged and still has no usable outbounds" ;; 15) - echolog "вќЊ Failed to persist subscription cache for section '$section'" + echolog "❌ Failed to persist subscription cache for section '$section'" ;; *) - echolog "вќЊ Failed to update subscription for section '$section': internal error rc=$update_result" + echolog "❌ Failed to update subscription for section '$section': internal error rc=$update_result" ;; esac @@ -1282,26 +1282,26 @@ subscription_update() { if [ "$updated_sections" -eq 0 ]; then if [ "$failed_sections" -gt 0 ]; then - echolog "вќЊ Subscription update finished with errors; keeping the last working cache" + echolog "❌ Subscription update finished with errors; keeping the last working cache" return 1 fi - echolog "в„№пёЏ Subscription update completed: no changes detected" + echolog "ℹ️ Subscription update completed: no changes detected" return 0 fi - echolog "рџ”„ Restarting netshift to apply updated subscriptions..." + echolog "🔄 Restarting netshift to apply updated subscriptions..." restart restart_rc=$? if [ "$restart_rc" -ne 0 ]; then - echolog "вќЊ Subscription was downloaded, but netshift restart failed" + echolog "❌ Subscription was downloaded, but netshift restart failed" return "$restart_rc" fi if [ "$failed_sections" -gt 0 ]; then - echolog "вњ… Subscription update applied for changed sections; failed sections kept their previous cache" + echolog "✅ Subscription update applied for changed sections; failed sections kept their previous cache" else - echolog "вњ… Subscription update completed" + echolog "✅ Subscription update completed" fi } @@ -1757,10 +1757,10 @@ configure_outbound_handler() { config=$(sing_box_cm_add_interface_outbound "$config" "$outbound_tag" "$interface_name" "$domain_resolver_tag") ;; block) - log "Connection type 'block' detected for the $section section – no outbound will be created (handled via reject route rules)" + log "Connection type 'block' detected for the $section section – no outbound will be created (handled via reject route rules)" ;; exclusion) - log "Connection type 'exclusion' detected for the $section section – no outbound will be created (handled via route rules)" + log "Connection type 'exclusion' detected for the $section section – no outbound will be created (handled via route rules)" ;; *) log "Unknown connection type '$connection_type' for the $section section. Aborted." "fatal" @@ -2791,7 +2791,7 @@ check_nft() { # Check if table exists if ! nft list table inet "$NFT_TABLE_NAME" > /dev/null 2>&1; then - nolog "вќЊ $NFT_TABLE_NAME not found" + nolog "❌ $NFT_TABLE_NAME not found" return 1 fi @@ -3445,9 +3445,9 @@ global_check() { local NETSHIFT_LUCI_VERSION="Unknown" [ -n "$1" ] && NETSHIFT_LUCI_VERSION="$1" - print_global "рџ“Ў Global check run!" - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "рџ› пёЏ System info" + print_global "📡 Global check run!" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "🛠️ System info" local system_info_json system_info_json=$(get_system_info) @@ -3462,17 +3462,17 @@ global_check() { openwrt_version=$(echo "$system_info_json" | jq -r '.openwrt_version // "unknown"') device_model=$(echo "$system_info_json" | jq -r '.device_model // "unknown"') - print_global "рџ•іпёЏ NetShift: $netshift_version (latest: $netshift_latest_version)" - print_global "рџ•іпёЏ LuCI App: $luci_app_version" - print_global "📦 Sing-box: $sing_box_version" - print_global "рџ›њ OpenWrt: $openwrt_version" - print_global "рџ›њ Device: $device_model" + print_global "🕳️ NetShift: $netshift_version (latest: $netshift_latest_version)" + print_global "🕳️ LuCI App: $luci_app_version" + print_global "📦 Sing-box: $sing_box_version" + print_global "🛜 OpenWrt: $openwrt_version" + print_global "🛜 Device: $device_model" else - print_global "вќЊ Failed to get system info" + print_global "❌ Failed to get system info" fi - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "вћЎпёЏ DNS status" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "➡️ DNS status" local dns_check_json dns_check_json=$(check_dns_available) @@ -3491,24 +3491,24 @@ global_check() { # Bootstrap DNS if [ -n "$bootstrap_dns_server" ]; then if [ "$bootstrap_dns_status" -eq 1 ]; then - print_global "вњ… Bootstrap DNS: $bootstrap_dns_server" + print_global "✅ Bootstrap DNS: $bootstrap_dns_server" else - print_global "вќЊ Bootstrap DNS: $bootstrap_dns_server" + print_global "❌ Bootstrap DNS: $bootstrap_dns_server" fi fi # DNS server status if [ "$dns_status" -eq 1 ]; then - print_global "вњ… Main DNS: $dns_server [$dns_type]" + print_global "✅ Main DNS: $dns_server [$dns_type]" else - print_global "вќЊ Main DNS: $dns_server [$dns_type]" + print_global "❌ Main DNS: $dns_server [$dns_type]" fi # DNS on router if [ "$dns_on_router" -eq 1 ]; then - print_global "вњ… DNS on router" + print_global "✅ DNS on router" else - print_global "вќЊ DNS on router" + print_global "❌ DNS on router" fi # DHCP configuration check @@ -3516,20 +3516,20 @@ global_check() { config_get dont_touch_dhcp "settings" "dont_touch_dhcp" if [ "$dont_touch_dhcp" = "1" ]; then - print_global "вљ пёЏ dont_touch_dhcp is enabled. рџ“„ DHCP config:" + print_global "⚠️ dont_touch_dhcp is enabled. 📄 DHCP config:" awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp elif [ "$dhcp_config_status" -eq 0 ]; then - print_global "вќЊ DHCP configuration differs from template. рџ“„ DHCP config:" + print_global "❌ DHCP configuration differs from template. 📄 DHCP config:" awk '/^config /{p=($2=="dnsmasq")} p' /etc/config/dhcp else - print_global "вњ… /etc/config/dhcp" + print_global "✅ /etc/config/dhcp" fi else - print_global "вќЊ Failed to get DNS info" + print_global "❌ Failed to get DNS info" fi - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "📦 Sing-box status" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "📦 Sing-box status" local singbox_check_json singbox_check_json=$(check_sing_box) @@ -3545,46 +3545,46 @@ global_check() { sing_box_ports_listening=$(echo "$singbox_check_json" | jq -r '.sing_box_ports_listening // 0') if [ "$sing_box_installed" -eq 1 ]; then - print_global "вњ… Sing-box installed" + print_global "✅ Sing-box installed" else - print_global "вќЊ Sing-box installed" + print_global "❌ Sing-box installed" fi if [ "$sing_box_version_ok" -eq 1 ]; then - print_global "вњ… Sing-box version is compatible (newer than 1.12.4)" + print_global "✅ Sing-box version is compatible (newer than 1.12.4)" else - print_global "вќЊ Sing-box version is not compatible (older than 1.12.4)" + print_global "❌ Sing-box version is not compatible (older than 1.12.4)" fi if [ "$sing_box_service_exist" -eq 1 ]; then - print_global "вњ… Sing-box service exist" + print_global "✅ Sing-box service exist" else - print_global "вќЊ Sing-box service exist" + print_global "❌ Sing-box service exist" fi if [ "$sing_box_autostart_disabled" -eq 1 ]; then - print_global "вњ… Sing-box autostart disabled" + print_global "✅ Sing-box autostart disabled" else - print_global "вќЊ Sing-box autostart disabled" + print_global "❌ Sing-box autostart disabled" fi if [ "$sing_box_process_running" -eq 1 ]; then - print_global "вњ… Sing-box process running" + print_global "✅ Sing-box process running" else - print_global "вќЊ Sing-box process running" + print_global "❌ Sing-box process running" fi if [ "$sing_box_ports_listening" -eq 1 ]; then - print_global "вњ… Sing-box listening ports" + print_global "✅ Sing-box listening ports" else - print_global "вќЊ Sing-box listening ports" + print_global "❌ Sing-box listening ports" fi else - print_global "вќЊ Failed to get sing-box info" + print_global "❌ Failed to get sing-box info" fi - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "рџ§± NFT rules status" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "🧱 NFT rules status" local nft_check_json nft_check_json=$(check_nft_rules) @@ -3602,78 +3602,78 @@ global_check() { rules_other_mark_exist=$(echo "$nft_check_json" | jq -r '.rules_other_mark_exist // 0') if [ "$table_exist" -eq 1 ]; then - print_global "вњ… Table exist" + print_global "✅ Table exist" else - print_global "вќЊ Table exist" + print_global "❌ Table exist" fi if [ "$rules_mangle_exist" -eq 1 ]; then - print_global "вњ… Rules mangle exist" + print_global "✅ Rules mangle exist" else - print_global "вќЊ Rules mangle exist" + print_global "❌ Rules mangle exist" fi if [ "$rules_mangle_counters" -eq 1 ]; then - print_global "вњ… Rules mangle counters" + print_global "✅ Rules mangle counters" else - print_global "вљ пёЏ Rules mangle counters" + print_global "⚠️ Rules mangle counters" fi if [ "$rules_mangle_output_exist" -eq 1 ]; then - print_global "вњ… Rules mangle output exist" + print_global "✅ Rules mangle output exist" else - print_global "вќЊ Rules mangle output exist" + print_global "❌ Rules mangle output exist" fi if [ "$rules_mangle_output_counters" -eq 1 ]; then - print_global "вњ… Rules mangle output counters" + print_global "✅ Rules mangle output counters" else - print_global "вљ пёЏ Rules mangle output counters" + print_global "⚠️ Rules mangle output counters" fi if [ "$rules_proxy_exist" -eq 1 ]; then - print_global "вњ… Rules proxy exist" + print_global "✅ Rules proxy exist" else - print_global "вќЊ Rules proxy exist" + print_global "❌ Rules proxy exist" fi if [ "$rules_proxy_counters" -eq 1 ]; then - print_global "вњ… Rules proxy counters" + print_global "✅ Rules proxy counters" else - print_global "вљ пёЏ Rules proxy counters" + print_global "⚠️ Rules proxy counters" fi if [ "$rules_other_mark_exist" -eq 1 ]; then - print_global "вљ пёЏ Additional marking rules found:" + print_global "⚠️ Additional marking rules found:" nft list ruleset | awk '/table inet '"$NFT_TABLE_NAME"'/{flag=1; next} /^table/{flag=0} !flag' | grep -E "mark set|meta mark" else - print_global "вњ… Additional marking rules found" + print_global "✅ Additional marking rules found" fi else - print_global "вќЊ Failed to get NFT rules info" + print_global "❌ Failed to get NFT rules info" fi - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "рџ“„ NetShift config" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "📄 NetShift config" show_config - # print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - # print_global "рџ”§ System check" + # print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + # print_global "🔧 System check" # if grep -E "^nameserver\s+([0-9]{1,3}\.){3}[0-9]{1,3}" "$RESOLV_CONF" | grep -vqE "127\.0\.0\.1|0\.0\.0\.0"; then - # print_global "вќЊ /etc/resolv.conf contains external nameserver:" + # print_global "❌ /etc/resolv.conf contains external nameserver:" # cat /etc/resolv.conf # echo "" # else - # print_global "вњ… /etc/resolv.conf" + # print_global "✅ /etc/resolv.conf" # fi - # print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - # print_global "рџ§± NFT table" + # print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + # print_global "🧱 NFT table" # check_nft - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "рџ“„ WAN config" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "📄 WAN config" if uci show network.wan > /dev/null 2>&1; then awk ' /^config / { @@ -3694,20 +3694,20 @@ global_check() { } ' /etc/config/network else - print_global "вќЊ WAN configuration not found" + print_global "❌ WAN configuration not found" fi if uci show network | grep -q endpoint_host; then uci show network | grep endpoint_host | cut -d'=' -f2 | tr -d "'\" " | while read -r host; do if [ "$host" = "engage.cloudflareclient.com" ]; then - print_global "вљ пёЏ WARP detected: $host" + print_global "⚠️ WARP detected: $host" continue fi ip_prefix=$(echo "$host" | cut -d'.' -f1,2) if echo "$CLOUDFLARE_OCTETS" | grep -wq "$ip_prefix"; then - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "вљ пёЏ WARP detected: $host" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "⚠️ WARP detected: $host" fi done fi @@ -3718,19 +3718,19 @@ global_check() { allowed_ips=$(uci get "${peer_section}.allowed_ips" 2> /dev/null) if [ "$allowed_ips" = "0.0.0.0/0" ]; then - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "вљ пёЏ WG Route allowed IP enabled with 0.0.0.0/0" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "⚠️ WG Route allowed IP enabled with 0.0.0.0/0" fi done fi if [ -f "/etc/init.d/zapret" ]; then - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "вљ пёЏ Zapret detected" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "⚠️ Zapret detected" fi - print_global "в”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓв”Ѓ" - print_global "🥸 FakeIP status" + print_global "━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_global "🥸 FakeIP status" local fakeip_check_json fakeip_check_json=$(check_fakeip) @@ -3741,21 +3741,21 @@ global_check() { fakeip_status=$(echo "$fakeip_check_json" | jq -r '.fakeip // false') if [ "$fakeip_status" = "true" ]; then - print_global "вњ… Router DNS is routed through sing-box" + print_global "✅ Router DNS is routed through sing-box" else - print_global "вљ пёЏ Router DNS is NOT routed through sing-box" + print_global "⚠️ Router DNS is NOT routed through sing-box" fi else - print_global "вќЊ Failed to get FakeIP info" + print_global "❌ Failed to get FakeIP info" fi local fakeip_address fakeip_address=$(dig +short @127.0.0.42 $FAKEIP_TEST_DOMAIN) if echo "$fakeip_address" | grep -q "^198\.18\."; then - print_global "вњ… Sing-box works with FakeIP: $fakeip_address" + print_global "✅ Sing-box works with FakeIP: $fakeip_address" else - print_global "вќЊ Sing-box does NOT work with FakeIP: $fakeip_address" + print_global "❌ Sing-box does NOT work with FakeIP: $fakeip_address" fi } diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 09c263a8..f3dfe988 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -132,6 +132,35 @@ test_syntax() { fi done + # Parse-check the CLI dispatcher itself (not just the libs). + local cli="${NETSHIFT_SRC}/usr/bin/netshift" + if [ ! -r "$cli" ]; then + fail "File not found: $cli" + elif ash -n "$cli" 2>&1; then + pass "Syntax OK: $(basename "$cli")" + else + fail "Syntax ERROR in $(basename "$cli")" "$(ash -n "$cli" 2>&1)" + fi + + # Guard against re-introduction of the task-004 double-encode mojibake + # (UTF-8 emoji/box-drawing read as CP1251 and re-saved as UTF-8). The + # corrupted bytes render as рџ… / в”… /  ; build the byte markers with + # printf octal escapes (busybox sed/grep lack \x). + if [ -r "$cli" ]; then + local mojibake_found=0 + local marker + for marker in '\321\200\321\237' '\320\262\342\200\235' '\320\262\320\202'; do + if grep -qF "$(printf "$marker")" "$cli" 2>/dev/null; then + mojibake_found=1 + fi + done + if [ "$mojibake_found" -eq 0 ]; then + pass "netshift CLI free of double-encode mojibake" + else + fail "netshift CLI contains residual mojibake (рџ/в”/вЂ)" + fi + fi + # Test that libraries can be sourced (requires /lib/functions stubs). # Use a temp script to avoid fragile shell quoting. local source_test="/tmp/netshift-source-test-$$.sh" From 87694d0db65efcbcf51cfccd1cdd65c984d0ee68 Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Thu, 4 Jun 2026 23:22:44 +0300 Subject: [PATCH 44/75] added vmess support --- .../memory/architect-orchestrator.md | 32 ++++ docs/agent-rules/memory/code-reviewer.md | 4 + .../memory/luci-frontend-developer.md | 31 ++++ .../memory/shell-backend-developer.md | 26 ++++ fe-app-netshift/locales/calls.json | 63 +++++++- fe-app-netshift/locales/netshift.pot | 40 ++++- fe-app-netshift/locales/netshift.ru.po | 33 +++- .../validators/tests/validateVmessUrl.test.js | 122 +++++++++++++++ .../src/validators/validateProxyUrl.ts | 7 +- .../src/validators/validateVmessUrl.ts | 78 ++++++++++ .../resources/view/netshift/main.js | 68 ++++++++- .../resources/view/netshift/section.js | 6 +- luci-app-netshift/po/ru/netshift.po | 33 +++- luci-app-netshift/po/templates/netshift.pot | 40 ++++- netshift/files/usr/lib/helpers.sh | 36 +++++ .../files/usr/lib/sing_box_config_facade.sh | 140 +++++++++++++++++ .../files/usr/lib/sing_box_config_manager.sh | 89 +++++++++++ tests/entrypoint.sh | 142 ++++++++++++++++++ 18 files changed, 957 insertions(+), 33 deletions(-) create mode 100644 fe-app-netshift/src/validators/tests/validateVmessUrl.test.js create mode 100644 fe-app-netshift/src/validators/validateVmessUrl.ts diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index addf8622..ea975797 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -86,6 +86,38 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> `xray_json_count_unsupported`) and dialerProxy-chained outbounds; dedups on the connection part. No-regex jq + busybox-safe sed pre-gate. +## sing-box-extended capability map (researched 2026-06) + +- NetShift ALREADY installs sing-box-extended: `updater.sh` pulls + `shtorm-7/sing-box-extended`; `is_sing_box_extended` gates features (today only + xhttp transport in the facade). So the runtime platform for extended protocols + exists; what's missing is config GENERATION (jq cm_*/cf_*), UCI schema, UI. +- Our facade currently builds only: socks4/4a/5, vless, ss, trojan, hysteria2. + Transports: ws, grpc, httpupgrade, xhttp. No endpoint/wireguard support at all + (`sing_box_cm_add_*_outbound` has no wireguard/endpoint). +- Extended (repo `sing-box-extended-extended/option/*.go`) adds many: anytls, + tuic, shadowtls, wireguard(+Amnezia/AWG), warp(+Amnezia), masque, mieru, + mtproxy, naive, openvpn, ssh, tor, trusttunnel, sudoku, bond, failover, vpn, + vmess; transports incl. v2ray kcp/quic, simple-obfs, sip003. +- Amnezia WG schema (sing-box 1.12 `endpoint` model): an `endpoint` with + `"type":"wireguard"`, `private_key`, `address` (listable prefix), `peers[]` + (address/port/public_key/pre_shared_key/allowed_ips/persistent_keepalive...), + plus nested `"amnezia": { jc,jmin,jmax,s1..s4, h1..h4 (ranges), i1..i5, j1..j3, + itime }`. WARP = same WG core + `amnezia` + Cloudflare `profile`/`reserved`. +- Feasibility tiers for porting to our ash+jq backend: + * EASY (pure-JSON outbound, no extra daemon, just a new cm_* + cf_* + URI/UCI + parse): tuic, anytls, shadowtls, vmess, naive, hysteria(v1). These mirror the + existing vless/trojan/hysteria2 pattern. + * MEDIUM: wireguard + Amnezia/AWG and WARP — needs the `endpoints[]` array + (new section in config skeleton, route ties to endpoint tag) + key/peer + parsing; input format must be decided (awg:// vs wg-conf vs UCI fields). + * HARD / likely out of scope: openvpn, mieru, masque, mtproxy(outbound), + trusttunnel, sudoku, tor, ssh, bond/failover/vpn groups — bespoke schemas, + some need extra config files/daemons; high test surface. +- Hard dependency for ANY of these: the user must be running the extended build; + gate generation behind `is_sing_box_extended` and fail safe (warn + skip) when + stock sing-box is installed, exactly like xhttp does today. + ## Workflow facts - Contribution gating: `CODEOWNERS=@yandexru45`; PRs accepted only after Telegram diff --git a/docs/agent-rules/memory/code-reviewer.md b/docs/agent-rules/memory/code-reviewer.md index cac99aa7..e5a01c2d 100644 --- a/docs/agent-rules/memory/code-reviewer.md +++ b/docs/agent-rules/memory/code-reviewer.md @@ -54,3 +54,7 @@ append recurring findings; keep under ~200 lines. decode HEAD blob vs working tree, strip `[^\x00-\x7F]` per line, expect 0 mismatched lines); beware PowerShell text pipelines which produce false UTF-16 diffs. + +- base64 share-link decode vs `sing_box_cf_add_proxy_outbound` `url_decode` (facade:65): the facade runs `url_decode` (+>space, %XX>byte) on the whole URL before the scheme case. Any case that base64-decodes the ENTIRE payload (vmess, future tuic/etc.) must use the RAW pre-url_decode link standard base64 contains '+'. The ss) case escapes this only because it decodes a short method:password userinfo. Beware synthetic test keys that avoid '+' masking this (false green). + +- For protocol validators that base64-decode a whole body (vmess, future tuic/etc.): the '+'-regression is real only if the dispatcher preserves '+'. validateProxyUrl only .trim()s, so '+' survives at the boundary a green direct-call '+' test is sufficient evidence; a dispatcher-level '+' assertion is the stronger guard. diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 50a3379b..e8264135 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -90,3 +90,34 @@ append findings; keep under ~200 lines. don't "fix" without understanding intent. - Filename typo `checks/contstants.ts` is imported with the typo everywhere — don't "correct" it and break imports. + +## Validator-test node globals (task-006) + +- `.test.js` files are linted as plain JS (typescript parser, no + `languageOptions.globals` in `eslint.config.js`), so bare `Buffer` / `btoa` + trips `no-undef` even though vitest runs in the node env at runtime. The + validator `.ts` files DON'T hit this (TS lib types cover `atob`). Fix in tests + WITHOUT editing eslint config: alias the node global via + `const NodeBuffer = globalThis.Buffer;` (`globalThis` is an allowed global) and + use `NodeBuffer.from(...).toString('base64')` for fixtures. +- VMess `vmess://` is base64(JSON) (V2RayN), NOT user@host. `validateVmessUrl` + decodes with `atob` (right-pad to %4 with `=` for unpadded tolerance, matching + backend), JSON.parse, then narrows `Record<string, unknown>` for + `add`/`id`/`port`. It is dispatcher-only (NOT in `validators/index.ts`), exactly + like `validateHysteria2Url`. Craft a `+`-containing base64 fixture with a field + like `ps:'>>>'` (verified to force `+`). + +## Corepack yarn 4.x vs classic lockfile (task-006) + +- This repo's `yarn.lock` is v1 (classic) but corepack may activate yarn 4.16.0. + Running `yarn install` migrates the lockfile + creates `.yarn/`/`.yarnrc.yml`. + AVOID `yarn install`; node_modules is committed/present. Run CI steps via local + bins: `node_modules/.bin/{prettier --write src, eslint src --ext .ts,.tsx + --max-warnings=0, vitest run, tsup src/main.ts}`. Run locales via + `node {extract-calls,generate-pot,generate-po ru,distribute-locales}.js`. + Before reporting: confirm `git diff --exit-code -- fe-app-netshift/yarn.lock` + and no `.yarn`/`.yarnrc.yml`. +- `locales/calls.json` is committed WITH Windows backslash paths + (`src\\validators\\...`) — generating it on Windows does NOT churn separators. +- The proxy-link help string `"vless://, ... links"` is duplicated 3× in + `section.js` (proxy_string + selector + urltest fields) — use edit replaceAll. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index e308c7b4..92a78fe8 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -100,6 +100,32 @@ findings; keep under ~200 lines. 3.11 at `...\Programs\Python\Python311`). Don't `print()` emoji to the PowerShell console (cp1251 codepage mangles it / raises); write results to a UTF-8 file and read it back. +## task-005 review-001: vmess base64 + url_decode landmine (proven) + +- `sing_box_cf_add_proxy_outbound` runs `url=$(url_decode "$url")` BEFORE the + scheme `case`, and `url_decode` does `s/+/ /g`. Any scheme that base64-decodes + the WHOLE payload (vmess `vmess://base64(JSON)`; future tuic/etc.) MUST decode + from the RAW link, not the url_decode'd one — standard base64's alphabet + includes `+`, so `+`→space corrupts ~1-in-64 real keys. Fix pattern: capture + `local raw_url="$3"` at the top (before url_decode) and pass `$raw_url` to the + whole-payload decoder. Other scheme cases keep using the url_decode'd `$url`. +- **busybox `tr` does NOT support POSIX char classes** — `tr -d '[:space:]'` + deletes the LITERAL chars `[ : s p a c e ]` (silently corrupts base64!). Use + explicit bytes: `tr -d ' \011\012\015'` (space/tab/LF/CR octal). Verified + in-container: input `aZ:[]cept123` → `Zt123` with `[:space:]`. This was a real + regression I introduced and caught via the `sb` smoke run. +- base64 padding normalization for unpadded links: right-pad payload length to a + multiple of 4 with `=` using `pad=$(( ${#p} % 4 ))` then a `while` append loop. + POSIX-safe, busybox-safe. +- To craft a base64 body that DELIBERATELY contains `+`: a `ps`/label value of + `node>>` (bytes 0x3E 0x3E) forces a 6-bit group = 62 → `+`. Realistic ASCII + host/word values rarely hit it; `>>` is reliable. +- Probing helpers in-container without fighting PowerShell quoting: write a tiny + `.sh` into `netshift/files/usr/lib/` (it's bind-mounted into the smoke + container at `/netshift/files`), run via + `docker compose ... run --rm --entrypoint sh netshift-test /netshift/files/usr/lib/_tmp.sh`, + then delete it. Inline `-c "..."` one-liners get mangled by PowerShell. + - `test_syntax` in `tests/entrypoint.sh` now also `ash -n`'s `usr/bin/netshift` and asserts no residual `рџ`/`в”`/`вЂ` markers (built via `printf` octal, since busybox grep lacks `\x`). Guards against re-introducing the mojibake. diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index 4e491ab6..08eee263 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -1034,6 +1034,56 @@ "src\\validators\\validateVlessUrl.ts:110" ] }, + { + "call": "Invalid VMess URL: invalid port", + "key": "Invalid VMess URL: invalid port", + "places": [ + "src\\validators\\validateVmessUrl.ts:73" + ] + }, + { + "call": "Invalid VMess URL: malformed base64", + "key": "Invalid VMess URL: malformed base64", + "places": [ + "src\\validators\\validateVmessUrl.ts:31" + ] + }, + { + "call": "Invalid VMess URL: malformed JSON", + "key": "Invalid VMess URL: malformed JSON", + "places": [ + "src\\validators\\validateVmessUrl.ts:41", + "src\\validators\\validateVmessUrl.ts:48" + ] + }, + { + "call": "Invalid VMess URL: missing address", + "key": "Invalid VMess URL: missing address", + "places": [ + "src\\validators\\validateVmessUrl.ts:57" + ] + }, + { + "call": "Invalid VMess URL: missing id", + "key": "Invalid VMess URL: missing id", + "places": [ + "src\\validators\\validateVmessUrl.ts:64" + ] + }, + { + "call": "Invalid VMess URL: must not contain spaces", + "key": "Invalid VMess URL: must not contain spaces", + "places": [ + "src\\validators\\validateVmessUrl.ts:14" + ] + }, + { + "call": "Invalid VMess URL: must start with vmess://", + "key": "Invalid VMess URL: must start with vmess://", + "places": [ + "src\\validators\\validateVmessUrl.ts:7" + ] + }, { "call": "IP address 0.0.0.0 is not allowed", "key": "IP address 0.0.0.0 is not allowed", @@ -1848,10 +1898,10 @@ ] }, { - "call": "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", - "key": "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", + "call": "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", + "key": "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", "places": [ - "src\\validators\\validateProxyUrl.ts:37" + "src\\validators\\validateProxyUrl.ts:42" ] }, { @@ -1955,7 +2005,8 @@ "src\\validators\\validateSubnet.ts:38", "src\\validators\\validateTrojanUrl.ts:59", "src\\validators\\validateUrl.ts:28", - "src\\validators\\validateVlessUrl.ts:108" + "src\\validators\\validateVlessUrl.ts:108", + "src\\validators\\validateVmessUrl.ts:77" ] }, { @@ -1982,8 +2033,8 @@ ] }, { - "call": "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", - "key": "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", + "call": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", + "key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "places": [ "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:38", "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:156", diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index 5ba7a1b9..d3a9259d 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-04 16:41+0300\n" -"PO-Revision-Date: 2026-06-04 16:41+0300\n" +"POT-Creation-Date: 2026-06-04 19:59+0300\n" +"PO-Revision-Date: 2026-06-04 19:59+0300\n" "Last-Translator: yandexru45 <sukadark228@gmail.com>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -619,6 +619,35 @@ msgstr "" msgid "Invalid VLESS URL: parsing failed" msgstr "" +#: src\validators\validateVmessUrl.ts:73 +msgid "Invalid VMess URL: invalid port" +msgstr "" + +#: src\validators\validateVmessUrl.ts:31 +msgid "Invalid VMess URL: malformed base64" +msgstr "" + +#: src\validators\validateVmessUrl.ts:41 +#: src\validators\validateVmessUrl.ts:48 +msgid "Invalid VMess URL: malformed JSON" +msgstr "" + +#: src\validators\validateVmessUrl.ts:57 +msgid "Invalid VMess URL: missing address" +msgstr "" + +#: src\validators\validateVmessUrl.ts:64 +msgid "Invalid VMess URL: missing id" +msgstr "" + +#: src\validators\validateVmessUrl.ts:14 +msgid "Invalid VMess URL: must not contain spaces" +msgstr "" + +#: src\validators\validateVmessUrl.ts:7 +msgid "Invalid VMess URL: must start with vmess://" +msgstr "" + #: src\validators\validateSubnet.ts:18 msgid "IP address 0.0.0.0 is not allowed" msgstr "" @@ -1093,8 +1122,8 @@ msgstr "" msgid "Uplink" msgstr "" -#: src\validators\validateProxyUrl.ts:37 -msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" +#: src\validators\validateProxyUrl.ts:42 +msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" #: src\validators\validateUrl.ts:17 @@ -1159,6 +1188,7 @@ msgstr "" #: src\validators\validateTrojanUrl.ts:59 #: src\validators\validateUrl.ts:28 #: src\validators\validateVlessUrl.ts:108 +#: src\validators\validateVmessUrl.ts:77 msgid "Valid" msgstr "" @@ -1179,7 +1209,7 @@ msgstr "" #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:38 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:156 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:179 -msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" +msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:405 diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 3c7cc5c8..e78ec4b8 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-04 19:41+0300\n" -"PO-Revision-Date: 2026-06-04 19:41+0300\n" +"POT-Creation-Date: 2026-06-04 22:59+0300\n" +"PO-Revision-Date: 2026-06-04 22:59+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -449,6 +449,27 @@ msgstr "Неверный формат URL" msgid "Invalid VLESS URL: parsing failed" msgstr "Неверный URL VLESS: ошибка разбора" +msgid "Invalid VMess URL: invalid port" +msgstr "Неверный URL VMess: недопустимый порт" + +msgid "Invalid VMess URL: malformed base64" +msgstr "Неверный URL VMess: некорректный base64" + +msgid "Invalid VMess URL: malformed JSON" +msgstr "Неверный URL VMess: некорректный JSON" + +msgid "Invalid VMess URL: missing address" +msgstr "Неверный URL VMess: отсутствует адрес" + +msgid "Invalid VMess URL: missing id" +msgstr "Неверный URL VMess: отсутствует id" + +msgid "Invalid VMess URL: must not contain spaces" +msgstr "Неверный URL VMess: не должен содержать пробелы" + +msgid "Invalid VMess URL: must start with vmess://" +msgstr "Неверный URL VMess: должен начинаться с vmess://" + msgid "IP address 0.0.0.0 is not allowed" msgstr "IP-адрес 0.0.0.0 не допускается" @@ -788,8 +809,8 @@ msgstr "Неизвестная ошибка" msgid "Uplink" msgstr "Исходящий" -msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" -msgstr "URL должен начинаться с vless://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://" +msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" +msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://" msgid "URL must use one of the following protocols:" msgstr "URL должен использовать один из следующих протоколов:" @@ -839,8 +860,8 @@ msgstr "Посмотреть логи" msgid "Visit Wiki" msgstr "Перейти в wiki" -msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" -msgstr "" +msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" +msgstr "ссылки vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2://" msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены." diff --git a/fe-app-netshift/src/validators/tests/validateVmessUrl.test.js b/fe-app-netshift/src/validators/tests/validateVmessUrl.test.js new file mode 100644 index 00000000..2057e2f2 --- /dev/null +++ b/fe-app-netshift/src/validators/tests/validateVmessUrl.test.js @@ -0,0 +1,122 @@ +import { describe, it, expect } from 'vitest'; +import { validateVmessUrl } from '../validateVmessUrl'; + +// Node global (vitest runs in the node environment); aliased so ESLint's +// no-undef does not flag the bare `Buffer` identifier. +const NodeBuffer = globalThis.Buffer; + +// Build a vmess:// link from a config object the V2RayN way: +// vmess:// + base64(JSON). +const b64 = (obj) => NodeBuffer.from(JSON.stringify(obj)).toString('base64'); +const vmess = (obj) => `vmess://${b64(obj)}`; + +const baseConfig = { + v: '2', + ps: 'node', + add: '1.2.3.4', + port: 443, + id: 'b831381d-6324-4d53-ad4f-8cda48b30811', + net: 'ws', + type: 'none', + host: '', + path: '/', + tls: 'tls', + aid: 0, +}; + +// A config whose JSON base64 contains a '+' char (ps:'>>>' forces it). +// Regression parity with the backend S1 fix. +const plusB64 = b64({ ...baseConfig, ps: '>>>' }); + +// An unpadded base64 variant of a valid config (strip trailing '=' padding). +const unpaddedBody = b64(baseConfig).replace(/=+$/, ''); + +const validUrls = [ + ['basic add/port/id', vmess({ add: '1.2.3.4', port: 443, id: 'uuid-1' })], + ['full config with net:ws tls:tls', vmess(baseConfig)], + ['port as numeric string', vmess({ ...baseConfig, port: '8443' })], + ['base64 body containing "+"', `vmess://${plusB64}`], + ['unpadded base64 body', `vmess://${unpaddedBody}`], +]; + +const invalidUrls = [ + ['wrong prefix', `vless://${b64(baseConfig)}`], + ['contains space', `vmess://${b64(baseConfig)} `], + ['non-base64 body', 'vmess://!!!not base64!!!'], + [ + 'base64 of non-JSON', + `vmess://${NodeBuffer.from('not json at all').toString('base64')}`, + ], + [ + 'base64 of JSON array', + `vmess://${NodeBuffer.from('[1,2,3]').toString('base64')}`, + ], + ['missing add', vmess({ port: 443, id: 'uuid-1' })], + ['empty add', vmess({ add: '', port: 443, id: 'uuid-1' })], + ['missing id', vmess({ add: '1.2.3.4', port: 443 })], + ['empty id', vmess({ add: '1.2.3.4', port: 443, id: '' })], + ['port 0', vmess({ add: '1.2.3.4', port: 0, id: 'uuid-1' })], + ['port 99999', vmess({ add: '1.2.3.4', port: 99999, id: 'uuid-1' })], + ['non-numeric port', vmess({ add: '1.2.3.4', port: 'abc', id: 'uuid-1' })], +]; + +describe('validateVmessUrl', () => { + describe.each(validUrls)('Valid URL: %s', (_desc, url) => { + it(`returns valid=true for "${url}"`, () => { + const res = validateVmessUrl(url); + expect(res.valid).toBe(true); + expect(res.message).toBe('Valid'); + }); + }); + + describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => { + it(`returns valid=false for "${url}"`, () => { + const res = validateVmessUrl(url); + expect(res.valid).toBe(false); + }); + }); + + it('reports the wrong-prefix message', () => { + const res = validateVmessUrl('http://example.com'); + expect(res.valid).toBe(false); + expect(res.message).toBe('Invalid VMess URL: must start with vmess://'); + }); + + it('reports malformed base64', () => { + const res = validateVmessUrl('vmess://@@@@'); + expect(res.valid).toBe(false); + expect(res.message).toBe('Invalid VMess URL: malformed base64'); + }); + + it('reports malformed JSON', () => { + const res = validateVmessUrl( + `vmess://${NodeBuffer.from('definitely not json').toString('base64')}`, + ); + expect(res.valid).toBe(false); + expect(res.message).toBe('Invalid VMess URL: malformed JSON'); + }); + + it('reports missing address', () => { + const res = validateVmessUrl(vmess({ port: 443, id: 'uuid-1' })); + expect(res.valid).toBe(false); + expect(res.message).toBe('Invalid VMess URL: missing address'); + }); + + it('reports missing id', () => { + const res = validateVmessUrl(vmess({ add: '1.2.3.4', port: 443 })); + expect(res.valid).toBe(false); + expect(res.message).toBe('Invalid VMess URL: missing id'); + }); + + it('reports invalid port', () => { + const res = validateVmessUrl( + vmess({ add: '1.2.3.4', port: 99999, id: 'uuid-1' }), + ); + expect(res.valid).toBe(false); + expect(res.message).toBe('Invalid VMess URL: invalid port'); + }); + + it('confirms the "+"-containing base64 fixture really contains "+"', () => { + expect(plusB64).toContain('+'); + }); +}); diff --git a/fe-app-netshift/src/validators/validateProxyUrl.ts b/fe-app-netshift/src/validators/validateProxyUrl.ts index d0be83d7..0e6f5851 100644 --- a/fe-app-netshift/src/validators/validateProxyUrl.ts +++ b/fe-app-netshift/src/validators/validateProxyUrl.ts @@ -4,6 +4,7 @@ import { validateVlessUrl } from './validateVlessUrl'; import { validateTrojanUrl } from './validateTrojanUrl'; import { validateSocksUrl } from './validateSocksUrl'; import { validateHysteria2Url } from './validateHysteriaUrl'; +import { validateVmessUrl } from './validateVmessUrl'; // TODO refactor current validation and add tests export function validateProxyUrl(url: string): ValidationResult { @@ -21,6 +22,10 @@ export function validateProxyUrl(url: string): ValidationResult { return validateTrojanUrl(trimmedUrl); } + if (trimmedUrl.startsWith('vmess://')) { + return validateVmessUrl(trimmedUrl); + } + if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) { return validateSocksUrl(trimmedUrl); } @@ -35,7 +40,7 @@ export function validateProxyUrl(url: string): ValidationResult { return { valid: false, message: _( - 'URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://', + 'URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://', ), }; } diff --git a/fe-app-netshift/src/validators/validateVmessUrl.ts b/fe-app-netshift/src/validators/validateVmessUrl.ts new file mode 100644 index 00000000..91897200 --- /dev/null +++ b/fe-app-netshift/src/validators/validateVmessUrl.ts @@ -0,0 +1,78 @@ +import { ValidationResult } from './types'; + +export function validateVmessUrl(url: string): ValidationResult { + if (!url.startsWith('vmess://')) { + return { + valid: false, + message: _('Invalid VMess URL: must start with vmess://'), + }; + } + + if (/\s/.test(url)) { + return { + valid: false, + message: _('Invalid VMess URL: must not contain spaces'), + }; + } + + const body = url.slice('vmess://'.length); + + // VMess (V2RayN) is vmess:// + base64(JSON), not a user@host URL. + // Tolerate unpadded base64 by right-padding to a multiple of 4, matching + // the backend fix. + const padded = body + '='.repeat((4 - (body.length % 4)) % 4); + + let decoded: string; + try { + decoded = atob(padded); + } catch (_e) { + return { + valid: false, + message: _('Invalid VMess URL: malformed base64'), + }; + } + + let parsed: unknown; + try { + parsed = JSON.parse(decoded); + } catch (_e) { + return { + valid: false, + message: _('Invalid VMess URL: malformed JSON'), + }; + } + + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + return { + valid: false, + message: _('Invalid VMess URL: malformed JSON'), + }; + } + + const config = parsed as Record<string, unknown>; + + if (typeof config.add !== 'string' || config.add.length === 0) { + return { + valid: false, + message: _('Invalid VMess URL: missing address'), + }; + } + + if (typeof config.id !== 'string' || config.id.length === 0) { + return { + valid: false, + message: _('Invalid VMess URL: missing id'), + }; + } + + const portNum = Number(config.port); + + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { + return { + valid: false, + message: _('Invalid VMess URL: invalid port'), + }; + } + + return { valid: true, message: _('Valid') }; +} diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 7f9abdb0..9f2f0f8d 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -539,6 +539,69 @@ function validateHysteria2Url(url) { } } +// src/validators/validateVmessUrl.ts +function validateVmessUrl(url) { + if (!url.startsWith("vmess://")) { + return { + valid: false, + message: _("Invalid VMess URL: must start with vmess://") + }; + } + if (/\s/.test(url)) { + return { + valid: false, + message: _("Invalid VMess URL: must not contain spaces") + }; + } + const body = url.slice("vmess://".length); + const padded = body + "=".repeat((4 - body.length % 4) % 4); + let decoded; + try { + decoded = atob(padded); + } catch (_e) { + return { + valid: false, + message: _("Invalid VMess URL: malformed base64") + }; + } + let parsed; + try { + parsed = JSON.parse(decoded); + } catch (_e) { + return { + valid: false, + message: _("Invalid VMess URL: malformed JSON") + }; + } + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + return { + valid: false, + message: _("Invalid VMess URL: malformed JSON") + }; + } + const config = parsed; + if (typeof config.add !== "string" || config.add.length === 0) { + return { + valid: false, + message: _("Invalid VMess URL: missing address") + }; + } + if (typeof config.id !== "string" || config.id.length === 0) { + return { + valid: false, + message: _("Invalid VMess URL: missing id") + }; + } + const portNum = Number(config.port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { + return { + valid: false, + message: _("Invalid VMess URL: invalid port") + }; + } + return { valid: true, message: _("Valid") }; +} + // src/validators/validateProxyUrl.ts function validateProxyUrl(url) { const trimmedUrl = url.trim(); @@ -551,6 +614,9 @@ function validateProxyUrl(url) { if (trimmedUrl.startsWith("trojan://")) { return validateTrojanUrl(trimmedUrl); } + if (trimmedUrl.startsWith("vmess://")) { + return validateVmessUrl(trimmedUrl); + } if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) { return validateSocksUrl(trimmedUrl); } @@ -560,7 +626,7 @@ function validateProxyUrl(url) { return { valid: false, message: _( - "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" + "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" ) }; } diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index 271c6ab6..c517e0de 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -35,7 +35,7 @@ function createSectionContent(section) { form.TextValue, "proxy_string", _("Proxy Configuration URL"), - _("vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") + _("vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") ); o.depends({ connection_type: "proxy", proxy_config_type: "url" }); o.rows = 5; @@ -153,7 +153,7 @@ function createSectionContent(section) { form.DynamicList, "selector_proxy_links", _("Selector Proxy Links"), - _("vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") + _("vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") ); o.depends({ connection_type: "proxy", proxy_config_type: "selector" }); o.rmempty = false; @@ -176,7 +176,7 @@ function createSectionContent(section) { form.DynamicList, "urltest_proxy_links", _("URLTest Proxy Links"), - _("vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") + _("vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") ); o.depends({ connection_type: "proxy", proxy_config_type: "urltest" }); o.rmempty = false; diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 3c7cc5c8..e78ec4b8 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-04 19:41+0300\n" -"PO-Revision-Date: 2026-06-04 19:41+0300\n" +"POT-Creation-Date: 2026-06-04 22:59+0300\n" +"PO-Revision-Date: 2026-06-04 22:59+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -449,6 +449,27 @@ msgstr "Неверный формат URL" msgid "Invalid VLESS URL: parsing failed" msgstr "Неверный URL VLESS: ошибка разбора" +msgid "Invalid VMess URL: invalid port" +msgstr "Неверный URL VMess: недопустимый порт" + +msgid "Invalid VMess URL: malformed base64" +msgstr "Неверный URL VMess: некорректный base64" + +msgid "Invalid VMess URL: malformed JSON" +msgstr "Неверный URL VMess: некорректный JSON" + +msgid "Invalid VMess URL: missing address" +msgstr "Неверный URL VMess: отсутствует адрес" + +msgid "Invalid VMess URL: missing id" +msgstr "Неверный URL VMess: отсутствует id" + +msgid "Invalid VMess URL: must not contain spaces" +msgstr "Неверный URL VMess: не должен содержать пробелы" + +msgid "Invalid VMess URL: must start with vmess://" +msgstr "Неверный URL VMess: должен начинаться с vmess://" + msgid "IP address 0.0.0.0 is not allowed" msgstr "IP-адрес 0.0.0.0 не допускается" @@ -788,8 +809,8 @@ msgstr "Неизвестная ошибка" msgid "Uplink" msgstr "Исходящий" -msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" -msgstr "URL должен начинаться с vless://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://" +msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" +msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://" msgid "URL must use one of the following protocols:" msgstr "URL должен использовать один из следующих протоколов:" @@ -839,8 +860,8 @@ msgstr "Посмотреть логи" msgid "Visit Wiki" msgstr "Перейти в wiki" -msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" -msgstr "" +msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" +msgstr "ссылки vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2://" msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены." diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index 5ba7a1b9..d3a9259d 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-04 16:41+0300\n" -"PO-Revision-Date: 2026-06-04 16:41+0300\n" +"POT-Creation-Date: 2026-06-04 19:59+0300\n" +"PO-Revision-Date: 2026-06-04 19:59+0300\n" "Last-Translator: yandexru45 <sukadark228@gmail.com>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -619,6 +619,35 @@ msgstr "" msgid "Invalid VLESS URL: parsing failed" msgstr "" +#: src\validators\validateVmessUrl.ts:73 +msgid "Invalid VMess URL: invalid port" +msgstr "" + +#: src\validators\validateVmessUrl.ts:31 +msgid "Invalid VMess URL: malformed base64" +msgstr "" + +#: src\validators\validateVmessUrl.ts:41 +#: src\validators\validateVmessUrl.ts:48 +msgid "Invalid VMess URL: malformed JSON" +msgstr "" + +#: src\validators\validateVmessUrl.ts:57 +msgid "Invalid VMess URL: missing address" +msgstr "" + +#: src\validators\validateVmessUrl.ts:64 +msgid "Invalid VMess URL: missing id" +msgstr "" + +#: src\validators\validateVmessUrl.ts:14 +msgid "Invalid VMess URL: must not contain spaces" +msgstr "" + +#: src\validators\validateVmessUrl.ts:7 +msgid "Invalid VMess URL: must start with vmess://" +msgstr "" + #: src\validators\validateSubnet.ts:18 msgid "IP address 0.0.0.0 is not allowed" msgstr "" @@ -1093,8 +1122,8 @@ msgstr "" msgid "Uplink" msgstr "" -#: src\validators\validateProxyUrl.ts:37 -msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" +#: src\validators\validateProxyUrl.ts:42 +msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" #: src\validators\validateUrl.ts:17 @@ -1159,6 +1188,7 @@ msgstr "" #: src\validators\validateTrojanUrl.ts:59 #: src\validators\validateUrl.ts:28 #: src\validators\validateVlessUrl.ts:108 +#: src\validators\validateVmessUrl.ts:77 msgid "Valid" msgstr "" @@ -1179,7 +1209,7 @@ msgstr "" #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:38 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:156 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:179 -msgid "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" +msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:405 diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index 9d437eb9..d8c779d9 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -221,6 +221,42 @@ base64_decode() { echo "$decoded_url" } +# Decodes a vmess:// share link (V2RayN base64(JSON) form) into its JSON object. +# Strips the vmess:// scheme prefix, base64-decodes the remainder, and echoes the +# decoded text (expected to be a JSON object; the caller validates with jq -e). +# Returns empty output when the input is not a base64(JSON) VMess link. +# +# IMPORTANT: this decodes the WHOLE payload as STANDARD base64 (alphabet +# includes '+'), so the caller MUST pass the RAW pre-url_decode link — passing a +# url_decode'd link rewrites '+'->space and corrupts the body. +# Arguments: +# $1 - the vmess:// link (raw, pre-url_decode) +vmess_link_to_json() { + local url="$1" + local payload decoded pad_len + + payload="${url#vmess://}" + [ -n "$payload" ] || return 0 + + # Normalize: strip whitespace (space, tab, CR, LF via octal escapes — busybox + # `tr` does NOT understand the POSIX `[:space:]` class and would instead + # delete those literal characters, corrupting the base64), then right-pad to + # a multiple of 4 with '=' so BusyBox `base64 -d` (which can reject missing + # padding) accepts real-world unpadded links. + payload="$(printf '%s' "$payload" | tr -d ' \011\012\015')" + pad_len=$(( ${#payload} % 4 )) + if [ "$pad_len" -ne 0 ]; then + pad_len=$(( 4 - pad_len )) + while [ "$pad_len" -gt 0 ]; do + payload="${payload}=" + pad_len=$(( pad_len - 1 )) + done + fi + + decoded="$(base64_decode "$payload")" + echo "$decoded" +} + # Generates a unique 16-character ID based on the current timestamp and a random number gen_id() { { date +%s; head -c 16 /dev/urandom; } | md5sum | cut -c1-16 diff --git a/netshift/files/usr/lib/sing_box_config_facade.sh b/netshift/files/usr/lib/sing_box_config_facade.sh index e35c03f0..08b1306d 100644 --- a/netshift/files/usr/lib/sing_box_config_facade.sh +++ b/netshift/files/usr/lib/sing_box_config_facade.sh @@ -62,6 +62,12 @@ sing_box_cf_add_proxy_outbound() { local url="$3" local udp_over_tcp="$4" + # Keep the RAW (pre-url_decode) link for schemes that base64-decode the + # WHOLE payload (vmess). url_decode rewrites '+'->space, which corrupts + # standard base64 bodies (the '+' is in the base64 alphabet). See the + # vmess) case below. + local raw_url="$3" + url=$(url_decode "$url") url=$(url_strip_fragment "$url") @@ -163,6 +169,57 @@ sing_box_cf_add_proxy_outbound() { "$obfuscator_password" "$upload_mbps" "$download_mbps") config=$(_add_outbound_security "$config" "$tag" "$url") ;; + vmess) + # ─── REFERENCE EXTENDED-GATING PATTERN (Tier-1 protocols copy this) ─── + # Generation is gated behind sing-box-extended. On a stock sing-box build + # we log a clear message and return the config UNCHANGED (no exit 1, no + # outbound added) so generation degrades safely and keeps the last-good + # config. tuic/hysteria1/anytls/shadowtls reuse this exact block. + if ! is_sing_box_extended; then + log "VMess requires sing-box-extended. Install sing-box-extended and retry." "error" + echo "$config" + return 0 + fi + # ───────────────────────────────────────────────────────────────────── + + local tag vmess_json vm_server vm_port vm_uuid vm_security vm_alter_id + local vm_net vm_host vm_path vm_tls vm_sni vm_alpn vm_fp + + tag=$(get_outbound_tag_by_section "$section") + + # Primary format: vmess://base64(JSON) (V2RayN). The URL form + # (vmess://<uuid>@<host>:<port>?...) is a phase-2 follow-on; not handled here. + # + # CRITICAL: VMess base64-decodes the WHOLE payload, so it MUST use the + # RAW pre-url_decode link ($raw_url, NOT $url). url_decode rewrites + # '+'->space, which would corrupt standard base64 bodies containing '+'. + # Future Tier-1 copiers (tuic/etc.) that base64-decode a whole payload + # MUST also use $raw_url for the same reason. + vmess_json=$(vmess_link_to_json "$raw_url") + if [ -z "$vmess_json" ] || ! echo "$vmess_json" | jq -e 'type == "object"' > /dev/null 2>&1; then + log "Cannot decode VMess link or it does not match the expected base64(JSON) format. Aborted." "fatal" + exit 1 + fi + + # Numbers-as-strings are tolerated: extract every field as a string. + vm_server=$(echo "$vmess_json" | jq -r '.add // ""') + vm_port=$(echo "$vmess_json" | jq -r '.port // "" | tostring') + vm_uuid=$(echo "$vmess_json" | jq -r '.id // ""') + vm_security=$(echo "$vmess_json" | jq -r '.scy // "" | tostring') + vm_alter_id=$(echo "$vmess_json" | jq -r '.aid // "" | tostring') + vm_net=$(echo "$vmess_json" | jq -r '.net // "" | tostring') + vm_host=$(echo "$vmess_json" | jq -r '.host // "" | tostring') + vm_path=$(echo "$vmess_json" | jq -r '.path // "" | tostring') + vm_tls=$(echo "$vmess_json" | jq -r '.tls // "" | tostring') + vm_sni=$(echo "$vmess_json" | jq -r '.sni // "" | tostring') + vm_alpn=$(echo "$vmess_json" | jq -r '.alpn // "" | tostring') + vm_fp=$(echo "$vmess_json" | jq -r '.fp // "" | tostring') + + config=$(sing_box_cm_add_vmess_outbound "$config" "$tag" "$vm_server" "$vm_port" "$vm_uuid" \ + "$vm_security" "$vm_alter_id") + config=$(_add_vmess_transport_and_security "$config" "$tag" "$vm_net" "$vm_host" "$vm_path" \ + "$vm_tls" "$vm_sni" "$vm_alpn" "$vm_fp" "$vm_server") + ;; *) log "Unsupported proxy $scheme type. Aborted." "fatal" exit 1 @@ -290,6 +347,89 @@ _add_outbound_transport() { echo "$config" } +####################################### +# Apply VMess TLS + transport to an already-added vmess outbound. +# VMess (V2RayN base64-JSON) carries transport in the JSON `net` field and TLS +# in `tls`/`sni`/`alpn`/`fp` — NOT in URL query params. So we do NOT route +# through _add_outbound_security / _add_outbound_transport (which read +# url_get_query_param); we set everything from the decoded-JSON values here. +# Arguments: +# config: string (JSON), sing-box configuration to modify +# tag: string, outbound tag to mutate +# net: string, vmess `net` (ws|grpc|h2|tcp|"") +# host: string, vmess `host` +# path: string, vmess `path` +# tls: string, vmess `tls` ("tls"/"1"/"true" enables TLS) +# sni: string, vmess `sni` +# alpn: string, vmess `alpn` (comma-separated) +# fp: string, vmess `fp` (uTLS fingerprint) +# server: string, vmess `add` (TLS server_name fallback when sni is empty) +# Outputs: +# Writes updated JSON configuration to stdout +####################################### +_add_vmess_transport_and_security() { + local config="$1" + local outbound_tag="$2" + local net="$3" + local host="$4" + local path="$5" + local tls="$6" + local sni="$7" + local alpn="$8" + local fp="$9" + local server="${10}" + + # Transport from `net` (NOT a query `type`). + local tls_required=0 + case "$net" in + ws) + config=$(sing_box_cm_set_ws_transport_for_outbound "$config" "$outbound_tag" "$path" "$host") + ;; + grpc) + # The core derives the gRPC serviceName from `path` (falls back to `host`). + local grpc_service_name="$path" + [ -n "$grpc_service_name" ] || grpc_service_name="$host" + config=$(sing_box_cm_set_grpc_transport_for_outbound "$config" "$outbound_tag" "$grpc_service_name") + ;; + h2) + # HTTP/2 transport mandates TLS. + config=$(sing_box_cm_set_http_transport_for_outbound "$config" "$outbound_tag" "$path" "$host") + tls_required=1 + ;; + tcp | "") ;; + *) + log "Unknown VMess transport '$net' detected." "error" + ;; + esac + + # TLS from `tls` (or forced by h2 transport). + local tls_enabled=0 + case "$tls" in + tls | 1 | true) tls_enabled=1 ;; + esac + [ "$tls_required" -eq 1 ] && tls_enabled=1 + + if [ "$tls_enabled" -eq 1 ]; then + local server_name alpn_json + server_name="$sni" + [ -n "$server_name" ] || server_name="$server" + alpn_json=$(comma_string_to_json_array "$alpn") + config=$( + sing_box_cm_set_tls_for_outbound \ + "$config" \ + "$outbound_tag" \ + "$server_name" \ + "" \ + "$([ "$alpn_json" = "[]" ] && echo null || echo "$alpn_json")" \ + "$fp" \ + "" \ + "" + ) + fi + + echo "$config" +} + sing_box_cf_add_json_outbound() { local config="$1" local section="$2" diff --git a/netshift/files/usr/lib/sing_box_config_manager.sh b/netshift/files/usr/lib/sing_box_config_manager.sh index 8e8258df..751c7a06 100644 --- a/netshift/files/usr/lib/sing_box_config_manager.sh +++ b/netshift/files/usr/lib/sing_box_config_manager.sh @@ -622,6 +622,56 @@ sing_box_cm_add_vless_outbound() { )]' } +####################################### +# Add a VMess outbound to the outbounds section of a sing-box JSON configuration. +# Requires sing-box-extended on the router (gated by the facade). +# Arguments: +# config: string (JSON), sing-box configuration to modify +# tag: string, identifier for the outbound +# server_address: string, IP address or hostname of the VMess server +# server_port: integer, port of the VMess server +# uuid: string, user UUID +# security: string, encryption method (optional; defaults to "auto" when empty) +# alter_id: integer, alterId (optional; omitted when empty or 0) +# Outputs: +# Writes updated JSON configuration to stdout +# Example: +# CONFIG=$( +# sing_box_cm_add_vmess_outbound "$CONFIG" "vmess-out" "example.com" 443 \ +# "bf000d23-0752-40b4-affe-68f7707a9661" "auto" "0" +# ) +####################################### +sing_box_cm_add_vmess_outbound() { + local config="$1" + local tag="$2" + local server_address="$3" + local server_port="$4" + local uuid="$5" + local security="$6" + local alter_id="$7" + + [ -n "$security" ] || security="auto" + + echo "$config" | jq \ + --arg tag "$tag" \ + --arg server_address "$server_address" \ + --arg server_port "$server_port" \ + --arg uuid "$uuid" \ + --arg security "$security" \ + --arg alter_id "$alter_id" \ + '.outbounds += [( + { + type: "vmess", + tag: $tag, + server: $server_address, + server_port: ($server_port | tonumber), + uuid: $uuid, + security: $security + } + + (if $alter_id != "" and $alter_id != "0" then {alter_id: ($alter_id | tonumber)} else {} end) + )]' +} + ####################################### # Add a Trojan outbound to the outbounds section of a sing-box JSON configuration. # Arguments: @@ -834,6 +884,45 @@ sing_box_cm_set_ws_transport_for_outbound() { )' } +####################################### +# Set HTTP/2 transport settings for an outbound in a sing-box JSON configuration. +# Used for VMess net=h2 links (sing-box "http" transport). HTTP/2 transport +# mandates TLS, so the caller must also set TLS on the outbound. +# Arguments: +# config: string (JSON), sing-box configuration to modify +# tag: string, identifier of the outbound to modify +# path: string, HTTP path (optional) +# host: string, Host header (single host; optional) +# Outputs: +# Writes updated JSON configuration to stdout +# Example: +# CONFIG=$(sing_box_cm_set_http_transport_for_outbound "$CONFIG" "vmess-out" "/path" "example.com") +####################################### +sing_box_cm_set_http_transport_for_outbound() { + local config="$1" + local tag="$2" + local path="$3" + local host="$4" + + echo "$config" | jq \ + --arg tag "$tag" \ + --arg path "$path" \ + --arg host "$host" \ + '.outbounds |= map( + if .tag == $tag then + . + { + transport: ( + { type: "http" } + + (if $path != "" then {path: $path} else {} end) + + (if $host != "" then {host: [$host]} else {} end) + ) + } + else + . + end + )' +} + ####################################### # Set XHTTP transport settings for an outbound in a sing-box JSON configuration. # Requires sing-box-extended on the router. diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index f3dfe988..2ec8e64e 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -406,6 +406,104 @@ test_sing_box_config() { fi rm -f "$test_config" "${test_config}.2" "${test_config}.3" "${test_config}.4" "${test_config}.5" + + # ── VMess vmess://base64(JSON) parse path (facade) ───────────────────── + # Validate the GENERATED outbound JSON SHAPE with jq (NOT a live sing-box + # check): the test container's sing-box is the stock build, which rejects + # the vmess type, so we assert shape only. The extended gate is exercised + # by toggling is_sing_box_extended via a shell override. + local facade_lib="${NETSHIFT_LIB_DIR}/sing_box_config_facade.sh" + if [ ! -r "$facade_lib" ]; then + fail "sing_box_config_facade.sh not found" + return + fi + + # The facade hardcodes NETSHIFT_LIB="/usr/lib/netshift" for its own sourcing + # of helpers.sh + sing_box_config_manager.sh; bind the bind-mounted sources + # to that runtime path so the facade resolves them in the container. + mkdir -p /usr/lib/netshift + ln -sf "${NETSHIFT_LIB_DIR}/helpers.sh" /usr/lib/netshift/helpers.sh + ln -sf "${NETSHIFT_LIB_DIR}/sing_box_config_manager.sh" /usr/lib/netshift/sing_box_config_manager.sh + + local vm_tmp="/tmp/test-vmess-facade-$$.sh" + cat > "$vm_tmp" << 'VMEOF' +# logging.sh is sourced by /usr/bin/netshift in production; the facade itself +# only sources helpers + manager, so pull it in for log() here. +. "NETSHIFT_LIB/logging.sh" 2>/dev/null || log() { :; } +. "FACADE_LIB_PATH" + +base_config='{"outbounds":[]}' + +# ws + tls synthetic link: base64(JSON). aid=0 must be omitted. +ws_json='{"v":"2","ps":"node-ws","add":"ws.example.com","port":"443","id":"11111111-2222-3333-4444-555555555555","aid":"0","scy":"auto","net":"ws","host":"ws.example.com","path":"/wspath","tls":"tls","sni":"sni.example.com","alpn":"h2,http/1.1","fp":"chrome"}' +ws_link="vmess://$(printf '%s' "$ws_json" | base64 | tr -d '\n')" + +# plain tcp synthetic link: no transport, no tls. +tcp_json='{"v":"2","ps":"node-tcp","add":"tcp.example.com","port":"8080","id":"99999999-8888-7777-6666-555555555555","aid":"0","scy":"auto","net":"tcp","host":"","path":"","tls":"","sni":"","alpn":"","fp":""}' +tcp_link="vmess://$(printf '%s' "$tcp_json" | base64 | tr -d '\n')" + +# REGRESSION (S1): a key whose STANDARD base64 body DELIBERATELY contains a '+'. +# The "node>>" ps label (bytes 0x3E 0x3E) forces a base64 group that maps to +# '+' (alphabet index 62). If the facade url_decode'd the link before decoding, +# the '+'->space rewrite would corrupt the body and base64 -d would fail/garble, +# so this outbound would NOT be generated. Asserting server/uuid here proves the +# raw-link threading keeps '+' intact. +plus_json='{"v":"2","ps":"node>>","add":"plus.example.com","port":"2053","id":"abcdef00-1111-2222-3333-444455556666","aid":"0","scy":"auto","net":"tcp","host":"","path":"","tls":"","sni":"","alpn":"","fp":""}' +plus_link="vmess://$(printf '%s' "$plus_json" | base64 | tr -d '\n')" +# Sanity: confirm the crafted base64 body actually contains a '+'. +case "$plus_link" in +*+*) echo 'vmess-plus-body-has-plus:OK' ;; +*) echo 'vmess-plus-body-has-plus:FAIL' ;; +esac + +# ── Extended ON: parse path produces a real vmess outbound ── +is_sing_box_extended() { return 0; } + +out_ws=$(sing_box_cf_add_proxy_outbound "$base_config" "vmess_ws" "$ws_link" "0") +echo "$out_ws" | jq -e '.outbounds[0].type == "vmess"' >/dev/null 2>&1 && echo 'vmess-ws-type:OK' || echo 'vmess-ws-type:FAIL' +echo "$out_ws" | jq -e '.outbounds[0].server == "ws.example.com"' >/dev/null 2>&1 && echo 'vmess-ws-server:OK' || echo 'vmess-ws-server:FAIL' +echo "$out_ws" | jq -e '.outbounds[0].server_port == 443' >/dev/null 2>&1 && echo 'vmess-ws-port:OK' || echo 'vmess-ws-port:FAIL' +echo "$out_ws" | jq -e '.outbounds[0] | has("alter_id") | not' >/dev/null 2>&1 && echo 'vmess-ws-aid-omitted:OK' || echo 'vmess-ws-aid-omitted:FAIL' +echo "$out_ws" | jq -e '.outbounds[0].transport.type == "ws"' >/dev/null 2>&1 && echo 'vmess-ws-transport:OK' || echo 'vmess-ws-transport:FAIL' +echo "$out_ws" | jq -e '.outbounds[0].transport.path == "/wspath"' >/dev/null 2>&1 && echo 'vmess-ws-path:OK' || echo 'vmess-ws-path:FAIL' +echo "$out_ws" | jq -e '.outbounds[0].transport.headers.Host == "ws.example.com"' >/dev/null 2>&1 && echo 'vmess-ws-host:OK' || echo 'vmess-ws-host:FAIL' +echo "$out_ws" | jq -e '.outbounds[0].tls.enabled == true' >/dev/null 2>&1 && echo 'vmess-ws-tls:OK' || echo 'vmess-ws-tls:FAIL' +echo "$out_ws" | jq -e '.outbounds[0].tls.server_name == "sni.example.com"' >/dev/null 2>&1 && echo 'vmess-ws-sni:OK' || echo 'vmess-ws-sni:FAIL' +echo "$out_ws" | jq -e '.outbounds[0].tls.alpn == ["h2","http/1.1"]' >/dev/null 2>&1 && echo 'vmess-ws-alpn:OK' || echo 'vmess-ws-alpn:FAIL' +echo "$out_ws" | jq -e '.outbounds[0].tls.utls.fingerprint == "chrome"' >/dev/null 2>&1 && echo 'vmess-ws-fp:OK' || echo 'vmess-ws-fp:FAIL' + +out_tcp=$(sing_box_cf_add_proxy_outbound "$base_config" "vmess_tcp" "$tcp_link" "0") +echo "$out_tcp" | jq -e '.outbounds[0].type == "vmess"' >/dev/null 2>&1 && echo 'vmess-tcp-type:OK' || echo 'vmess-tcp-type:FAIL' +echo "$out_tcp" | jq -e '.outbounds[0] | has("transport") | not' >/dev/null 2>&1 && echo 'vmess-tcp-no-transport:OK' || echo 'vmess-tcp-no-transport:FAIL' +echo "$out_tcp" | jq -e '.outbounds[0] | has("tls") | not' >/dev/null 2>&1 && echo 'vmess-tcp-no-tls:OK' || echo 'vmess-tcp-no-tls:FAIL' +echo "$out_tcp" | jq -e '.outbounds[0].security == "auto"' >/dev/null 2>&1 && echo 'vmess-tcp-security:OK' || echo 'vmess-tcp-security:FAIL' + +# ── REGRESSION (S1): '+'-in-base64 link must parse via the RAW link ── +out_plus=$(sing_box_cf_add_proxy_outbound "$base_config" "vmess_plus" "$plus_link" "0") +echo "$out_plus" | jq -e '.outbounds[0].type == "vmess"' >/dev/null 2>&1 && echo 'vmess-plus-type:OK' || echo 'vmess-plus-type:FAIL' +echo "$out_plus" | jq -e '.outbounds[0].server == "plus.example.com"' >/dev/null 2>&1 && echo 'vmess-plus-server:OK' || echo 'vmess-plus-server:FAIL' +echo "$out_plus" | jq -e '.outbounds[0].server_port == 2053' >/dev/null 2>&1 && echo 'vmess-plus-port:OK' || echo 'vmess-plus-port:FAIL' +echo "$out_plus" | jq -e '.outbounds[0].uuid == "abcdef00-1111-2222-3333-444455556666"' >/dev/null 2>&1 && echo 'vmess-plus-uuid:OK' || echo 'vmess-plus-uuid:FAIL' + +# ── Extended OFF: gate returns config UNCHANGED (no vmess outbound) ── +is_sing_box_extended() { return 1; } +out_gate=$(sing_box_cf_add_proxy_outbound "$base_config" "vmess_gate" "$ws_link" "0") +echo "$out_gate" | jq -e '.outbounds | length == 0' >/dev/null 2>&1 && echo 'vmess-gate-unchanged:OK' || echo 'vmess-gate-unchanged:FAIL' + +echo 'DONE' +VMEOF + sed -i "s|FACADE_LIB_PATH|$facade_lib|; s|NETSHIFT_LIB|$NETSHIFT_LIB_DIR|g" "$vm_tmp" + + sh "$vm_tmp" 2>&1 | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + *:SKIP) skip "$line" ;; + DONE) ;; + *) ;; + esac + done + rm -f "$vm_tmp" } # ───────────────────────────────────────────────────────────────── @@ -532,6 +630,50 @@ test_config_manager() { else fail "jq: route rule failed" fi + + # ── VMess outbound primitive (sing_box_cm_add_vmess_outbound) ────────── + local cm_lib="${NETSHIFT_LIB_DIR}/sing_box_config_manager.sh" + if [ ! -r "$cm_lib" ]; then + fail "sing_box_config_manager.sh not found" + return + fi + + local cm_tmp="/tmp/test-cm-vmess-$$.sh" + cat > "$cm_tmp" << 'CMEOF' +. "CM_LIB_PATH" + +base_config='{"outbounds":[]}' + +# Default security ("auto") + alter_id omitted when "0". +out=$(sing_box_cm_add_vmess_outbound "$base_config" "vmess-out" "example.com" "443" \ + "bf000d23-0752-40b4-affe-68f7707a9661" "" "0") +echo "$out" | jq -e '.outbounds[0].type == "vmess"' >/dev/null 2>&1 && echo 'cm-vmess-type:OK' || echo 'cm-vmess-type:FAIL' +echo "$out" | jq -e '.outbounds[0].server == "example.com"' >/dev/null 2>&1 && echo 'cm-vmess-server:OK' || echo 'cm-vmess-server:FAIL' +echo "$out" | jq -e '.outbounds[0].server_port == 443' >/dev/null 2>&1 && echo 'cm-vmess-port:OK' || echo 'cm-vmess-port:FAIL' +echo "$out" | jq -e '.outbounds[0].uuid == "bf000d23-0752-40b4-affe-68f7707a9661"' >/dev/null 2>&1 && echo 'cm-vmess-uuid:OK' || echo 'cm-vmess-uuid:FAIL' +echo "$out" | jq -e '.outbounds[0].security == "auto"' >/dev/null 2>&1 && echo 'cm-vmess-security-default:OK' || echo 'cm-vmess-security-default:FAIL' +echo "$out" | jq -e '.outbounds[0] | has("alter_id") | not' >/dev/null 2>&1 && echo 'cm-vmess-aid-omitted:OK' || echo 'cm-vmess-aid-omitted:FAIL' + +# Explicit security + non-zero alter_id present as a number. +out2=$(sing_box_cm_add_vmess_outbound "$base_config" "vmess-out" "example.com" "443" \ + "bf000d23-0752-40b4-affe-68f7707a9661" "aes-128-gcm" "64") +echo "$out2" | jq -e '.outbounds[0].security == "aes-128-gcm"' >/dev/null 2>&1 && echo 'cm-vmess-security-explicit:OK' || echo 'cm-vmess-security-explicit:FAIL' +echo "$out2" | jq -e '.outbounds[0].alter_id == 64' >/dev/null 2>&1 && echo 'cm-vmess-aid-number:OK' || echo 'cm-vmess-aid-number:FAIL' + +echo 'DONE' +CMEOF + sed -i "s|CM_LIB_PATH|$cm_lib|" "$cm_tmp" + + sh "$cm_tmp" 2>&1 | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + *:SKIP) skip "$line" ;; + DONE) ;; + *) ;; + esac + done + rm -f "$cm_tmp" } # ───────────────────────────────────────────────────────────────── From e293bf5e14c3afd5cdde0caff2c8427992d47e6e Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Fri, 5 Jun 2026 08:33:54 +0300 Subject: [PATCH 45/75] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=B0=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=84=D1=80=D0=BE=D0=BD=D1=82=D0=B5=D0=BD=D0=B4=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BA=D0=B5=20=D1=8F=D0=B4=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 65 +++ .../memory/shell-backend-developer.md | 58 +++ netshift/files/usr/bin/netshift | 10 +- netshift/files/usr/lib/updater.sh | 430 ++++++++++++++++++ tests/docker-compose.yml | 4 +- tests/entrypoint.sh | 251 +++++++++- 6 files changed, 812 insertions(+), 6 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index ea975797..d3168dd3 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -86,6 +86,71 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> `xray_json_count_unsupported`) and dialerProxy-chained outbounds; dedups on the connection part. No-regex jq + busybox-safe sed pre-gate. +## Core-switch (sing-box <-> extended) failure — DIAGNOSED on real hardware 2026-06 + +- SYMPTOM: switching stock->extended fails; on the router the new ~79MB binary + sits at /usr/bin/sing-box but with perms `rw-------` (NOT executable), the + tmpfs backup + downloaded archive remain, sing-box won't run. +- ROOT CAUSE: **rpcd timeout**. rpcd runs with `-t 30` (30s). The UI calls + `component_action sing_box install_extended` SYNCHRONOUSLY via LuCI fs.exec. + Download (~29MB over a slow/proxied link) + gzip extract of the 50MB binary + (measured **13s just for extract** on aarch64 cortex-a53) exceeds 30s, so rpcd + KILLS the process mid-flight — AFTER `tar -O > /usr/bin/sing-box` (file written + `rw-------` under the context umask 0077) but BEFORE `chmod 0755` + the + `LD_LIBRARY_PATH=/usr/lib sing-box version` validation. Hence the un-chmod'd + binary, leftover backup/archive, no cleanup. +- DISPROVEN earlier guesses: (a) NOT a disk-space issue (repro'd with free + space). (b) NOT the missing-LD_LIBRARY_PATH theory — the extended binary runs + `sing-box version` fine WITHOUT LD_LIBRARY_PATH (libcronet only needed at + runtime for naive); `chmod 0755` itself works under umask 0077. The code's + chmod/validate is correct; it just never gets to run. +- FIX DIRECTION (matches podkop-plus): make core-switch ASYNCHRONOUS — podkop-plus + has `component_action_async` (writes output to a file, forks the work) + + `component_action_status` (UI polls). NetShift's updater is synchronous and has + no async/status path. Port that model: fork the install, return immediately, + poll status; UI shows progress instead of hitting the 30s rpcd wall. +- Secondary hardening to fold in: chmod 0755 BEFORE validation is already there + but ordering/robustness should survive interruption; also rulesets in + /tmp/sing-box/rulesets were `rw-------` (umask 0077) — sing-box could still read + them as root, not the failure cause, but worth normalizing. +- Manual recovery that works: `chmod 0755 /usr/bin/sing-box` (the downloaded + extended binary is valid), `rm -rf /tmp/netshift-sbext.*`, restart netshift. +- Router access for testing: `ssh root@192.168.1.1` (no password). aarch64, + OpenWrt 24.10.5, overlay 60.9M (16.5M free), /tmp tmpfs 117M. scp does NOT work + (no sftp-server) — push scripts via `echo <base64> | base64 -d > f` over ssh. + +## Core-switch async fix (task-007) — on-device verified 2026-06; SECOND bug found + +- task-007 async model WORKS on real hardware: `component_action_async` returns + in 0s with a job_id (no more rpcd 30s kill), `component_action_status` polling + goes running->finished cleanly. The PRIMARY bug (synchronous timeout) is fixed. +- BUT live-testing exposed a SECOND, deeper bug in `updates_install_sing_box_stable` + (extended->stock): it has NO backup/rollback (unlike the extended path) AND the + whole switch happens while NetShift's nft tproxy + dnsmasq redirect are STILL + active. Sequence that bricked the router: + 1. install_stable removes/replaces the extended binary, then `opkg/apk install + sing-box` needs working internet — but the only internet was THROUGH the now + -dead VPN. opkg fails with "Operation not permitted" + DNS timeout (the nft + kill-switch sends marked traffic to a dead sing-box). + 2. Net result: /usr/bin/sing-box GONE, no rollback, router has no working core + and can't fetch one (extended path also fails: GitHub unreachable w/o VPN). +- This is a CLASSIC kill-switch deadlock: you can't download a new core because + the old core (that provided connectivity) is gone. +- RESCUE that works: `/etc/init.d/netshift stop` (tears down nft/dnsmasq so direct + internet returns) -> set a real resolver -> `opkg update && opkg install + sing-box` -> `/etc/init.d/netshift restart`. Verified: restored stock 1.12.22, + sing-box running. +- DESIGN IMPLICATION for the stable-rollback path (future task): before + install_stable, KEEP a backup of the current (extended) binary on tmpfs and + RESTORE it if the package install fails (so a failed downgrade never leaves the + router core-less) — mirror the extended path's backup/restore. Also consider + tearing down the redirect (or a temporary direct route) during a core swap so + the package manager can reach the feeds. The extended->stock path fundamentally + needs connectivity that the dead VPN may have been providing. +- Router note: stock sing-box install also drops `/etc/config/sing-box-opkg` and + `/etc/sing-box/config.json-opkg` (conffile conflicts) — harmless, NetShift owns + its own config path. + ## sing-box-extended capability map (researched 2026-06) - NetShift ALREADY installs sing-box-extended: `updater.sh` pulls diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 92a78fe8..a2411ee8 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -129,3 +129,61 @@ findings; keep under ~200 lines. - `test_syntax` in `tests/entrypoint.sh` now also `ash -n`'s `usr/bin/netshift` and asserts no residual `рџ`/`в”`/`вЂ` markers (built via `printf` octal, since busybox grep lacks `\x`). Guards against re-introducing the mojibake. + +## task-007: async component-action job state (rpcd 30s wall fix) + +- Root cause of "core switch fails": the UI called `component_action sing_box + install_extended` SYNCHRONOUSLY via rpcd `fs.exec`; rpcd has `-t 30` and kills + the worker mid-extract (after `tar -O > /usr/bin/sing-box`, before + `chmod 0755`). The JS-side `timeout: 600000` does NOT help (server-side limit). + Fix = fork the worker detached; return a job_id in <<30s; poll status. +- Job-state machinery lives in `updater.sh` (jq, no ucode — podkop-plus uses + `json_utils_ucode` which we don't have). State dir `/var/run/netshift/ + component-actions` (tmpfs). Constants: `UPDATES_JOB_DIR`, + `UPDATES_JOB_FINISHED_TTL_MINUTES=60`, `UPDATES_JOB_ORPHAN_OUTPUT_TTL_MINUTES=60`, + `UPDATES_JOB_STALE_GRACE_SECONDS=15`. +- **State object contract (STABLE — frontend task-008 depends on these field + names):** `{ success, running, component, action, message, pid, started_at, + updated_at, exit_code, version, latest_version }`. running: + `running:true,success:true,exit_code:null`. finished: `running:false`, + success/version/message parsed from the worker stdout JSON, exit_code from `$?`. +- HUP-proof fork: `( trap '' HUP; "$0" component_action "$c" "$a" >"$out" 2>&1; + updates_write_finished_job_state ... "$?" "$out" ) >/dev/null 2>&1 &`; record + `$!` into the running state via `updates_update_running_job_pid`. `trap '' HUP` + is what survives the rpcd session close. The async wrapper NEVER `exit 1`s on a + worker failure — the failure is recorded in the finished state. +- finished-state stdout parser (`updates_extract_worker_json`): `updates_log`/ + `echolog` can pollute the worker's stdout, so: (1) if the WHOLE file is valid + JSON (`jq -e .`) use it; else (2) `sed -n 's/^[^{]*\({.*\)$/\1/p' | tail -n 1` + then `jq -e` validate. sed is busybox-safe; NO Oniguruma. success derives from + `$w.success // ($exit_code == 0)`; version from `$w.version // $w.current_version`. +- Path-traversal guard: `updates_job_state_path` rejects ids matching + `*[!A-Za-z0-9._-]*` or empty/`.`/`..` → return 1. The id comes straight from + the (ACL-gated) UI, so this is the security boundary. `component_action_status` + returns a safe self-contained `{success:false,running:false,...}` (via + `updates_job_status_response`, non-zero rc) for invalid id / missing file. +- Stale detection (`updates_refresh_running_job_state`): running:true but pid not + `kill -0` alive AND past `started_at + STALE_GRACE` → rewrite as finished/stale + (`success:false`). Prevents the UI polling a crashed worker forever. +- Idempotent install (Req 4): at the START of `updates_install_sing_box_extended`, + if `/usr/bin/sing-box` exists but is not `-x` OR fails a `version` probe, `rm` + it up front (don't back up a broken partial artifact). `chmod 0755` stays + IMMEDIATELY after stream-extract and BEFORE validation — keep that order. +- **`set -e` + command substitution landmine (smoke harness):** under `set -e`, + `x="$(cmd-that-returns-nonzero)"` ABORTS the whole script. When a test + deliberately invokes a failing command (e.g. invalid-id status returns rc 1), + run it as `cmd > tmpfile 2>/dev/null || rc=$?` then read tmpfile — do NOT + capture via `$(...)` in an assignment, and do NOT use `|| true` (that clobbers + `$?` so you can't assert the non-zero rc). This cost me one debug cycle. +- **Brace-in-default-param landmine (busybox ash):** `${VAR:-{json...}}` emits an + EXTRA literal `}` even when VAR is set (the inner `{...}` confuses the `}` + matching), corrupting JSON. Use `[ -z "$VAR" ] && VAR='{...}'` then print `$VAR`. +- New top-level smoke test `test_jobstate` (alias `jobstate`): stubs the worker + via a tiny generated CLI whose `$0` IS the stub (because `component_action_async` + forks `"$0" component_action ...`); controls the worker with `STUB_JSON`/ + `STUB_SLEEP`/`STUB_RC` env; isolates state under `JOBSTUB_DIR`. Registered in + `all)`, case alias, usage line, and the docker-compose comment. +- Dispatcher (`bin/netshift`): `component_action_async) component_action_async + "$2" "$3" ;;` and `component_action_status) component_action_status "$2" ;;` + replaced the old naive `"$0" component_action ... > /tmp/...json &` hack. ACL + needs NO change (`/usr/bin/netshift` is exec-allowed wholesale). diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index ac16aecf..963e7bc6 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -3791,7 +3791,9 @@ Available commands: global_check Run global system check component_action Run component action: <component> <action> (e.g. sing_box install_extended|install_stable|check_update) - component_action_async Run component_action in background, returns job file path + component_action_async Start component_action in background; echoes a job_id + (use with component_action_status to poll the outcome) + component_action_status Report an async component action by job_id: <job_id> EOF } @@ -3875,8 +3877,10 @@ component_action) component_action "$2" "$3" ;; component_action_async) - "$0" component_action "$2" "$3" > "/tmp/netshift-component-$$.json" 2>&1 & - echo "{\"success\":true,\"job\":\"/tmp/netshift-component-$$.json\"}" + component_action_async "$2" "$3" + ;; +component_action_status) + component_action_status "$2" ;; *) show_help diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 7be49d5a..7c5644f5 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -7,6 +7,19 @@ SB_EXT_ARCH_SUFFIX="" UPDATES_SING_BOX_EXTENDED_REPO="shtorm-7/sing-box-extended" +# Async component-action job state. State lives on tmpfs (/var/run): it survives +# the rpcd call that started the worker but is intentionally transient (cleared +# on reboot — a reboot mid-job simply loses the job, which is acceptable since +# the install either already landed on disk or will be redone). +UPDATES_JOB_DIR="/var/run/netshift/component-actions" +# Finished state/.out files older than this are garbage-collected (minutes). +UPDATES_JOB_FINISHED_TTL_MINUTES=60 +# Orphaned worker .out files older than this are reaped (minutes). +UPDATES_JOB_ORPHAN_OUTPUT_TTL_MINUTES=60 +# Grace window after start before a running job whose pid is dead is declared +# stale (seconds) — covers the race between fork and the pid being recorded. +UPDATES_JOB_STALE_GRACE_SECONDS=15 + updates_log() { local message="$1" local level="${2:-info}" @@ -14,6 +27,409 @@ updates_log() { log "Updater: $message" "$level" } +# ── Async component-action job state (jq, atomic) ─────────────────── +# +# The UI starts long-running component actions (e.g. switching the sing-box +# core) via `component_action_async`, which forks the real worker +# (`component_action`) into a detached background process and returns a job_id +# immediately — staying well under the rpcd 30s call timeout. The UI then polls +# `component_action_status <job_id>`. State is small JSON objects written +# atomically (`*.tmp.$$` + mv) and built with jq `--arg`/`--argjson` only (no +# Oniguruma anywhere). +# +# State object contract (STABLE — consumed by the frontend, task-008): +# { success, running, component, action, message, pid, +# started_at, updated_at, exit_code, version, latest_version } +# * running state : running:true, success:true, exit_code:null +# * finished state: running:false, success/version/message parsed from the +# worker's captured stdout JSON, exit_code from the worker's $?. + +# Echoes the on-disk state path for a job id, or returns 1 for an unsafe id. +# Rejecting anything outside [A-Za-z0-9._-] (and empty/./..) prevents path +# traversal — the id reaches us straight from the (ACL-gated) UI. +updates_job_state_path() { + local job_id="$1" + + case "$job_id" in + "" | "." | "..") return 1 ;; + *[!A-Za-z0-9._-]*) return 1 ;; + esac + + printf '%s/%s.json\n' "$UPDATES_JOB_DIR" "$job_id" +} + +# Emits a small {"success","job_id","message"} response for the async call. +updates_job_json_response() { + local success="$1" + local job_id="$2" + local message="${3:-}" + + jq -nc \ + --argjson success "$success" \ + --arg job_id "$job_id" \ + --arg message "$message" \ + '{success: $success, job_id: $job_id, message: $message}' +} + +# Emits a self-contained status object (used for invalid-id / not-found / error +# replies that have no state file to cat). +updates_job_status_response() { + local success="$1" + local running="$2" + local message="$3" + + jq -nc \ + --argjson success "$success" \ + --argjson running "$running" \ + --arg message "$message" \ + '{success: $success, running: $running, component: "sing_box", + action: "", message: $message, pid: null, started_at: 0, + updated_at: 0, exit_code: null, version: "", latest_version: ""}' +} + +# Returns a monotonic-ish wall clock as an integer (0 on failure). +updates_now_seconds() { + local now + + now="$(date +%s 2>/dev/null)" + case "$now" in + "" | *[!0-9]*) now=0 ;; + esac + printf '%s\n' "$now" +} + +# Writes the "running" state for a job. pid may be empty (recorded as null and +# patched in later once the worker is forked). +updates_write_running_job_state() { + local state_file="$1" + local component="$2" + local action="$3" + local pid="${4:-}" + local tmp_file started_at pid_json rc + + mkdir -p "$UPDATES_JOB_DIR" || return 1 + started_at="$(updates_now_seconds)" + tmp_file="${state_file}.tmp.$$" + + case "$pid" in + "" | *[!0-9]*) pid_json="null" ;; + *) pid_json="$pid" ;; + esac + + jq -nc \ + --arg component "$component" \ + --arg action "$action" \ + --argjson pid "$pid_json" \ + --argjson started_at "$started_at" \ + '{success: true, running: true, component: $component, + action: $action, message: "Component action is running", + pid: $pid, started_at: $started_at, updated_at: $started_at, + exit_code: null, version: "", latest_version: ""}' \ + >"$tmp_file" && mv "$tmp_file" "$state_file" + rc=$? + + rm -f "$tmp_file" 2>/dev/null + return $rc +} + +# Patches the pid into an existing running state file. +updates_update_running_job_pid() { + local state_file="$1" + local pid="$2" + local tmp_file rc + + case "$pid" in + "" | *[!0-9]*) return 1 ;; + esac + + [ -f "$state_file" ] || return 1 + tmp_file="${state_file}.tmp.$$" + + jq -c \ + --argjson pid "$pid" \ + '.pid = $pid' \ + "$state_file" >"$tmp_file" && mv "$tmp_file" "$state_file" + rc=$? + + rm -f "$tmp_file" 2>/dev/null + return $rc +} + +# Rewrites a running state file as a failed/stale finished state. +updates_mark_stale_job_state() { + local state_file="$1" + local tmp_file updated_at rc + + [ -f "$state_file" ] || return 1 + updated_at="$(updates_now_seconds)" + tmp_file="${state_file}.tmp.$$" + + jq -c \ + --argjson updated_at "$updated_at" \ + '. + {success: false, running: false, + message: "Component action worker is no longer running", + updated_at: $updated_at, + exit_code: (if (.exit_code == null) then -1 else .exit_code end)}' \ + "$state_file" >"$tmp_file" && mv "$tmp_file" "$state_file" + rc=$? + + rm -f "$tmp_file" 2>/dev/null + return $rc +} + +# 0 if the recorded start time is still inside the stale grace window. +updates_started_at_is_within_stale_grace() { + local started_at="$1" + local now age + + case "$started_at" in + "" | *[!0-9]*) return 1 ;; + esac + [ "$started_at" -gt 0 ] || return 1 + + now="$(updates_now_seconds)" + [ "$now" -gt 0 ] || return 1 + + age=$((now - started_at)) + [ "$age" -lt "$UPDATES_JOB_STALE_GRACE_SECONDS" ] +} + +# 0 if the state file is currently flagged running:true. +updates_job_state_is_running() { + local state_file="$1" + + [ -f "$state_file" ] || return 1 + jq -e '.running == true' "$state_file" >/dev/null 2>&1 +} + +# If a job claims running:true but its pid is gone (past the grace window), +# rewrite it as a stale finished state so the UI never polls a dead worker +# forever. +updates_refresh_running_job_state() { + local state_file="$1" + local pid started_at + + updates_job_state_is_running "$state_file" || return 0 + + pid="$(jq -r '.pid // ""' "$state_file" 2>/dev/null)" + started_at="$(jq -r '.started_at // 0' "$state_file" 2>/dev/null)" + + case "$pid" in + "" | *[!0-9]*) + updates_started_at_is_within_stale_grace "$started_at" && return 0 + updates_mark_stale_job_state "$state_file" + return 0 + ;; + esac + + if kill -0 "$pid" 2>/dev/null; then + return 0 + fi + + updates_started_at_is_within_stale_grace "$started_at" && return 0 + # Re-check under the (rare) race where the worker finished and rewrote the + # state between our running check and here. + updates_job_state_is_running "$state_file" || return 0 + updates_mark_stale_job_state "$state_file" +} + +# Garbage-collects old job artifacts. Never removes a still-running job. +updates_cleanup_component_jobs() { + local output_file state_file + + [ -d "$UPDATES_JOB_DIR" ] || return 0 + + # Reap orphan worker outputs whose state is finished (or missing). + find "$UPDATES_JOB_DIR" -type f -name '*.out' -mmin "+$UPDATES_JOB_ORPHAN_OUTPUT_TTL_MINUTES" 2>/dev/null | + while IFS= read -r output_file; do + [ -f "$output_file" ] || continue + state_file="${output_file%.out}.json" + if [ -f "$state_file" ]; then + updates_refresh_running_job_state "$state_file" + if updates_job_state_is_running "$state_file"; then + continue + fi + fi + rm -f "$output_file" 2>/dev/null || true + done + + # Remove old finished state files (running ones are kept). + find "$UPDATES_JOB_DIR" -type f -name '*.json' -mmin "+$UPDATES_JOB_FINISHED_TTL_MINUTES" 2>/dev/null | + while IFS= read -r state_file; do + [ -f "$state_file" ] || continue + updates_refresh_running_job_state "$state_file" + updates_job_state_is_running "$state_file" && continue + rm -f "$state_file" 2>/dev/null || true + done +} + +# Extracts the LAST well-formed JSON object from the worker's captured stdout +# into $dest. The worker echoes one JSON object, but updates_log/echolog may +# also have written plain log lines to the same stream, so: +# 1. if the WHOLE file is valid JSON, use it; +# 2. else fall back to the last line that, after stripping any leading +# non-`{` prefix, parses as a JSON object. +# busybox-safe sed, jq for validation — NO Oniguruma. +updates_extract_worker_json() { + local output_file="$1" + local dest="$2" + + [ -s "$output_file" ] || return 1 + + if jq -e . "$output_file" >/dev/null 2>&1; then + cp "$output_file" "$dest" 2>/dev/null || return 1 + return 0 + fi + + sed -n 's/^[^{]*\({.*\)$/\1/p' "$output_file" 2>/dev/null | tail -n 1 >"$dest" + if [ -s "$dest" ] && jq -e . "$dest" >/dev/null 2>&1; then + return 0 + fi + + rm -f "$dest" 2>/dev/null + return 1 +} + +# Builds the finished state from the worker's captured stdout + its exit code. +updates_write_finished_job_state() { + local state_file="$1" + local component="$2" + local action="$3" + local exit_code="$4" + local output_file="$5" + local tmp_file json_file updated_at raw_output rc + + updated_at="$(updates_now_seconds)" + tmp_file="${state_file}.tmp.$$" + json_file="${output_file}.json" + + case "$exit_code" in + "" | *[!0-9]*) exit_code=1 ;; + esac + + if updates_extract_worker_json "$output_file" "$json_file"; then + # Worker JSON shape: {success, message?, version?, current_version?, + # latest_version?, status?}. Surface what is present; fall back + # sensibly. success also derives from a zero exit code if the worker + # JSON omitted it. + jq -nc \ + --slurpfile worker "$json_file" \ + --arg component "$component" \ + --arg action "$action" \ + --argjson exit_code "$exit_code" \ + --argjson updated_at "$updated_at" \ + '($worker[0]) as $w + | {success: ($w.success // ($exit_code == 0)), + running: false, + component: $component, + action: $action, + message: ($w.message // ""), + pid: null, + started_at: 0, + updated_at: $updated_at, + exit_code: $exit_code, + version: ($w.version // $w.current_version // ""), + latest_version: ($w.latest_version // "")}' \ + >"$tmp_file" && mv "$tmp_file" "$state_file" + rc=$? + rm -f "$tmp_file" "$json_file" "$output_file" 2>/dev/null + return $rc + fi + rm -f "$json_file" 2>/dev/null + + # No parseable worker JSON: record a generic failure, surfacing a trimmed + # snippet of whatever the worker printed. + raw_output="$(tr '\n' ' ' <"$output_file" 2>/dev/null | cut -c1-240)" + [ -n "$raw_output" ] || raw_output="Component action failed" + + jq -nc \ + --arg component "$component" \ + --arg action "$action" \ + --arg message "$raw_output" \ + --argjson exit_code "$exit_code" \ + --argjson updated_at "$updated_at" \ + '{success: false, running: false, component: $component, + action: $action, message: $message, pid: null, started_at: 0, + updated_at: $updated_at, exit_code: $exit_code, version: "", + latest_version: ""}' \ + >"$tmp_file" && mv "$tmp_file" "$state_file" + rc=$? + + rm -f "$tmp_file" "$output_file" 2>/dev/null + return $rc +} + +# Starts `component_action` in a detached, HUP-proof background process and +# returns a job_id immediately. Never `exit 1`s on a worker failure — the +# worker's outcome is captured into the finished state for polling. +component_action_async() { + local component="$1" + local action="$2" + local job_id state_file output_file job_pid + + if ! mkdir -p "$UPDATES_JOB_DIR"; then + updates_job_json_response false "" "Failed to create component action state directory" + return 1 + fi + + updates_cleanup_component_jobs + + job_id="$(updates_now_seconds)-$$" + state_file="$(updates_job_state_path "$job_id")" || { + updates_job_json_response false "" "Failed to prepare component action job" + return 1 + } + output_file="$UPDATES_JOB_DIR/$job_id.out" + + if ! updates_write_running_job_state "$state_file" "$component" "$action"; then + updates_job_json_response false "" "Failed to write component action state" + return 1 + fi + + # Detached + HUP-proof: trap '' HUP so the rpcd session close (SIGHUP on the + # process group) does not kill the worker. The worker's single JSON object + # is captured to $output_file; on completion we transcribe it (+ exit code) + # into the finished state. + ( + trap '' HUP + "$0" component_action "$component" "$action" >"$output_file" 2>&1 + updates_write_finished_job_state "$state_file" "$component" "$action" "$?" "$output_file" + ) >/dev/null 2>&1 & + job_pid="$!" + + if ! updates_update_running_job_pid "$state_file" "$job_pid"; then + kill "$job_pid" 2>/dev/null || true + updates_job_json_response false "" "Failed to record component action worker pid" + return 1 + fi + + updates_job_json_response true "$job_id" "Component action started" + return 0 +} + +# Reports the status of an async component-action job by job_id. +component_action_status() { + local job_id="$1" + local state_file + + mkdir -p "$UPDATES_JOB_DIR" 2>/dev/null || true + updates_cleanup_component_jobs + + state_file="$(updates_job_state_path "$job_id")" || { + updates_job_status_response false false "Invalid component action job id" + return 1 + } + + if [ ! -f "$state_file" ]; then + updates_job_status_response false false "Component action job was not found" + return 1 + fi + + updates_refresh_running_job_state "$state_file" + cat "$state_file" + return 0 +} + # Returns 0 if the system uses musl libc. updates_system_uses_musl() { ls /lib/ld-musl-*.so* >/dev/null 2>&1 && return 0 @@ -233,6 +649,20 @@ updates_install_sing_box_extended() { local binary_path cronet_path local backup_binary="" backup_cronet="" new_version + # Interruption-tolerant heal: a run killed mid-flight (e.g. the old rpcd 30s + # timeout) could leave a non-executable /usr/bin/sing-box behind. Such a + # partial artifact must NOT be trusted (e.g. backed up as if it were a real + # binary) — the install below replaces it anyway, but we drop it up front so + # the tmpfs backup never preserves a broken binary and the version probe + # never reads garbage from it. + if [ -e /usr/bin/sing-box ] && { + [ ! -x /usr/bin/sing-box ] || + ! LD_LIBRARY_PATH=/usr/lib /usr/bin/sing-box version >/dev/null 2>&1 + }; then + updates_log "Found a non-runnable /usr/bin/sing-box (likely a partial install); discarding it before reinstall" "warn" + rm -f /usr/bin/sing-box + fi + if ! updates_resolve_sing_box_extended_arch_suffix; then updates_log "Unsupported architecture for sing-box-extended" "error" echo "{\"success\":false,\"message\":\"Unsupported architecture for sing-box-extended\"}" diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index fec02415..8c9d4dfc 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -8,8 +8,8 @@ # Run specific test: # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # -# Test names: all, deps, syntax, config, helpers, nft, -# dnsmasq, lifecycle, diagnostics, subscription +# Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, +# diagnostics, subscription, jobstate # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 2ec8e64e..e147dbc1 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1329,6 +1329,253 @@ FBEOF rm -f "$fb" } +# ───────────────────────────────────────────────────────────────── +# Test: Async component-action job state (updater.sh) +# ───────────────────────────────────────────────────────────────── +# Exercises the jq job-state machinery from updater.sh with a STUBBED worker +# (no network, no real download). A tiny stub CLI sources the real updater.sh, +# provides a trivial `log`, and lets the `component_action` worker be controlled +# by env vars (STUB_JSON / STUB_SLEEP / STUB_RC). component_action_async forks +# `"$0" component_action ...`, so $0 must be the stub CLI itself — hence the +# separate executable. All assertions are jq-validated; tokens are parsed with +# the same name:OK/FAIL convention as test_subscription. +test_jobstate() { + header "Async Component-Action Job State (updater.sh)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local updater="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$updater" ]; then + skip "updater.sh not found in ${NETSHIFT_LIB_DIR}" + return + fi + + local stub="/tmp/netshift-jobstub-$$" + cat > "$stub" << 'STUBEOF' +#!/bin/sh +# Minimal stand-in for /usr/bin/netshift that exposes the async job-state API. +log() { :; } +echolog() { :; } +nolog() { :; } +# Isolate state under a per-process tmpfs dir so parallel/old runs never clash. +UPDATES_JOB_DIR="${JOBSTUB_DIR:-/tmp/netshift-jobstub-state}" + +. "UPDATER_PATH" + +# Re-pin after sourcing (the source sets its own default). +UPDATES_JOB_DIR="${JOBSTUB_DIR:-/tmp/netshift-jobstub-state}" + +case "$1" in +component_action) + # Stubbed worker: emit a (possibly delayed) JSON object then exit STUB_RC. + [ -n "$STUB_SLEEP" ] && sleep "$STUB_SLEEP" + if [ -z "$STUB_JSON" ]; then + STUB_JSON='{"success":true,"version":"1.0.0-extended"}' + fi + printf '%s\n' "$STUB_JSON" + exit "${STUB_RC:-0}" + ;; +component_action_async) + component_action_async "$2" "$3" + ;; +component_action_status) + component_action_status "$2" + ;; +esac +STUBEOF + sed -i "s|UPDATER_PATH|$updater|g" "$stub" + chmod 0755 "$stub" + + local jdir="/tmp/netshift-jobstate-$$" + rm -rf "$jdir" + + # ── 1. async returns {success:true, job_id} fast; running state appears ── + local start_async end_async elapsed async_json job_id + start_async="$(date +%s)" + async_json="$(JOBSTUB_DIR="$jdir" STUB_SLEEP=2 STUB_JSON='{"success":true,"version":"1.7.0-extended"}' \ + "$stub" component_action_async sing_box install_extended)" + end_async="$(date +%s)" + elapsed=$((end_async - start_async)) + + if echo "$async_json" | jq -e '.success == true and (.job_id | length) > 0' > /dev/null 2>&1; then + pass "async returns success+job_id ($async_json)" + else + fail "async did not return success+job_id" "$async_json" + fi + if [ "$elapsed" -lt 5 ]; then + pass "async returned fast (${elapsed}s, well under 30s)" + else + fail "async too slow: ${elapsed}s" + fi + + job_id="$(echo "$async_json" | jq -r '.job_id')" + if [ -f "$jdir/$job_id.json" ]; then + pass "running state file created" + else + fail "running state file missing: $jdir/$job_id.json" + fi + # While the stub sleeps, the state must read running:true / success:true. + if jq -e '.running == true and .success == true and .exit_code == null' \ + "$jdir/$job_id.json" > /dev/null 2>&1; then + pass "running state has running:true,success:true,exit_code:null" + else + fail "running state shape wrong" "$(cat "$jdir/$job_id.json" 2>/dev/null)" + fi + # The recorded pid must be a live integer while running. + local running_pid + running_pid="$(jq -r '.pid' "$jdir/$job_id.json" 2>/dev/null)" + case "$running_pid" in + '' | *[!0-9]*) fail "running pid not an integer: '$running_pid'" ;; + *) pass "running pid recorded ($running_pid)" ;; + esac + + # ── 2. after the worker finishes, status reports the surfaced outcome ──── + # Wait for the background worker (stub sleeps 2s) to complete. + local waited=0 + while [ "$waited" -lt 15 ]; do + if jq -e '.running == false' "$jdir/$job_id.json" > /dev/null 2>&1; then + break + fi + sleep 1 + waited=$((waited + 1)) + done + + local status_json + status_json="$(JOBSTUB_DIR="$jdir" "$stub" component_action_status "$job_id")" + if echo "$status_json" | jq -e '.running == false and .success == true and .exit_code == 0 and .version == "1.7.0-extended"' > /dev/null 2>&1; then + pass "finished status surfaces success/version/exit_code" + else + fail "finished status wrong" "$status_json" + fi + + # ── 2b. a failing worker is recorded (success:false, non-zero exit) ────── + local fail_json fail_id fail_status + fail_json="$(JOBSTUB_DIR="$jdir" STUB_RC=3 STUB_JSON='{"success":false,"message":"boom"}' \ + "$stub" component_action_async sing_box install_extended)" + fail_id="$(echo "$fail_json" | jq -r '.job_id')" + waited=0 + while [ "$waited" -lt 15 ]; do + if jq -e '.running == false' "$jdir/$fail_id.json" > /dev/null 2>&1; then + break + fi + sleep 1 + waited=$((waited + 1)) + done + fail_status="$(JOBSTUB_DIR="$jdir" "$stub" component_action_status "$fail_id")" + if echo "$fail_status" | jq -e '.running == false and .success == false and .exit_code == 3 and .message == "boom"' > /dev/null 2>&1; then + pass "failed worker recorded (success:false, exit_code:3, message surfaced)" + else + fail "failed worker status wrong" "$fail_status" + fi + + # ── 2c. worker stdout polluted with log lines: last JSON object wins ───── + local noisy_json noisy_id noisy_status + noisy_json="$(JOBSTUB_DIR="$jdir" \ + STUB_JSON='Updater: some log line +another stray line {not-json} +{"success":true,"version":"9.9.9-extended"}' \ + "$stub" component_action_async sing_box install_extended)" + noisy_id="$(echo "$noisy_json" | jq -r '.job_id')" + waited=0 + while [ "$waited" -lt 15 ]; do + if jq -e '.running == false' "$jdir/$noisy_id.json" > /dev/null 2>&1; then + break + fi + sleep 1 + waited=$((waited + 1)) + done + noisy_status="$(JOBSTUB_DIR="$jdir" "$stub" component_action_status "$noisy_id")" + if echo "$noisy_status" | jq -e '.running == false and .success == true and .version == "9.9.9-extended"' > /dev/null 2>&1; then + pass "finished parser extracts the LAST well-formed JSON object from noisy stdout" + else + fail "noisy-stdout parse wrong" "$noisy_status" + fi + + # ── 3. invalid / traversal job ids are rejected safely ────────────────── + local bad bad_json bad_rc bad_out="/tmp/netshift-jobstate-bad-$$" + for bad in "../foo" "../../etc/passwd" "foo/bar" "a b" "" "."; do + bad_rc=0 + JOBSTUB_DIR="$jdir" "$stub" component_action_status "$bad" > "$bad_out" 2>/dev/null || bad_rc=$? + bad_json="$(cat "$bad_out" 2>/dev/null)" + if [ "$bad_rc" -ne 0 ] \ + && echo "$bad_json" | jq -e '.success == false and .running == false' > /dev/null 2>&1; then + pass "invalid job_id rejected safely: '$bad'" + else + fail "invalid job_id NOT rejected: '$bad'" "rc=$bad_rc json=$bad_json" + fi + done + rm -f "$bad_out" + # The validator must never resolve a traversal id to a path. + local fb_jobstate="/tmp/netshift-jobstate-validate-$$.sh" + cat > "$fb_jobstate" << 'VEOF' +log() { :; } +UPDATES_JOB_DIR="VDIR" +. "UPDATER_PATH" +UPDATES_JOB_DIR="VDIR" +if updates_job_state_path "../foo" >/dev/null 2>&1; then + echo 'jobstate-traversal-rejected:FAIL' +else + echo 'jobstate-traversal-rejected:OK' +fi +if updates_job_state_path "good-1.2_3" >/dev/null 2>&1; then + echo 'jobstate-valid-id-accepted:OK' +else + echo 'jobstate-valid-id-accepted:FAIL' +fi +VEOF + sed -i "s|UPDATER_PATH|$updater|g;s|VDIR|$jdir|g" "$fb_jobstate" + ash "$fb_jobstate" 2>/dev/null | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + esac + done + rm -f "$fb_jobstate" + + # ── 4. stale job: running:true with a dead pid past grace → finished ───── + local stale_dir="$jdir/stale" + mkdir -p "$stale_dir" + local stale_state="$stale_dir/staletest.json" + local stale_sh="/tmp/netshift-jobstate-stale-$$.sh" + cat > "$stale_sh" << 'SEOF' +log() { :; } +UPDATES_JOB_DIR="SDIR" +. "UPDATER_PATH" +UPDATES_JOB_DIR="SDIR" +state="SSTATE" +# Pick a pid that is certainly dead, and a started_at far in the past so we are +# well beyond the stale grace window. +dead_pid=999999 +while kill -0 "$dead_pid" 2>/dev/null; do + dead_pid=$((dead_pid + 1)) +done +old_started=$(( $(date +%s) - 3600 )) +jq -nc --argjson pid "$dead_pid" --argjson started "$old_started" \ + '{success:true,running:true,component:"sing_box",action:"install_extended", + message:"Component action is running",pid:$pid,started_at:$started, + updated_at:$started,exit_code:null,version:"",latest_version:""}' > "$state" +updates_refresh_running_job_state "$state" +if jq -e '.running == false and .success == false' "$state" >/dev/null 2>&1; then + echo 'jobstate-stale-marked-finished:OK' +else + echo 'jobstate-stale-marked-finished:FAIL' +fi +SEOF + sed -i "s|UPDATER_PATH|$updater|g;s|SDIR|$stale_dir|g;s|SSTATE|$stale_state|g" "$stale_sh" + ash "$stale_sh" 2>/dev/null | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + esac + done + rm -f "$stale_sh" + + rm -rf "$jdir" "$stub" +} + # ───────────────────────────────────────────────────────────────── # Main # ───────────────────────────────────────────────────────────────── @@ -1353,6 +1600,7 @@ main() { test_nft test_diagnostics test_subscription + test_jobstate ;; deps) test_deps ;; syntax) test_syntax ;; @@ -1361,12 +1609,13 @@ main() { nft) test_nft ;; diagnostics) test_diagnostics ;; subscription) test_subscription ;; + jobstate) test_jobstate ;; jq) test_jq_helpers ;; cm) test_config_manager ;; sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription" + echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription jobstate" exit 1 ;; esac From b9cd60221618ff7e7250b00912325e299dd8ac6e Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Fri, 5 Jun 2026 09:18:20 +0300 Subject: [PATCH 46/75] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=81=D0=B0=D0=BC=D0=BE=D0=B2=D0=BE=D1=81=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81?= =?UTF-8?q?=D0=B5=D1=82=D0=B8=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=20=D1=8F=D0=B4=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/agent-rules/memory/code-reviewer.md | 2 + .../memory/shell-backend-developer.md | 53 +++ netshift/files/usr/lib/constants.sh | 15 + netshift/files/usr/lib/updater.sh | 413 +++++++++++++++++- tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 336 +++++++++++++- 6 files changed, 804 insertions(+), 17 deletions(-) diff --git a/docs/agent-rules/memory/code-reviewer.md b/docs/agent-rules/memory/code-reviewer.md index e5a01c2d..78dfad1a 100644 --- a/docs/agent-rules/memory/code-reviewer.md +++ b/docs/agent-rules/memory/code-reviewer.md @@ -58,3 +58,5 @@ append recurring findings; keep under ~200 lines. - base64 share-link decode vs `sing_box_cf_add_proxy_outbound` `url_decode` (facade:65): the facade runs `url_decode` (+>space, %XX>byte) on the whole URL before the scheme case. Any case that base64-decodes the ENTIRE payload (vmess, future tuic/etc.) must use the RAW pre-url_decode link standard base64 contains '+'. The ss) case escapes this only because it decodes a short method:password userinfo. Beware synthetic test keys that avoid '+' masking this (false green). - For protocol validators that base64-decode a whole body (vmess, future tuic/etc.): the '+'-regression is real only if the dispatcher preserves '+'. validateProxyUrl only .trim()s, so '+' survives at the boundary a green direct-call '+' test is sufficient evidence; a dispatcher-level '+' assertion is the stronger guard. + +- Wrapper/core split for always-run cleanup (task-009 core-switch): verify the public wrapper captures core stdout to a temp file + rc, then UNCONDITIONALLY calls restore/cleanup before re-emitting JSON and return rc; confirm the worker runs without set -e (else a non-zero core rc could skip trailing cleanup) and that _*_core never exits. For never-end-core-less rollbacks, confirm the tmpfs backup happens BEFORE the package manager/extract touches the binary and is dropped ONLY on the confirmed-good path; strongest test deletes the live mock binary on simulated failure and asserts original bytes restored. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index a2411ee8..f5af8dc5 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -187,3 +187,56 @@ findings; keep under ~200 lines. "$2" "$3" ;;` and `component_action_status) component_action_status "$2" ;;` replaced the old naive `"$0" component_action ... > /tmp/...json &` hack. ACL needs NO change (`/usr/bin/netshift` is exec-allowed wholesale). + +## task-009: core-switch connectivity self-heal + rollback (anti-brick) + +- Root cause of the on-hardware brick: `updates_install_sing_box_stable` + removed/replaced /usr/bin/sing-box via opkg/apk with NO backup, while the + kill-switch (nft tproxy + dnsmasq->127.0.0.42->dead sing-box) blocked feed + access. Binary GONE, no rollback. Extended path already had a tmpfs backup. +- Fix shape (variant B): both install paths are now thin PUBLIC wrappers + (`updates_install_sing_box_extended`/`_stable`) that run + `updates_ensure_connectivity <dir>` (preflight; if fail -> selfheal) then call + the renamed private core (`_updates_install_sing_box_*_core`), then ALWAYS + `updates_restore_after_swap`. **Epilogue guarantee = single cleanup call**: + core echoes JSON to a `/tmp/...result.$$` capture file + returns rc; wrapper + runs restore once, re-emits the JSON, returns rc. No early return skips it (no + trap needed — the wrapper has exactly one core call). +- `updates_preflight_connectivity <stable|extended>` is direction-aware: stable + probes `UPDATES_FEED_PROBE_HOST` (downloads.openwrt.org), extended probes + `UPDATES_GITHUB_PROBE_HOST` (api.github.com). Probe = DNS resolve (dig + `+short`, nslookup fallback; bind-dig is a dep) AND a curl `-fsSI`/wget + `--spider` HEAD with `--connect-timeout 5`. No jq/regex. +- `updates_selfheal_connectivity`: (1) backup `/etc/resolv.conf` to tmpfs + (`UPDATES_RESOLV_BACKUP`), write temp resolver (`UPDATES_HEAL_RESOLVERS` + 1.1.1.1+9.9.9.9) atomically, recheck; (2) if still failing, tear down redirect + via the EXISTING `/etc/init.d/netshift stop` (dnsmasq_restore + stop_main), + recheck. Records `UPDATES_HEAL_RESOLV_REPLACED`/`UPDATES_HEAL_REDIRECT_DOWN` + module-level flags so the epilogue restores EXACTLY what changed (restore + resolv.conf via mv-back, bring redirect up via `/etc/init.d/netshift start`). + Reused stop/start so dnsmasq UCI + shutdown_correctly bookkeeping stays right; + NO hand-rolled nft flush, NO sacred-constant change. +- Stable core gained tmpfs backup/rollback (`updates_stable_rollback`) mirroring + the extended path: backup binary+libcronet BEFORE package install; restore on + install-fail OR still-extended validation. CRITICAL ordering: connectivity is + confirmed (preflight/heal in the wrapper) BEFORE the core touches the binary — + if heal fails the wrapper aborts and nothing is removed. +- **Testability indirection**: added `UPDATES_SING_BOX_BIN`/`UPDATES_LIBCRONET_LIB` + constants (default the real /usr/bin/sing-box, /usr/lib/libcronet.so) and used + them in the STABLE core+rollback only, so the smoke test can point them at + /tmp mocks without clobbering the container's real binary. Extended path still + uses the literals (spec said mirror, not refactor). +- New top-level smoke test `test_selfheal` (alias `selfheal`): a generated + driver sources updater.sh, re-pins RESOLV_CONF/probe-hosts/bin paths, stubs + dig/nslookup/curl/opkg via a PATH-prepended bin dir whose behaviour is keyed + off marker files, and installs a fake `/etc/init.d/netshift` that logs + stop/start/restart (absolute path can't be PATH-overridden — write+restore the + real one). 5 scenarios: preflight-pass, dns-heal, teardown-heal, heal-fail + (abort, binary intact), stable-install-fail (backup restored). Registered in + `all)`, case alias, usage line, docker-compose comment. +- **`set -e` landmine (again)**: the worker returns non-zero on recoverable + failures (success:false). Calling it directly inside a test under `set -e` + aborts the WHOLE suite mid-run (only the passes before it print, summary never + runs, rc=1 with no FAIL line). Wrap the invocation `... || true` — assertions + read JSON/file-state, not rc. (Distinct from the task-007 `$(...)`-capture + variant.) diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 243f232d..1d30daaa 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -38,6 +38,21 @@ NFT_OUTBOUND_MARK="0x00200000" ## sing-box SB_REQUIRED_VERSION="1.12.0" +# Core-switch connectivity self-heal (task-009). Hosts probed before a core +# swap, depending on direction: the stable (stock) install pulls from the +# OpenWrt package feeds, the extended install pulls from the GitHub API. +UPDATES_FEED_PROBE_HOST="downloads.openwrt.org" +UPDATES_GITHUB_PROBE_HOST="api.github.com" +# Temporary public resolvers written to /etc/resolv.conf when DNS healing is +# needed (the user's upstream may itself be the now-dead VPN). +UPDATES_HEAL_RESOLVERS="1.1.1.1 9.9.9.9" +# tmpfs backup path for the original /etc/resolv.conf during a heal. +UPDATES_RESOLV_BACKUP="/tmp/netshift-resolv.conf.bak" +# Installed core paths (indirected so the stable backup/rollback path is unit +# testable without clobbering the real binary). These are the real on-device +# locations; tests override them. +UPDATES_SING_BOX_BIN="/usr/bin/sing-box" +UPDATES_LIBCRONET_LIB="/usr/lib/libcronet.so" # DNS SB_DNS_SERVER_TAG="dns-server" SB_FAKEIP_DNS_SERVER_TAG="fakeip-server" diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 7c5644f5..903e1907 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -632,6 +632,276 @@ updates_restart_netshift() { fi } +# ── Core-switch connectivity self-heal + restore (task-009) ───────── +# +# Switching the core needs working internet (package feeds for the stable +# install, the GitHub API for the extended install). On the operator's router +# the only egress was THROUGH the now-dead VPN, so the swap deadlocked behind +# NetShift's own kill-switch (nft tproxy + dnsmasq -> dead sing-box) and bricked +# the box: the stock binary was removed with no way to fetch a replacement. +# +# The fix encodes the manual rescue as self-healing with active connectivity +# repair: pre-flight a connectivity probe; if it fails, heal (a working +# temporary resolver, then tear down the redirect via the EXISTING +# `/etc/init.d/netshift stop`) and re-check; only swap once a feed is reachable; +# ALWAYS restore the original resolv.conf and the redirect afterwards. +# +# What the heal changed is recorded in two module-level flags so the restore +# epilogue touches back EXACTLY what was changed (and nothing leaks): +# UPDATES_HEAL_RESOLV_REPLACED=1 -> /etc/resolv.conf was overwritten +# UPDATES_HEAL_REDIRECT_DOWN=1 -> the NetShift redirect was torn down +UPDATES_HEAL_RESOLV_REPLACED=0 +UPDATES_HEAL_REDIRECT_DOWN=0 + +# Resolves the probe host for a swap direction. +# stable -> OpenWrt package feeds host +# extended -> GitHub API host +updates_preflight_host_for_direction() { + local direction="$1" + + case "$direction" in + stable) printf '%s\n' "$UPDATES_FEED_PROBE_HOST" ;; + extended) printf '%s\n' "$UPDATES_GITHUB_PROBE_HOST" ;; + *) return 1 ;; + esac +} + +# Returns 0 if a DNS lookup of $host resolves, using bind-dig (a dependency) +# with an nslookup fallback. Small timeouts; logs the outcome. +updates_dns_resolves() { + local host="$1" + + if command -v dig >/dev/null 2>&1; then + if dig +time=3 +tries=1 +short "$host" 2>/dev/null | grep -q '[0-9a-fA-F]'; then + return 0 + fi + return 1 + fi + + if command -v nslookup >/dev/null 2>&1; then + # busybox nslookup: any "Address" line beyond the server line means a + # successful resolution. Avoid jq/regex; plain grep is fine here. + if nslookup "$host" 2>/dev/null | grep -q 'Address'; then + return 0 + fi + return 1 + fi + + return 1 +} + +# Returns 0 if an HTTPS reachability check to $host succeeds within a short +# connect timeout (curl HEAD --fail, wget --spider fallback). +updates_host_reachable() { + local host="$1" + local url="https://$host" + + if command -v curl >/dev/null 2>&1; then + curl --connect-timeout 5 -m 8 -fsSI -A "netshift-updater" "$url" >/dev/null 2>&1 && return 0 + return 1 + fi + + if command -v wget >/dev/null 2>&1; then + wget -T 8 -q --spider "$url" >/dev/null 2>&1 && return 0 + return 1 + fi + + return 1 +} + +# Direction-aware connectivity pre-flight. Returns 0 if the host needed for the +# CURRENT swap direction is both resolvable AND reachable, non-zero otherwise. +# Logs each probe so the outcome is visible via the job message / syslog. +updates_preflight_connectivity() { + local direction="$1" + local host + + host="$(updates_preflight_host_for_direction "$direction")" || { + updates_log "Connectivity pre-flight: unknown direction '$direction'" "error" + return 1 + } + + if ! updates_dns_resolves "$host"; then + updates_log "Connectivity pre-flight: DNS resolve of $host FAILED" "warn" + return 1 + fi + updates_log "Connectivity pre-flight: DNS resolve of $host ok" + + if ! updates_host_reachable "$host"; then + updates_log "Connectivity pre-flight: HTTPS reachability of $host FAILED" "warn" + return 1 + fi + updates_log "Connectivity pre-flight: HTTPS reachability of $host ok" + + return 0 +} + +# Writes a temporary working resolver to /etc/resolv.conf, backing up the +# original to tmpfs first. Records UPDATES_HEAL_RESOLV_REPLACED so the epilogue +# restores it. Atomic write (*.tmp.$$ + mv). +updates_write_temp_resolver() { + local resolver tmp_file + + # Back up the original exactly once. + if [ "$UPDATES_HEAL_RESOLV_REPLACED" -eq 0 ]; then + if [ -e "$RESOLV_CONF" ]; then + cp -p "$RESOLV_CONF" "$UPDATES_RESOLV_BACKUP" 2>/dev/null || true + else + # No original to restore; mark the backup absent so the epilogue + # removes the temp file rather than restoring a phantom. + rm -f "$UPDATES_RESOLV_BACKUP" 2>/dev/null || true + fi + fi + + tmp_file="${RESOLV_CONF}.netshift.tmp.$$" + : >"$tmp_file" 2>/dev/null || return 1 + for resolver in $UPDATES_HEAL_RESOLVERS; do + printf 'nameserver %s\n' "$resolver" >>"$tmp_file" 2>/dev/null || { + rm -f "$tmp_file" 2>/dev/null + return 1 + } + done + + if mv -f "$tmp_file" "$RESOLV_CONF" 2>/dev/null; then + UPDATES_HEAL_RESOLV_REPLACED=1 + updates_log "Self-heal: wrote temporary resolver ($UPDATES_HEAL_RESOLVERS) to $RESOLV_CONF" + return 0 + fi + + rm -f "$tmp_file" 2>/dev/null + return 1 +} + +# Tears down the NetShift redirect (kill-switch) by invoking the EXISTING +# `/etc/init.d/netshift stop` — this runs dnsmasq_restore + stop_main (nft +# table delete + ip rule/route flush + sing-box stop) and flips +# shutdown_correctly so the dnsmasq UCI bookkeeping stays consistent. Records +# UPDATES_HEAL_REDIRECT_DOWN so the epilogue brings it back. +updates_teardown_redirect() { + if [ ! -x /etc/init.d/netshift ]; then + updates_log "Self-heal: /etc/init.d/netshift not present; cannot tear down redirect" "warn" + return 1 + fi + + updates_log "Self-heal: tearing down the NetShift redirect via /etc/init.d/netshift stop" + /etc/init.d/netshift stop >/dev/null 2>&1 || true + UPDATES_HEAL_REDIRECT_DOWN=1 + return 0 +} + +# Variant B self-heal — only invoked when pre-flight fails. Reversible steps, +# each logged and each recorded in UPDATES_HEAL_* so the epilogue restores +# precisely what was touched: +# 1. temp resolver -> re-check +# 2. still failing -> tear down the redirect -> re-check +# Returns 0 when connectivity is restored, non-zero when healing failed. +updates_selfheal_connectivity() { + local direction="$1" + + updates_log "Connectivity pre-flight failed; attempting self-heal (variant B)" "warn" + + # Step 1: temporary resolver, then re-check. + if updates_write_temp_resolver; then + if updates_preflight_connectivity "$direction"; then + updates_log "Self-heal: connectivity restored by temporary resolver (dns_healed)" + return 0 + fi + else + updates_log "Self-heal: failed to write temporary resolver" "warn" + fi + + # Step 2: tear down the redirect (kill-switch), then re-check. + if updates_teardown_redirect; then + if updates_preflight_connectivity "$direction"; then + updates_log "Self-heal: connectivity restored after redirect teardown (redirect_down)" + return 0 + fi + fi + + updates_log "Self-heal: connectivity could NOT be restored" "error" + return 1 +} + +# Restore epilogue — MUST run on EVERY exit path of an install (success, install +# failure, heal failure). Restores exactly what the heal changed: +# * resolv.conf replaced -> restore the backed-up original (or drop the temp +# file if there was no original); +# * redirect torn down -> bring NetShift back up via `/etc/init.d/netshift +# start` so nft/dnsmasq/routing + shutdown_correctly are reinstated. +# Idempotent: clears the flags so a second call is a no-op. +updates_restore_after_swap() { + if [ "$UPDATES_HEAL_RESOLV_REPLACED" -eq 1 ]; then + if [ -e "$UPDATES_RESOLV_BACKUP" ]; then + if mv -f "$UPDATES_RESOLV_BACKUP" "$RESOLV_CONF" 2>/dev/null; then + updates_log "Restore: original $RESOLV_CONF reinstated" + else + updates_log "Restore: failed to reinstate original $RESOLV_CONF" "warn" + fi + else + # No original existed: remove our temporary resolver. + rm -f "$RESOLV_CONF" 2>/dev/null || true + updates_log "Restore: removed temporary $RESOLV_CONF (no original to restore)" + fi + UPDATES_HEAL_RESOLV_REPLACED=0 + fi + + if [ "$UPDATES_HEAL_REDIRECT_DOWN" -eq 1 ]; then + if [ -x /etc/init.d/netshift ]; then + updates_log "Restore: bringing the NetShift redirect back up via /etc/init.d/netshift start" + /etc/init.d/netshift start >/dev/null 2>&1 || true + fi + UPDATES_HEAL_REDIRECT_DOWN=0 + fi +} + +# Runs pre-flight for a direction and, on failure, the self-heal. Returns 0 when +# connectivity is confirmed (possibly after healing), non-zero when it could not +# be established. Callers MUST run updates_restore_after_swap on every exit path +# regardless of this function's result. +updates_ensure_connectivity() { + local direction="$1" + + if updates_preflight_connectivity "$direction"; then + return 0 + fi + + updates_selfheal_connectivity "$direction" +} + +# Public entry: install sing-box-extended with the connectivity self-heal +# preamble + the always-run restore epilogue around the real worker. +# +# The epilogue is guaranteed via a SINGLE cleanup path: the core worker echoes +# its JSON to a capture file and returns an rc; we then ALWAYS call +# updates_restore_after_swap once, re-emit the captured JSON, and return the rc. +# No early `return` skips the restore. +updates_install_sing_box_extended() { + local rc out json + + UPDATES_HEAL_RESOLV_REPLACED=0 + UPDATES_HEAL_REDIRECT_DOWN=0 + + if ! updates_ensure_connectivity "extended"; then + # Heal failed: nothing was removed (extended only touches the binary + # AFTER a reachable feed), so the router keeps its working core. + updates_restore_after_swap + updates_log "Aborting extended install: GitHub unreachable and self-heal failed (existing core left intact)" "error" + echo "{\"success\":false,\"message\":\"GitHub API unreachable and connectivity self-heal failed; core switch aborted (existing sing-box left intact)\"}" + return 1 + fi + + out="/tmp/netshift-sbext-result.$$" + _updates_install_sing_box_extended_core >"$out" 2>/dev/null + rc=$? + json="$(cat "$out" 2>/dev/null)" + rm -f "$out" 2>/dev/null + + updates_restore_after_swap + + [ -n "$json" ] && printf '%s\n' "$json" + return "$rc" +} + # Downloads and installs sing-box-extended, replacing /usr/bin/sing-box. # Echoes a JSON result on stdout. # @@ -644,7 +914,7 @@ updates_restart_netshift() { # live binary FIRST to reclaim overlay space; then stream-extract the new # member directly onto the final path so only ONE binary ever occupies # overlay. On any failure the tmpfs backup is moved back into place. -updates_install_sing_box_extended() { +_updates_install_sing_box_extended_core() { local tmp_dir archive releases tag rel asset_url local binary_path cronet_path local backup_binary="" backup_cronet="" new_version @@ -796,15 +1066,93 @@ updates_install_sing_box_extended() { return 0 } +# Public entry: install the stock (stable) sing-box with the connectivity +# self-heal preamble + the always-run restore epilogue around the real worker. +# +# CRITICAL ordering (learned from the on-hardware brick): the stable path +# removes/replaces the binary via the package manager, which needs working feed +# connectivity. So we MUST confirm a reachable feed (pre-flight, then self-heal) +# BEFORE the worker touches the binary. If the heal fails, we abort here — +# nothing has been removed, so the router keeps its working (extended) core. +# +# The epilogue is guaranteed via a SINGLE cleanup path: the core worker echoes +# its JSON to a capture file and returns an rc; we then ALWAYS call +# updates_restore_after_swap once, re-emit the captured JSON, and return the rc. +updates_install_sing_box_stable() { + local rc out json + + UPDATES_HEAL_RESOLV_REPLACED=0 + UPDATES_HEAL_REDIRECT_DOWN=0 + + if ! updates_ensure_connectivity "stable"; then + # Heal failed BEFORE the binary was touched: do NOT proceed to the + # package install. The router keeps its current working core. + updates_restore_after_swap + updates_log "Aborting stable install: package feeds unreachable and self-heal failed (binary NOT removed; existing core left intact)" "error" + echo "{\"success\":false,\"message\":\"Package feeds unreachable and connectivity self-heal failed; core switch aborted (existing sing-box left intact)\"}" + return 1 + fi + + out="/tmp/netshift-sbstable-result.$$" + _updates_install_sing_box_stable_core >"$out" 2>/dev/null + rc=$? + json="$(cat "$out" 2>/dev/null)" + rm -f "$out" 2>/dev/null + + updates_restore_after_swap + + [ -n "$json" ] && printf '%s\n' "$json" + return "$rc" +} + # Reinstalls the stock (stable) sing-box via the system package manager, # reverting an "extended" install. Unlike the extended path this never touches # the GitHub API. Echoes a JSON result on stdout. # +# Backup/rollback parity with the extended path (task-009): the current binary +# (and libcronet.so if present) is backed up to TMPFS before the install. If +# the package install fails OR the post-install non-extended validation fails, +# the tmpfs backup is restored so the router keeps a working core (it stays on +# the extended build rather than ending core-less). The backup is dropped only +# after a confirmed-good install. +# # The install result is checked (no silent "|| true" that always reports # success), and the outcome is validated to be a NON-extended build so a failed # downgrade is surfaced honestly instead of masquerading as success. -updates_install_sing_box_stable() { +_updates_install_sing_box_stable_core() { local new_version installed=1 + local tmp_dir backup_binary="" backup_cronet="" + + # Remove stale temp dirs from an interrupted earlier run (tmpfs is small). + rm -rf /tmp/netshift-sbstable.* 2>/dev/null + + tmp_dir="$(mktemp -d /tmp/netshift-sbstable.XXXXXX 2>/dev/null)" + if [ -z "$tmp_dir" ]; then + updates_log "Failed to create temporary directory" "error" + echo "{\"success\":false,\"message\":\"Failed to create temporary directory\"}" + return 1 + fi + + # Back up the current binary/lib ON TMPFS (/tmp) BEFORE the package manager + # touches anything, so a failed install can be rolled back to a working core. + if [ -e "$UPDATES_SING_BOX_BIN" ]; then + backup_binary="$tmp_dir/sing-box.backup" + if ! cp -p "$UPDATES_SING_BOX_BIN" "$backup_binary" 2>/dev/null; then + rm -rf "$tmp_dir" + updates_log "Failed to backup current sing-box binary" "error" + echo "{\"success\":false,\"message\":\"Failed to backup current sing-box binary\"}" + return 1 + fi + fi + if [ -e "$UPDATES_LIBCRONET_LIB" ]; then + backup_cronet="$tmp_dir/libcronet.so.backup" + if ! cp -p "$UPDATES_LIBCRONET_LIB" "$backup_cronet" 2>/dev/null; then + rm -rf "$tmp_dir" + updates_log "Failed to backup current libcronet.so" "error" + echo "{\"success\":false,\"message\":\"Failed to backup current libcronet.so\"}" + return 1 + fi + fi if command -v apk >/dev/null 2>&1; then updates_log "Updating apk package lists" @@ -822,41 +1170,76 @@ updates_install_sing_box_stable() { opkg install --force-downgrade sing-box </dev/null >/dev/null 2>&1 || installed=0 fi else + rm -rf "$tmp_dir" updates_log "No supported package manager (apk/opkg) found" "error" echo "{\"success\":false,\"message\":\"No supported package manager found\"}" return 1 fi if [ "$installed" -eq 0 ]; then - updates_log "Failed to install stable sing-box via package manager" "error" - echo "{\"success\":false,\"message\":\"Failed to install stable sing-box (package manager error; check connectivity/repositories)\"}" + # Package install failed (it may have already removed/half-replaced the + # binary). Restore the tmpfs backup so a working core remains. + updates_stable_rollback "$backup_binary" "$backup_cronet" + rm -rf "$tmp_dir" + updates_log "Failed to install stable sing-box via package manager; previous binary restored" "error" + echo "{\"success\":false,\"message\":\"Failed to install stable sing-box (package manager error); previous binary restored\"}" return 1 fi - # The extended path side-loads /usr/lib/libcronet.so next to the binary. - # Stock sing-box does not use it; remove the leftover so the rollback is - # clean. The package manager never installs this file, so this is safe. - if [ -e /usr/lib/libcronet.so ]; then - updates_log "Removing leftover libcronet.so from extended install" - rm -f /usr/lib/libcronet.so 2>/dev/null || true - fi - updates_restart_netshift new_version="$(get_sing_box_version)" # Validate the rollback actually took effect: the running binary must no - # longer be an "extended" build. + # longer be an "extended" build. If it still is, the install did not land — + # restore the backup so the router keeps a known-good core. if is_sing_box_extended "$new_version"; then - updates_log "Stable install reported success but sing-box is still extended ($new_version)" "error" - echo "{\"success\":false,\"message\":\"sing-box is still the extended build after install; rollback did not take effect\"}" + updates_stable_rollback "$backup_binary" "$backup_cronet" + rm -rf "$tmp_dir" + updates_log "Stable install reported success but sing-box is still extended ($new_version); previous binary restored" "error" + echo "{\"success\":false,\"message\":\"sing-box is still the extended build after install; rollback did not take effect (previous binary restored)\"}" return 1 fi + # Confirmed-good install. The extended path side-loads /usr/lib/libcronet.so + # next to the binary; stock sing-box does not use it, so drop the leftover. + if [ -e "$UPDATES_LIBCRONET_LIB" ]; then + updates_log "Removing leftover libcronet.so from extended install" + rm -f "$UPDATES_LIBCRONET_LIB" 2>/dev/null || true + fi + + # Drop the backup only now that the install is confirmed good. + rm -rf "$tmp_dir" updates_log "Stable sing-box installed: ${new_version:-unknown}" echo "{\"success\":true,\"version\":\"$new_version\"}" return 0 } +# Restores the tmpfs backup of /usr/bin/sing-box (and libcronet.so) into place. +# Used by the stable path when the package install or validation fails so the +# router never ends core-less. Best-effort; logs the outcome. +updates_stable_rollback() { + local backup_binary="$1" + local backup_cronet="$2" + + if [ -n "$backup_binary" ] && [ -e "$backup_binary" ]; then + rm -f "$UPDATES_SING_BOX_BIN" 2>/dev/null + if mv -f "$backup_binary" "$UPDATES_SING_BOX_BIN" 2>/dev/null; then + chmod 0755 "$UPDATES_SING_BOX_BIN" 2>/dev/null || true + updates_log "Rollback: restored previous sing-box binary from tmpfs backup" + else + updates_log "Rollback: FAILED to restore sing-box binary from backup" "error" + fi + fi + + if [ -n "$backup_cronet" ] && [ -e "$backup_cronet" ]; then + rm -f "$UPDATES_LIBCRONET_LIB" 2>/dev/null + if mv -f "$backup_cronet" "$UPDATES_LIBCRONET_LIB" 2>/dev/null; then + chmod 0644 "$UPDATES_LIBCRONET_LIB" 2>/dev/null || true + updates_log "Rollback: restored previous libcronet.so from tmpfs backup" + fi + fi +} + # Checks whether a newer sing-box-extended release is available. # Echoes a JSON status (latest|outdated) on stdout. updates_check_sing_box_extended() { diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 8c9d4dfc..45739ec0 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -9,7 +9,7 @@ # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, -# diagnostics, subscription, jobstate +# diagnostics, subscription, jobstate, selfheal # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index e147dbc1..07f0ee84 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1576,6 +1576,338 @@ SEOF rm -rf "$jdir" "$stub" } +# ───────────────────────────────────────────────────────────────── +# Test: Core-switch connectivity self-heal + rollback (updater.sh, task-009) +# +# Fully mocked — no real network, no real package install, no real binary +# touched. A generated driver sources updater.sh, points RESOLV_CONF and the +# tmpfs backup at test files, stubs dig/nslookup/curl/opkg/apk and a fake +# /etc/init.d/netshift via a PATH-prepended bin dir + a writable init stub, and +# drives each scenario via env flags. The driver emits `name:OK`/`name:FAIL` +# tokens which the case parser turns into pass/fail. +# ───────────────────────────────────────────────────────────────── +test_selfheal() { + header "Core-switch Connectivity Self-Heal + Rollback (updater.sh)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local updater="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$updater" ]; then + skip "updater.sh not found in ${NETSHIFT_LIB_DIR}" + return + fi + + local work="/tmp/netshift-selfheal-$$" + rm -rf "$work" + mkdir -p "$work/bin" "$work/init" + + # ── Command stubs (PATH-prepended). Behaviour is driven by env files so the + # driver can flip them between scenarios without rewriting the stubs. ────── + # + # DNS/HTTPS probes: a stub "succeeds" only when its marker file is present. + cat > "$work/bin/dig" << 'DIGEOF' +#!/bin/sh +# Echo an address (so the resolver-detect grep matches) only if allowed. +[ -f "$SELFHEAL_DNS_OK" ] && { echo "1.2.3.4"; exit 0; } +exit 1 +DIGEOF + cat > "$work/bin/nslookup" << 'NSEOF' +#!/bin/sh +[ -f "$SELFHEAL_DNS_OK" ] && { echo "Address 1.2.3.4"; exit 0; } +exit 1 +NSEOF + cat > "$work/bin/curl" << 'CURLEOF' +#!/bin/sh +# Reachability probe (-I/HEAD). Succeed only when the marker is present. +[ -f "$SELFHEAL_HTTP_OK" ] && exit 0 +exit 1 +CURLEOF + # opkg/apk stubs: package "install" succeeds or fails per marker, and on a + # "successful" stable install they flip the installed core to non-extended. + cat > "$work/bin/opkg" << 'OPKGEOF' +#!/bin/sh +case "$1" in +update) exit 0 ;; +install) + if [ -f "$SELFHEAL_PKG_OK" ]; then + printf 'stable-1.12.0\n' > "$SELFHEAL_CORE_VERSION" + exit 0 + fi + # Simulate a package failure that ALSO removed the live binary (the brick + # scenario): blow away the mock binary so the rollback must restore it. + rm -f "$SELFHEAL_BIN" 2>/dev/null + exit 1 + ;; +esac +exit 0 +OPKGEOF + chmod 0755 "$work/bin/dig" "$work/bin/nslookup" "$work/bin/curl" "$work/bin/opkg" + + # Fake /etc/init.d/netshift: records each invocation (stop/start/restart) to + # a log so the driver can assert teardown/bring-up happened. + cat > "$work/init/netshift" << 'INITEOF' +#!/bin/sh +printf '%s\n' "$1" >> "$SELFHEAL_INIT_LOG" +exit 0 +INITEOF + chmod 0755 "$work/init/netshift" + + # ── Driver: sources updater, overrides paths/helpers, runs one scenario. ── + local drv="$work/driver.sh" + cat > "$drv" << 'DRVEOF' +log() { :; } +echolog() { :; } +nolog() { :; } +updates_log() { :; } +RESOLV_CONF="DRV_RESOLV" +UPDATES_RESOLV_BACKUP="DRV_BACKUP" +UPDATES_FEED_PROBE_HOST="feeds.test" +UPDATES_GITHUB_PROBE_HOST="github.test" +UPDATES_HEAL_RESOLVERS="1.1.1.1 9.9.9.9" +UPDATES_SING_BOX_BIN="$SELFHEAL_BIN" +UPDATES_LIBCRONET_LIB="DRV_CRONET" +. "DRV_UPDATER" +# Re-pin after sourcing (the source sets its own defaults). +RESOLV_CONF="DRV_RESOLV" +UPDATES_RESOLV_BACKUP="DRV_BACKUP" +UPDATES_FEED_PROBE_HOST="feeds.test" +UPDATES_GITHUB_PROBE_HOST="github.test" +UPDATES_HEAL_RESOLVERS="1.1.1.1 9.9.9.9" +UPDATES_SING_BOX_BIN="$SELFHEAL_BIN" +UPDATES_LIBCRONET_LIB="DRV_CRONET" + +# Mocked helpers used by the stable core (normally from helpers.sh). +get_sing_box_version() { cat "$SELFHEAL_CORE_VERSION" 2>/dev/null; } +is_sing_box_extended() { + case "${1:-$(get_sing_box_version)}" in + *extended*) return 0 ;; + *) return 1 ;; + esac +} +# Make the post-install restart a no-op probe (the fake init records it anyway). +updates_restart_netshift() { /etc/init.d/netshift restart >/dev/null 2>&1 || true; } + +case "$1" in +run_stable) updates_install_sing_box_stable ;; +esac +DRVEOF + sed -i "s|DRV_UPDATER|$updater|g;s|DRV_RESOLV|$work/resolv.conf|g;s|DRV_BACKUP|$work/resolv.bak|g;s|DRV_CRONET|$work/libcronet.so|g" "$drv" + + # Common per-run wiring: PATH-prepended stubs + fake init under /etc/init.d. + # We back up any real /etc/init.d/netshift and restore it at the end. + local init_target="/etc/init.d/netshift" + local init_saved="" + if [ -e "$init_target" ]; then + init_saved="$work/netshift.realinit" + cp -p "$init_target" "$init_saved" 2>/dev/null || init_saved="" + fi + mkdir -p /etc/init.d 2>/dev/null || true + cp -p "$work/init/netshift" "$init_target" 2>/dev/null + chmod 0755 "$init_target" 2>/dev/null || true + + # Marker/state files shared with the stubs via env. + export SELFHEAL_DNS_OK="$work/dns_ok" + export SELFHEAL_HTTP_OK="$work/http_ok" + export SELFHEAL_PKG_OK="$work/pkg_ok" + export SELFHEAL_INIT_LOG="$work/init.log" + export SELFHEAL_CORE_VERSION="$work/core.version" + export SELFHEAL_BIN="$work/usr-bin-sing-box" + + local out="$work/out.json" + + run_scenario() { + # The worker returns non-zero on recoverable failures (success:false); + # under `set -e` that would abort the suite, so swallow the rc here — the + # assertions read the JSON + file state, not the exit code. + rm -f "$work/init.log" + PATH="$work/bin:$PATH" ash "$drv" run_stable > "$out" 2>/dev/null || true + } + + # ── Scenario 1: pre-flight passes → install proceeds, no teardown ───────── + : > "$SELFHEAL_DNS_OK"; : > "$SELFHEAL_HTTP_OK"; : > "$SELFHEAL_PKG_OK" + printf 'extended-1.12.0\n' > "$SELFHEAL_CORE_VERSION" + printf 'original-resolver\n' > "$work/resolv.conf" + : > "$work/usr-bin-sing-box" + run_scenario + if jq -e '.success == true' "$out" > /dev/null 2>&1; then + pass "selfheal-preflight-pass-proceeds:OK" + else + fail "selfheal-preflight-pass-proceeds:FAIL" "$(cat "$out" 2>/dev/null)" + fi + if [ ! -f "$work/init.log" ] || ! grep -q 'stop' "$work/init.log"; then + pass "selfheal-preflight-pass-no-teardown:OK" + else + fail "selfheal-preflight-pass-no-teardown:FAIL" "init.log=$(cat "$work/init.log" 2>/dev/null)" + fi + if [ "$(cat "$work/resolv.conf" 2>/dev/null)" = "original-resolver" ]; then + pass "selfheal-preflight-pass-resolv-untouched:OK" + else + fail "selfheal-preflight-pass-resolv-untouched:FAIL" "$(cat "$work/resolv.conf" 2>/dev/null)" + fi + + # ── Scenario 2: pre-flight fails → DNS heal succeeds → resolv restored ──── + # DNS fails first, but once the temp resolver is written DNS+HTTP pass. We + # model "temp resolver fixes DNS" by making the DNS probe key off the temp + # resolver content: the stub succeeds only when the marker exists, and the + # heal writes the marker via a wrapper. Simpler: DNS off initially, but the + # heal's resolv write triggers a hook that flips DNS on. We emulate that by + # having the temp-resolver write observed through resolv.conf content. + rm -f "$SELFHEAL_DNS_OK"; : > "$SELFHEAL_HTTP_OK"; : > "$SELFHEAL_PKG_OK" + printf 'extended-1.12.0\n' > "$SELFHEAL_CORE_VERSION" + printf 'original-resolver\n' > "$work/resolv.conf" + : > "$work/usr-bin-sing-box" + # dig stub variant for scenario 2: DNS resolves only once resolv.conf holds + # the temp resolver (i.e. after the heal wrote it). + cat > "$work/bin/dig" << 'DIG2EOF' +#!/bin/sh +grep -q '1.1.1.1' "DRV_RESOLV2" 2>/dev/null && { echo "1.2.3.4"; exit 0; } +exit 1 +DIG2EOF + sed -i "s|DRV_RESOLV2|$work/resolv.conf|g" "$work/bin/dig" + chmod 0755 "$work/bin/dig" + run_scenario + if jq -e '.success == true' "$out" > /dev/null 2>&1; then + pass "selfheal-dns-heal-proceeds:OK" + else + fail "selfheal-dns-heal-proceeds:FAIL" "$(cat "$out" 2>/dev/null)" + fi + # Epilogue must have restored the ORIGINAL resolv.conf. + if [ "$(cat "$work/resolv.conf" 2>/dev/null)" = "original-resolver" ]; then + pass "selfheal-dns-heal-resolv-restored:OK" + else + fail "selfheal-dns-heal-resolv-restored:FAIL" "$(cat "$work/resolv.conf" 2>/dev/null)" + fi + # DNS heal alone was enough → redirect should NOT have been torn down. + if [ ! -f "$work/init.log" ] || ! grep -q 'stop' "$work/init.log"; then + pass "selfheal-dns-heal-no-teardown:OK" + else + fail "selfheal-dns-heal-no-teardown:FAIL" "init.log=$(cat "$work/init.log" 2>/dev/null)" + fi + + # ── Scenario 3: DNS heal insufficient → redirect teardown heals ─────────── + # DNS resolves even with the temp resolver, but HTTP only comes up AFTER the + # redirect is torn down (the fake init writes a marker on stop that flips + # HTTP on). + rm -f "$SELFHEAL_DNS_OK"; rm -f "$SELFHEAL_HTTP_OK"; : > "$SELFHEAL_PKG_OK" + printf 'extended-1.12.0\n' > "$SELFHEAL_CORE_VERSION" + printf 'original-resolver\n' > "$work/resolv.conf" + : > "$work/usr-bin-sing-box" + # DNS resolves only with temp resolver present (as scenario 2). + # HTTP succeeds only after init stop has been recorded. + cat > "$work/bin/curl" << 'CURL3EOF' +#!/bin/sh +grep -q 'stop' "$SELFHEAL_INIT_LOG" 2>/dev/null && exit 0 +exit 1 +CURL3EOF + chmod 0755 "$work/bin/curl" + run_scenario + if jq -e '.success == true' "$out" > /dev/null 2>&1; then + pass "selfheal-teardown-heal-proceeds:OK" + else + fail "selfheal-teardown-heal-proceeds:FAIL" "$(cat "$out" 2>/dev/null)" + fi + if grep -q 'stop' "$work/init.log" 2>/dev/null; then + pass "selfheal-teardown-taken:OK" + else + fail "selfheal-teardown-taken:FAIL" "init.log=$(cat "$work/init.log" 2>/dev/null)" + fi + if grep -q 'start' "$work/init.log" 2>/dev/null; then + pass "selfheal-teardown-bringup-called:OK" + else + fail "selfheal-teardown-bringup-called:FAIL" "init.log=$(cat "$work/init.log" 2>/dev/null)" + fi + if [ "$(cat "$work/resolv.conf" 2>/dev/null)" = "original-resolver" ]; then + pass "selfheal-teardown-resolv-restored:OK" + else + fail "selfheal-teardown-resolv-restored:FAIL" "$(cat "$work/resolv.conf" 2>/dev/null)" + fi + + # ── Scenario 4: heal fails entirely → install ABORTED, binary not removed ─ + rm -f "$SELFHEAL_DNS_OK"; rm -f "$SELFHEAL_HTTP_OK"; : > "$SELFHEAL_PKG_OK" + printf 'extended-1.12.0\n' > "$SELFHEAL_CORE_VERSION" + printf 'original-resolver\n' > "$work/resolv.conf" + : > "$work/usr-bin-sing-box" + # DNS never resolves; HTTP never reachable even after teardown. + cat > "$work/bin/dig" << 'DIG4EOF' +#!/bin/sh +exit 1 +DIG4EOF + cat > "$work/bin/curl" << 'CURL4EOF' +#!/bin/sh +exit 1 +CURL4EOF + chmod 0755 "$work/bin/dig" "$work/bin/curl" + run_scenario + if jq -e '.success == false and (.message | length) > 0' "$out" > /dev/null 2>&1; then + pass "selfheal-heal-fail-aborts-successfalse:OK" + else + fail "selfheal-heal-fail-aborts-successfalse:FAIL" "$(cat "$out" 2>/dev/null)" + fi + # The (mock) binary must NOT have been removed (opkg install never ran). + if [ -e "$work/usr-bin-sing-box" ]; then + pass "selfheal-heal-fail-binary-intact:OK" + else + fail "selfheal-heal-fail-binary-intact:FAIL" "mock binary was removed" + fi + # Original resolv.conf restored by the epilogue. + if [ "$(cat "$work/resolv.conf" 2>/dev/null)" = "original-resolver" ]; then + pass "selfheal-heal-fail-resolv-restored:OK" + else + fail "selfheal-heal-fail-resolv-restored:FAIL" "$(cat "$work/resolv.conf" 2>/dev/null)" + fi + # Redirect was torn down during the (failed) heal → epilogue brings it back. + if grep -q 'start' "$work/init.log" 2>/dev/null; then + pass "selfheal-heal-fail-bringup-called:OK" + else + fail "selfheal-heal-fail-bringup-called:FAIL" "init.log=$(cat "$work/init.log" 2>/dev/null)" + fi + + # ── Scenario 5: stable install fails after binary removed → backup restored + : > "$SELFHEAL_DNS_OK"; : > "$SELFHEAL_HTTP_OK"; rm -f "$SELFHEAL_PKG_OK" + printf 'extended-1.12.0\n' > "$SELFHEAL_CORE_VERSION" + printf 'original-resolver\n' > "$work/resolv.conf" + printf 'EXTENDED-CORE-BYTES\n' > "$work/usr-bin-sing-box" + # Connectivity is fine; dig/curl just check the markers. + cat > "$work/bin/dig" << 'DIG5EOF' +#!/bin/sh +[ -f "$SELFHEAL_DNS_OK" ] && { echo "1.2.3.4"; exit 0; } +exit 1 +DIG5EOF + cat > "$work/bin/curl" << 'CURL5EOF' +#!/bin/sh +[ -f "$SELFHEAL_HTTP_OK" ] && exit 0 +exit 1 +CURL5EOF + chmod 0755 "$work/bin/dig" "$work/bin/curl" + run_scenario + if jq -e '.success == false' "$out" > /dev/null 2>&1; then + pass "selfheal-stable-install-fail-successfalse:OK" + else + fail "selfheal-stable-install-fail-successfalse:FAIL" "$(cat "$out" 2>/dev/null)" + fi + # The opkg stub removed the live binary; the tmpfs backup must be restored + # so a working binary remains with the ORIGINAL extended bytes. + if [ -e "$work/usr-bin-sing-box" ] && \ + [ "$(cat "$work/usr-bin-sing-box" 2>/dev/null)" = "EXTENDED-CORE-BYTES" ]; then + pass "selfheal-stable-install-fail-backup-restored:OK" + else + fail "selfheal-stable-install-fail-backup-restored:FAIL" "$(cat "$work/usr-bin-sing-box" 2>/dev/null)" + fi + + # ── Restore the real init script (if any) and clean up. ────────────────── + if [ -n "$init_saved" ] && [ -e "$init_saved" ]; then + cp -p "$init_saved" "$init_target" 2>/dev/null || true + else + rm -f "$init_target" 2>/dev/null || true + fi + unset SELFHEAL_DNS_OK SELFHEAL_HTTP_OK SELFHEAL_PKG_OK SELFHEAL_INIT_LOG \ + SELFHEAL_CORE_VERSION SELFHEAL_BIN + rm -rf "$work" +} + # ───────────────────────────────────────────────────────────────── # Main # ───────────────────────────────────────────────────────────────── @@ -1601,6 +1933,7 @@ main() { test_diagnostics test_subscription test_jobstate + test_selfheal ;; deps) test_deps ;; syntax) test_syntax ;; @@ -1610,12 +1943,13 @@ main() { diagnostics) test_diagnostics ;; subscription) test_subscription ;; jobstate) test_jobstate ;; + selfheal) test_selfheal ;; jq) test_jq_helpers ;; cm) test_config_manager ;; sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription jobstate" + echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription jobstate selfheal" exit 1 ;; esac From 8c9ef3dbf94c9a4699cc374a9c2b464ee79fd987 Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Fri, 5 Jun 2026 10:41:44 +0300 Subject: [PATCH 47/75] =?UTF-8?q?=D0=B4=D0=BE=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=B0=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BD=D0=B0=20=D1=84=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D0=BD=D0=B4=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/agent-rules/memory/code-reviewer.md | 2 + .../memory/luci-frontend-developer.md | 47 ++++++++ fe-app-netshift/locales/calls.json | 28 ++++- fe-app-netshift/locales/netshift.pot | 23 +++- fe-app-netshift/locales/netshift.ru.po | 13 +- .../src/netshift/methods/shell/index.ts | 101 +++++++++++----- .../shell/pollSingBoxComponentAction.ts | 84 +++++++++++++ .../src/netshift/methods/shell/sleep.ts | 3 + .../tests/pollSingBoxComponentAction.test.js | 106 ++++++++++++++++ .../tests/singBoxComponentAction.test.js | 79 ++++++++++++ .../tabs/diagnostic/initController.ts | 5 + .../resources/view/netshift/main.js | 114 +++++++++++++++--- luci-app-netshift/po/ru/netshift.po | 13 +- luci-app-netshift/po/templates/netshift.pot | 23 +++- 14 files changed, 578 insertions(+), 63 deletions(-) create mode 100644 fe-app-netshift/src/netshift/methods/shell/pollSingBoxComponentAction.ts create mode 100644 fe-app-netshift/src/netshift/methods/shell/sleep.ts create mode 100644 fe-app-netshift/src/netshift/methods/shell/tests/pollSingBoxComponentAction.test.js create mode 100644 fe-app-netshift/src/netshift/methods/shell/tests/singBoxComponentAction.test.js diff --git a/docs/agent-rules/memory/code-reviewer.md b/docs/agent-rules/memory/code-reviewer.md index 78dfad1a..e37ba421 100644 --- a/docs/agent-rules/memory/code-reviewer.md +++ b/docs/agent-rules/memory/code-reviewer.md @@ -60,3 +60,5 @@ append recurring findings; keep under ~200 lines. - For protocol validators that base64-decode a whole body (vmess, future tuic/etc.): the '+'-regression is real only if the dispatcher preserves '+'. validateProxyUrl only .trim()s, so '+' survives at the boundary a green direct-call '+' test is sufficient evidence; a dispatcher-level '+' assertion is the stronger guard. - Wrapper/core split for always-run cleanup (task-009 core-switch): verify the public wrapper captures core stdout to a temp file + rc, then UNCONDITIONALLY calls restore/cleanup before re-emitting JSON and return rc; confirm the worker runs without set -e (else a non-zero core rc could skip trailing cleanup) and that _*_core never exits. For never-end-core-less rollbacks, confirm the tmpfs backup happens BEFORE the package manager/extract touches the binary and is dropped ONLY on the confirmed-good path; strongest test deletes the live mock binary on simulated failure and asserts original bytes restored. + +- Frontend barrel exposure: anything added to src/helpers/index.ts (or any export* barrel reaching main.ts) AND actually used appears in the generated main.js baseclass.extend block as a main.* symbol; unused re-exports get tree-shaken. So internal-only helper + added to barrel + used = it WILL leak to main.*. To keep a helper truly internal, place it in the consuming module, not the barrel. diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index e8264135..4d76e1a5 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -83,6 +83,53 @@ append findings; keep under ~200 lines. `describe.each`, `_()` identity-mocked in `tests/setup/global-mocks.ts`, node env (no DOM). New pure logic SHOULD ship a test. DOM/service/render code is untested (no DOM mocks) — verify those by reasoning + build. +- DO NOT import a `.test.js` from `methods/shell/index.ts` (or anything that + transitively imports the helpers barrel → `withTimeout` → `../netshift` + logger → `TabService` which calls `new MutationObserver` at module init). + In the node env that throws `MutationObserver is not defined` at COLLECT time + (suite fails with "no tests"). Fix: put pure/testable logic in its OWN module + that imports leaf helpers by DIRECT path (e.g. `../../../helpers/sleep`, NOT + the barrel), and import the test from that module. Done for task-008's + `pollSingBoxComponentAction.ts`. + +## Async core switch (task-008) — the rpcd 30s pattern + +- The core switch (sing-box install_extended/install_stable) must use the + ASYNC backend contract, not a single sync `component_action` exec — rpcd + kills any single fs.exec at 30s SERVER-SIDE regardless of the JS + `timeout:600000`. Pattern: `executeShellCommand(['component_action_async', + 'sing_box', action])` → parse `{success,job_id,message}` → if no job_id, + fail fast → poll `['component_action_status', jobId]` in a loop every ~2s + with SHORT individual execs until `running !== true`; safety cap ~150 polls. + Each status call is tiny (ms) so never hits the wall. +- Status contract fields (task-007/009): `{success,running,component,action, + message,pid,started_at,updated_at,exit_code,version,latest_version}`. + Map terminal → `{success, version, message}`. +- `check_update` stays on the SYNC `component_action` path (fast, not subject + to 30s) — branch inside `singBoxComponentAction` on `action`. +- Make the poll loop a PURE fn `pollSingBoxComponentAction(fetchStatus, + sleepFn=sleep, intervalMs, maxPolls)` with injected `fetchStatus`+`sleepFn` + so tests pass a no-op sleep (`() => Promise.resolve()`) and never wait 2s. + `sleep(ms)` lives in `helpers/sleep.ts` (Promise+setTimeout). +- `showToast` type is only `'success' | 'error'` — for an in-progress info + toast just use `'success'` (don't widen the helper signature for it). +- BARREL LEAK GOTCHA: adding `export * from './sleep'` to `helpers/index.ts` + made `sleep` appear as `main.sleep` in the generated baseclass.extend export + block (used barrel exports are NOT tree-shaken). For a truly INTERNAL helper, + do NOT put it in the barrel — place it next to its only consumer (e.g. + `methods/shell/sleep.ts`) and import by direct relative path. Verify after + build: `git diff main.js | grep '^\+\s\+[a-z]\+,$'` shows no new bare export + line. (task-008 M1.) +- TESTING the REAL `singBoxComponentAction` despite the DOM/MutationObserver + collect crash: `vi.mock('<barrel path>', () => ({ executeShellCommand: ... }))` + short-circuits the `helpers`→`withTimeout`→`../netshift`→TabService chain so + the method module imports cleanly, THEN `const { X } = await import('../index')`. + CRITICAL: `vi.mock` factory paths are relative to the TEST file, and must + resolve to the SAME absolute module the SUT imports. From + `methods/shell/tests/`, the SUT's `../../../helpers` is `../../../../helpers` + from the test, and `./callBaseMethod` is `../../callBaseMethod`. Get these + wrong and the real (DOM-crashing) module loads → "MutationObserver is not + defined" at collect. (task-008 M2.) ## Landmines diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index 08eee263..0405a1c5 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -237,6 +237,21 @@ "src\\partials\\modal\\renderModal.ts:20" ] }, + { + "call": "Core switch failed", + "key": "Core switch failed", + "places": [ + "src\\netshift\\methods\\shell\\index.ts:157", + "src\\netshift\\methods\\shell\\pollSingBoxComponentAction.ts:65" + ] + }, + { + "call": "Core switch timed out", + "key": "Core switch timed out", + "places": [ + "src\\netshift\\methods\\shell\\pollSingBoxComponentAction.ts:82" + ] + }, { "call": "Currently unavailable", "key": "Currently unavailable", @@ -640,8 +655,8 @@ "src\\netshift\\tabs\\diagnostic\\initController.ts:267", "src\\netshift\\tabs\\diagnostic\\initController.ts:304", "src\\netshift\\tabs\\diagnostic\\initController.ts:308", - "src\\netshift\\tabs\\diagnostic\\initController.ts:342", - "src\\netshift\\tabs\\diagnostic\\initController.ts:346" + "src\\netshift\\tabs\\diagnostic\\initController.ts:347", + "src\\netshift\\tabs\\diagnostic\\initController.ts:351" ] }, { @@ -1625,7 +1640,7 @@ "call": "Sing-box core changed, version:", "key": "Sing-box core changed, version:", "places": [ - "src\\netshift\\tabs\\diagnostic\\initController.ts:337" + "src\\netshift\\tabs\\diagnostic\\initController.ts:342" ] }, { @@ -1748,6 +1763,13 @@ "src\\helpers\\copyToClipboard.ts:10" ] }, + { + "call": "Switching sing-box core, this may take a few minutes…", + "key": "Switching sing-box core, this may take a few minutes…", + "places": [ + "src\\netshift\\tabs\\diagnostic\\initController.ts:331" + ] + }, { "call": "System info", "key": "System info", diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index d3a9259d..ecafb2d4 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-04 19:59+0300\n" -"PO-Revision-Date: 2026-06-04 19:59+0300\n" +"POT-Creation-Date: 2026-06-05 06:27+0300\n" +"PO-Revision-Date: 2026-06-05 06:27+0300\n" "Last-Translator: yandexru45 <sukadark228@gmail.com>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -155,6 +155,15 @@ msgstr "" msgid "Copy" msgstr "" +#: src\netshift\methods\shell\index.ts:157 +#: src\netshift\methods\shell\pollSingBoxComponentAction.ts:65 +msgid "Core switch failed" +msgstr "" + +#: src\netshift\methods\shell\pollSingBoxComponentAction.ts:82 +msgid "Core switch timed out" +msgstr "" + #: src\netshift\tabs\dashboard\partials\renderWidget.ts:22 msgid "Currently unavailable" msgstr "" @@ -389,8 +398,8 @@ msgstr "" #: src\netshift\tabs\diagnostic\initController.ts:267 #: src\netshift\tabs\diagnostic\initController.ts:304 #: src\netshift\tabs\diagnostic\initController.ts:308 -#: src\netshift\tabs\diagnostic\initController.ts:342 -#: src\netshift\tabs\diagnostic\initController.ts:346 +#: src\netshift\tabs\diagnostic\initController.ts:347 +#: src\netshift\tabs\diagnostic\initController.ts:351 msgid "Failed to execute!" msgstr "" @@ -960,7 +969,7 @@ msgstr "" msgid "Sing-box autostart disabled" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:337 +#: src\netshift\tabs\diagnostic\initController.ts:342 msgid "Sing-box core changed, version:" msgstr "" @@ -1033,6 +1042,10 @@ msgstr "" msgid "Successfully copied!" msgstr "" +#: src\netshift\tabs\diagnostic\initController.ts:331 +msgid "Switching sing-box core, this may take a few minutes…" +msgstr "" + #: src\netshift\tabs\dashboard\initController.ts:304 msgid "System info" msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index e78ec4b8..2aa7144c 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-04 22:59+0300\n" -"PO-Revision-Date: 2026-06-04 22:59+0300\n" +"POT-Creation-Date: 2026-06-05 09:28+0300\n" +"PO-Revision-Date: 2026-06-05 09:28+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -116,6 +116,12 @@ msgstr "URL подключения" msgid "Copy" msgstr "Копировать" +msgid "Core switch failed" +msgstr "Не удалось переключить ядро" + +msgid "Core switch timed out" +msgstr "Истекло время ожидания переключения ядра" + msgid "Currently unavailable" msgstr "Временно недоступно" @@ -749,6 +755,9 @@ msgstr "" msgid "Successfully copied!" msgstr "Успешно скопировано!" +msgid "Switching sing-box core, this may take a few minutes…" +msgstr "Переключение ядра sing-box, это может занять несколько минут…" + msgid "System info" msgstr "Системная информация" diff --git a/fe-app-netshift/src/netshift/methods/shell/index.ts b/fe-app-netshift/src/netshift/methods/shell/index.ts index 428a42c5..63dc14da 100644 --- a/fe-app-netshift/src/netshift/methods/shell/index.ts +++ b/fe-app-netshift/src/netshift/methods/shell/index.ts @@ -1,12 +1,12 @@ import { callBaseMethod } from './callBaseMethod'; import { ClashAPI, NetShift } from '../../types'; import { executeShellCommand } from '../../../helpers'; - -interface SingBoxComponentActionResult { - success: boolean; - version?: string; - message?: string; -} +import { + ComponentActionStartResponse, + SingBoxComponentActionResult, + parseComponentActionStatus, + pollSingBoxComponentAction, +} from './pollSingBoxComponentAction'; export const NetShiftShellMethods = { checkDNSAvailable: async () => @@ -96,34 +96,81 @@ export const NetShiftShellMethods = { singBoxComponentAction: async ( action: 'install_extended' | 'install_stable' | 'check_update', ): Promise<SingBoxComponentActionResult> => { - const response = await executeShellCommand({ + // `check_update` is a quick single call — not subject to the rpcd 30s wall — + // so keep it on the SYNCHRONOUS `component_action` path (unchanged shape). + if (action === 'check_update') { + const response = await executeShellCommand({ + command: '/usr/bin/netshift', + args: ['component_action', 'sing_box', action], + timeout: 600000, + }); + + if (response.stdout) { + try { + const parsed = JSON.parse( + response.stdout, + ) as SingBoxComponentActionResult; + + return { + success: Boolean(parsed.success), + version: parsed.version, + message: parsed.message, + }; + } catch (_e) { + return { + success: false, + message: response.stdout, + }; + } + } + + return { + success: false, + message: response.stderr || '', + }; + } + + // Install actions can take minutes — drive the async backend contract: + // start the job, then poll `component_action_status` with short execs so + // rpcd never kills a single long-running call. + const startResponse = await executeShellCommand({ command: '/usr/bin/netshift', - args: ['component_action', 'sing_box', action], - timeout: 600000, + args: ['component_action_async', 'sing_box', action], }); - if (response.stdout) { - try { - const parsed = JSON.parse( - response.stdout, - ) as SingBoxComponentActionResult; + let start: ComponentActionStartResponse | null = null; - return { - success: Boolean(parsed.success), - version: parsed.version, - message: parsed.message, - }; + if (startResponse.stdout) { + try { + start = JSON.parse( + startResponse.stdout, + ) as ComponentActionStartResponse; } catch (_e) { - return { - success: false, - message: response.stdout, - }; + start = null; } } - return { - success: false, - message: response.stderr || '', - }; + if (!start || start.success !== true || !start.job_id) { + return { + success: false, + message: + start?.message || startResponse.stderr || _('Core switch failed'), + }; + } + + const jobId = start.job_id; + + return pollSingBoxComponentAction(async () => { + const statusResponse = await executeShellCommand({ + command: '/usr/bin/netshift', + args: ['component_action_status', jobId], + }); + + if (!statusResponse.stdout) { + return null; + } + + return parseComponentActionStatus(statusResponse.stdout); + }); }, }; diff --git a/fe-app-netshift/src/netshift/methods/shell/pollSingBoxComponentAction.ts b/fe-app-netshift/src/netshift/methods/shell/pollSingBoxComponentAction.ts new file mode 100644 index 00000000..d3cbc79a --- /dev/null +++ b/fe-app-netshift/src/netshift/methods/shell/pollSingBoxComponentAction.ts @@ -0,0 +1,84 @@ +import { sleep } from './sleep'; + +export interface SingBoxComponentActionResult { + success: boolean; + version?: string; + message?: string; +} + +// Shape echoed by `component_action_async sing_box <action>` on start. +export interface ComponentActionStartResponse { + success?: boolean; + job_id?: string; + message?: string; +} + +// Shape echoed by `component_action_status <job_id>` (task-007/009 contract). +export interface ComponentActionStatus { + success?: boolean; + running?: boolean; + component?: string; + action?: string; + message?: string; + pid?: number | null; + started_at?: number; + updated_at?: number; + exit_code?: number | null; + version?: string; + latest_version?: string; +} + +// ~2s between polls; ~150 polls ≈ 5 min backstop against a wedged job. +export const POLL_INTERVAL_MS = 2000; +export const MAX_POLLS = 150; + +export function parseComponentActionStatus( + stdout: string, +): ComponentActionStatus | null { + try { + return JSON.parse(stdout) as ComponentActionStatus; + } catch (_e) { + return null; + } +} + +/** + * Pure poll loop for the async core switch. Each `fetchStatus` call is a tiny, + * individual `component_action_status` exec (well under the rpcd 30s wall); the + * loop runs until the job is no longer running (a parse failure or + * `running === false` is terminal). `sleepFn` is injected so tests can avoid + * real 2s waits. + */ +export async function pollSingBoxComponentAction( + fetchStatus: () => Promise<ComponentActionStatus | null>, + sleepFn: (ms: number) => Promise<void> = sleep, + intervalMs: number = POLL_INTERVAL_MS, + maxPolls: number = MAX_POLLS, +): Promise<SingBoxComponentActionResult> { + for (let poll = 0; poll < maxPolls; poll += 1) { + const status = await fetchStatus(); + + // A parse failure (null) is terminal — we cannot keep polling blindly. + if (!status) { + return { + success: false, + message: _('Core switch failed'), + }; + } + + if (status.running !== true) { + return { + success: Boolean(status.success), + version: status.version, + message: status.message, + }; + } + + await sleepFn(intervalMs); + } + + return { + success: false, + message: _('Core switch timed out'), + }; +} diff --git a/fe-app-netshift/src/netshift/methods/shell/sleep.ts b/fe-app-netshift/src/netshift/methods/shell/sleep.ts new file mode 100644 index 00000000..421bda08 --- /dev/null +++ b/fe-app-netshift/src/netshift/methods/shell/sleep.ts @@ -0,0 +1,3 @@ +export function sleep(ms: number): Promise<void> { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/fe-app-netshift/src/netshift/methods/shell/tests/pollSingBoxComponentAction.test.js b/fe-app-netshift/src/netshift/methods/shell/tests/pollSingBoxComponentAction.test.js new file mode 100644 index 00000000..bf7e097c --- /dev/null +++ b/fe-app-netshift/src/netshift/methods/shell/tests/pollSingBoxComponentAction.test.js @@ -0,0 +1,106 @@ +import { describe, expect, it, vi } from 'vitest'; +import { pollSingBoxComponentAction } from '../pollSingBoxComponentAction'; + +// No-op sleep so polls resolve instantly (no real 2s waits in tests). +const noSleep = () => Promise.resolve(); + +// Build a fetchStatus callback that returns the queued statuses in order, +// then keeps returning the last one. +function makeFetchStatus(statuses) { + let index = 0; + + return vi.fn(async () => { + const status = statuses[Math.min(index, statuses.length - 1)]; + index += 1; + + return status; + }); +} + +describe('pollSingBoxComponentAction', () => { + it('resolves success with version after N running polls then terminal', async () => { + const fetchStatus = makeFetchStatus([ + { running: true, success: true, exit_code: null }, + { running: true, success: true, exit_code: null }, + { + running: false, + success: true, + version: '1.12.4', + message: 'Core switched', + exit_code: 0, + }, + ]); + + const result = await pollSingBoxComponentAction(fetchStatus, noSleep); + + expect(result).toEqual({ + success: true, + version: '1.12.4', + message: 'Core switched', + }); + // 3 status reads (2 running + 1 terminal). + expect(fetchStatus).toHaveBeenCalledTimes(3); + }); + + it('surfaces the failure message on terminal success:false', async () => { + const fetchStatus = makeFetchStatus([ + { running: true, success: true }, + { + running: false, + success: false, + message: 'core switch aborted (existing sing-box left intact)', + exit_code: 1, + }, + ]); + + const result = await pollSingBoxComponentAction(fetchStatus, noSleep); + + expect(result.success).toBe(false); + expect(result.message).toBe( + 'core switch aborted (existing sing-box left intact)', + ); + }); + + it('treats a parse failure (null status) as terminal failure', async () => { + const fetchStatus = makeFetchStatus([ + { running: true, success: true }, + null, + ]); + + const result = await pollSingBoxComponentAction(fetchStatus, noSleep); + + expect(result.success).toBe(false); + expect(result.message).toBe('Core switch failed'); + }); + + it('returns timeout when the safety cap is exceeded', async () => { + // Always running → never terminal. + const fetchStatus = vi.fn(async () => ({ running: true, success: true })); + + const result = await pollSingBoxComponentAction(fetchStatus, noSleep, 0, 5); + + expect(result.success).toBe(false); + expect(result.message).toBe('Core switch timed out'); + expect(fetchStatus).toHaveBeenCalledTimes(5); + }); + + it('returns immediately on a terminal-first status', async () => { + const fetchStatus = makeFetchStatus([ + { + running: false, + success: true, + version: '1.13.0', + message: 'done', + }, + ]); + + const result = await pollSingBoxComponentAction(fetchStatus, noSleep); + + expect(result).toEqual({ + success: true, + version: '1.13.0', + message: 'done', + }); + expect(fetchStatus).toHaveBeenCalledTimes(1); + }); +}); diff --git a/fe-app-netshift/src/netshift/methods/shell/tests/singBoxComponentAction.test.js b/fe-app-netshift/src/netshift/methods/shell/tests/singBoxComponentAction.test.js new file mode 100644 index 00000000..ab6f448c --- /dev/null +++ b/fe-app-netshift/src/netshift/methods/shell/tests/singBoxComponentAction.test.js @@ -0,0 +1,79 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +// `methods/shell/index.ts` (and its `callBaseMethod` import) pull in the +// `../../../helpers` barrel, which transitively loads `withTimeout` → +// `../netshift` → `TabService`, whose constructor calls `new MutationObserver` +// at module-init time. In the node test env that throws +// "MutationObserver is not defined" at COLLECT time. Mocking the helpers barrel +// here short-circuits that chain so we can import and exercise the REAL +// `singBoxComponentAction` method while controlling its `executeShellCommand`. +const executeShellCommand = vi.fn(); + +vi.mock('../../../../helpers', () => ({ + executeShellCommand: (...args) => executeShellCommand(...args), +})); + +// Avoid pulling the real `callBaseMethod` (also imports the helpers barrel and +// the LuCI types); the start-failure path under test never reaches it. +vi.mock('../../callBaseMethod', () => ({ + callBaseMethod: vi.fn(), +})); + +const { NetShiftShellMethods } = await import('../index'); + +afterEach(() => { + executeShellCommand.mockReset(); +}); + +describe('singBoxComponentAction (start-failure path)', () => { + it('fails fast on start success:false WITHOUT entering the poll loop', async () => { + executeShellCommand.mockResolvedValueOnce({ + stdout: JSON.stringify({ + success: false, + message: 'binary updater is busy', + }), + stderr: '', + }); + + const result = + await NetShiftShellMethods.singBoxComponentAction('install_extended'); + + expect(result).toEqual({ + success: false, + message: 'binary updater is busy', + }); + // Only the async-start call ran; no `component_action_status` polls. + expect(executeShellCommand).toHaveBeenCalledTimes(1); + expect(executeShellCommand).toHaveBeenCalledWith({ + command: '/usr/bin/netshift', + args: ['component_action_async', 'sing_box', 'install_extended'], + }); + }); + + it('fails fast when the start response has no job_id', async () => { + executeShellCommand.mockResolvedValueOnce({ + stdout: JSON.stringify({ success: true }), + stderr: '', + }); + + const result = + await NetShiftShellMethods.singBoxComponentAction('install_stable'); + + expect(result.success).toBe(false); + expect(executeShellCommand).toHaveBeenCalledTimes(1); + }); + + it('surfaces stderr / generic message when start output is unparseable', async () => { + executeShellCommand.mockResolvedValueOnce({ + stdout: 'not json', + stderr: 'boom', + }); + + const result = + await NetShiftShellMethods.singBoxComponentAction('install_extended'); + + expect(result.success).toBe(false); + expect(result.message).toBe('boom'); + expect(executeShellCommand).toHaveBeenCalledTimes(1); + }); +}); diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts index fd515d18..68d74970 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts @@ -327,6 +327,11 @@ async function handleInstallSingBox() { const isExtended = store.get().diagnosticsSystemInfo.sing_box_extended === 1; + showToast( + _('Switching sing-box core, this may take a few minutes…'), + 'success', + ); + try { const result = await NetShiftShellMethods.singBoxComponentAction( isExtended ? 'install_stable' : 'install_extended', diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 9f2f0f8d..57e7bfa3 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -699,6 +699,45 @@ var NetShift; })(AvailableClashAPIMethods = NetShift2.AvailableClashAPIMethods || (NetShift2.AvailableClashAPIMethods = {})); })(NetShift || (NetShift = {})); +// src/netshift/methods/shell/sleep.ts +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// src/netshift/methods/shell/pollSingBoxComponentAction.ts +var POLL_INTERVAL_MS = 2e3; +var MAX_POLLS = 150; +function parseComponentActionStatus(stdout) { + try { + return JSON.parse(stdout); + } catch (_e) { + return null; + } +} +async function pollSingBoxComponentAction(fetchStatus, sleepFn = sleep, intervalMs = POLL_INTERVAL_MS, maxPolls = MAX_POLLS) { + for (let poll = 0; poll < maxPolls; poll += 1) { + const status = await fetchStatus(); + if (!status) { + return { + success: false, + message: _("Core switch failed") + }; + } + if (status.running !== true) { + return { + success: Boolean(status.success), + version: status.version, + message: status.message + }; + } + await sleepFn(intervalMs); + } + return { + success: false, + message: _("Core switch timed out") + }; +} + // src/netshift/methods/shell/index.ts var NetShiftShellMethods = { checkDNSAvailable: async () => callBaseMethod( @@ -766,32 +805,65 @@ var NetShiftShellMethods = { ), subscriptionUpdate: async () => callBaseMethod(NetShift.AvailableMethods.SUBSCRIPTION_UPDATE), singBoxComponentAction: async (action) => { - const response = await executeShellCommand({ + if (action === "check_update") { + const response = await executeShellCommand({ + command: "/usr/bin/netshift", + args: ["component_action", "sing_box", action], + timeout: 6e5 + }); + if (response.stdout) { + try { + const parsed = JSON.parse( + response.stdout + ); + return { + success: Boolean(parsed.success), + version: parsed.version, + message: parsed.message + }; + } catch (_e) { + return { + success: false, + message: response.stdout + }; + } + } + return { + success: false, + message: response.stderr || "" + }; + } + const startResponse = await executeShellCommand({ command: "/usr/bin/netshift", - args: ["component_action", "sing_box", action], - timeout: 6e5 + args: ["component_action_async", "sing_box", action] }); - if (response.stdout) { + let start = null; + if (startResponse.stdout) { try { - const parsed = JSON.parse( - response.stdout + start = JSON.parse( + startResponse.stdout ); - return { - success: Boolean(parsed.success), - version: parsed.version, - message: parsed.message - }; } catch (_e) { - return { - success: false, - message: response.stdout - }; + start = null; } } - return { - success: false, - message: response.stderr || "" - }; + if (!start || start.success !== true || !start.job_id) { + return { + success: false, + message: start?.message || startResponse.stderr || _("Core switch failed") + }; + } + const jobId = start.job_id; + return pollSingBoxComponentAction(async () => { + const statusResponse = await executeShellCommand({ + command: "/usr/bin/netshift", + args: ["component_action_status", jobId] + }); + if (!statusResponse.stdout) { + return null; + } + return parseComponentActionStatus(statusResponse.stdout); + }); } }; @@ -4432,6 +4504,10 @@ async function handleInstallSingBox() { } }); const isExtended = store.get().diagnosticsSystemInfo.sing_box_extended === 1; + showToast( + _("Switching sing-box core, this may take a few minutes\u2026"), + "success" + ); try { const result = await NetShiftShellMethods.singBoxComponentAction( isExtended ? "install_stable" : "install_extended" diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index e78ec4b8..2aa7144c 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-04 22:59+0300\n" -"PO-Revision-Date: 2026-06-04 22:59+0300\n" +"POT-Creation-Date: 2026-06-05 09:28+0300\n" +"PO-Revision-Date: 2026-06-05 09:28+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -116,6 +116,12 @@ msgstr "URL подключения" msgid "Copy" msgstr "Копировать" +msgid "Core switch failed" +msgstr "Не удалось переключить ядро" + +msgid "Core switch timed out" +msgstr "Истекло время ожидания переключения ядра" + msgid "Currently unavailable" msgstr "Временно недоступно" @@ -749,6 +755,9 @@ msgstr "" msgid "Successfully copied!" msgstr "Успешно скопировано!" +msgid "Switching sing-box core, this may take a few minutes…" +msgstr "Переключение ядра sing-box, это может занять несколько минут…" + msgid "System info" msgstr "Системная информация" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index d3a9259d..ecafb2d4 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-04 19:59+0300\n" -"PO-Revision-Date: 2026-06-04 19:59+0300\n" +"POT-Creation-Date: 2026-06-05 06:27+0300\n" +"PO-Revision-Date: 2026-06-05 06:27+0300\n" "Last-Translator: yandexru45 <sukadark228@gmail.com>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -155,6 +155,15 @@ msgstr "" msgid "Copy" msgstr "" +#: src\netshift\methods\shell\index.ts:157 +#: src\netshift\methods\shell\pollSingBoxComponentAction.ts:65 +msgid "Core switch failed" +msgstr "" + +#: src\netshift\methods\shell\pollSingBoxComponentAction.ts:82 +msgid "Core switch timed out" +msgstr "" + #: src\netshift\tabs\dashboard\partials\renderWidget.ts:22 msgid "Currently unavailable" msgstr "" @@ -389,8 +398,8 @@ msgstr "" #: src\netshift\tabs\diagnostic\initController.ts:267 #: src\netshift\tabs\diagnostic\initController.ts:304 #: src\netshift\tabs\diagnostic\initController.ts:308 -#: src\netshift\tabs\diagnostic\initController.ts:342 -#: src\netshift\tabs\diagnostic\initController.ts:346 +#: src\netshift\tabs\diagnostic\initController.ts:347 +#: src\netshift\tabs\diagnostic\initController.ts:351 msgid "Failed to execute!" msgstr "" @@ -960,7 +969,7 @@ msgstr "" msgid "Sing-box autostart disabled" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:337 +#: src\netshift\tabs\diagnostic\initController.ts:342 msgid "Sing-box core changed, version:" msgstr "" @@ -1033,6 +1042,10 @@ msgstr "" msgid "Successfully copied!" msgstr "" +#: src\netshift\tabs\diagnostic\initController.ts:331 +msgid "Switching sing-box core, this may take a few minutes…" +msgstr "" + #: src\netshift\tabs\dashboard\initController.ts:304 msgid "System info" msgstr "" From 7b261dbc1ef1525c172a92a6d8ada731d4e2444c Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Fri, 5 Jun 2026 12:30:04 +0300 Subject: [PATCH 48/75] =?UTF-8?q?=D0=BF=D0=BE=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=87=D1=83=D0=B2=D1=81=D1=82=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 15 ++++ docs/agent-rules/memory/code-reviewer.md | 2 + .../memory/shell-backend-developer.md | 24 +++++ .../files/usr/lib/sing_box_config_facade.sh | 26 ++++-- tests/entrypoint.sh | 88 ++++++++++++++++++- 5 files changed, 148 insertions(+), 7 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index d3168dd3..0ef9d2a5 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -183,6 +183,21 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> gate generation behind `is_sing_box_extended` and fail safe (warn + skip) when stock sing-box is installed, exactly like xhttp does today. +## Subscription keyword filter — Cyrillic case bug (task-010, found on hardware 2026-06) + +- REAL bug (not version skew): the keyword filter's "case-insensitive" claim only + holds for ASCII. `sing_box_cf_prepare_subscription_batch` + (sing_box_config_facade.sh:542/543/567) uses jq `ascii_downcase`, which does + NOT lowercase Cyrillic (or any non-ASCII). +- FIX: replace the 3 `ascii_downcase` in prepare_subscription_batch with an inline + jq `def ucfold` (codepoint arithmetic, NO Oniguruma): ASCII A-Z (65–90)+32, + Cyrillic А-Я (1040–1071)+32, Ё(1025)->ё(1105). Apply to BOTH the keyword list + and the node name. `explode`/`map`/`implode`/`index` all work on the device jq. + (Это inline — этот jq-вызов НЕ импортирует helpers.jq.) +- rejected-hash (`<section>.rejected`, md5 of body) can wedge a retry storm if a + STUB body once got cached as rejected; it self-clears once a real body downloads + (return 0 path rm's it). Not the root cause here but amplified the symptom. + ## Workflow facts - Contribution gating: `CODEOWNERS=@yandexru45`; PRs accepted only after Telegram diff --git a/docs/agent-rules/memory/code-reviewer.md b/docs/agent-rules/memory/code-reviewer.md index e37ba421..cee972eb 100644 --- a/docs/agent-rules/memory/code-reviewer.md +++ b/docs/agent-rules/memory/code-reviewer.md @@ -62,3 +62,5 @@ append recurring findings; keep under ~200 lines. - Wrapper/core split for always-run cleanup (task-009 core-switch): verify the public wrapper captures core stdout to a temp file + rc, then UNCONDITIONALLY calls restore/cleanup before re-emitting JSON and return rc; confirm the worker runs without set -e (else a non-zero core rc could skip trailing cleanup) and that _*_core never exits. For never-end-core-less rollbacks, confirm the tmpfs backup happens BEFORE the package manager/extract touches the binary and is dropped ONLY on the confirmed-good path; strongest test deletes the live mock binary on simulated failure and asserts original bytes restored. - Frontend barrel exposure: anything added to src/helpers/index.ts (or any export* barrel reaching main.ts) AND actually used appears in the generated main.js baseclass.extend block as a main.* symbol; unused re-exports get tree-shaken. So internal-only helper + added to barrel + used = it WILL leak to main.*. To keep a helper truly internal, place it in the consuming module, not the barrel. + +- OpenWrt jq ascii_downcase only folds ASCII A-Z; case-insensitive matching on Cyrillic/Unicode names needs an inline codepoint fold (explode/map/implode: ASCII 65-90 +32, Cyrillic 1040-1071 +32, Yo 1025->1105). When reviewing such a fold: (a) already-lowercase ranges excluded (no double-fold), (b) def before first use when the program does NOT import helpers.jq, (c) a pure-emoji-keyword exact-match test proves non-folded codepoints pass through unchanged on both sides. (task-010) diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index f5af8dc5..fc130cc6 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -240,3 +240,27 @@ findings; keep under ~200 lines. runs, rc=1 with no FAIL line). Wrap the invocation `... || true` — assertions read JSON/file-state, not rc. (Distinct from the task-007 `$(...)`-capture variant.) + +## task-010: keyword filter case-fold is ASCII+Cyrillic (not just ASCII) + +- **`ascii_downcase` only folds ASCII A-Z** — Cyrillic server tags (e.g. + `Германия`) stayed mixed-case, so a Cyrillic include keyword in any other + case matched 0 nodes → kept=0 → blocked outbound (hardware-confirmed: include + `[ГеРма,пОЛЬш,рос]` over 316 outbounds gave 0 before, 28 after). +- Fix lives in `sing_box_cf_prepare_subscription_batch` + (`sing_box_config_facade.sh`). That jq call does NOT `import` helpers.jq, so the + fold is defined **inline** at the top of the program as `def ucfold:` using only + `explode`/`map`/`implode` (NO Oniguruma): ASCII `65-90`→`+32`, Cyrillic + `1040-1071` (А-Я)→`+32`, and the single out-of-block `Ё` `1025`→`1105` (ё). + Everything else (emoji/other scripts) passes through unchanged → still matches + as exact codepoint substrings. Replaced the 3 `ascii_downcase` uses (the two + `$inc`/`$exc` list normalizers + the `$name | ucfold` in the select). The + `index()`-based `name_passes_keywords` substring logic is unchanged. +- Cyrillic codepoints: А-Я = 1040-1071, а-я = 1072-1103 (so +32), Ё = 1025 + sits BEFORE the block, ё = 1105 sits AFTER it — hence the special-case branch. +- Smoke: extended the existing FBEOF block in `test_subscription` with CASE K + (Cyrillic). No new top-level test / registration needed — it rides the existing + `subscription` category. Synthetic names with literal UTF-8 (`Германия`, + `Орёл`, etc.) in the heredoc are fine; assert via `.count`/`.names`. Used a + `case "$x" in *Польша*)` membership check rather than exact-name compare for the + exclude case (order-independent). All ran green in-container. diff --git a/netshift/files/usr/lib/sing_box_config_facade.sh b/netshift/files/usr/lib/sing_box_config_facade.sh index 08b1306d..28b41287 100644 --- a/netshift/files/usr/lib/sing_box_config_facade.sh +++ b/netshift/files/usr/lib/sing_box_config_facade.sh @@ -535,13 +535,27 @@ sing_box_cf_prepare_subscription_batch() { --argjson extended "$sing_box_extended" \ --argjson include_keywords "$include_keywords_json" \ --argjson exclude_keywords "$exclude_keywords_json" ' + # Codepoint-based case fold. OpenWrt jq has no Oniguruma and ascii_downcase + # only maps ASCII A-Z (leaving Cyrillic mixed-case), so define an inline + # fold (this jq program does NOT import helpers.jq). It lowercases ASCII + # AND Cyrillic (incl. the Yo letter outside the contiguous block); emoji + # and all other scripts pass through unchanged and thus match as exact + # codepoint substrings. + def ucfold: + explode + | map( + if (. >= 65 and . <= 90) then . + 32 # ASCII A-Z -> a-z + elif (. >= 1040 and . <= 1071) then . + 32 # Cyrillic А-Я -> а-я + elif (. == 1025) then 1105 # Ё -> ё + else . end) + | implode; # Normalise the keyword lists: drop empty items and precompute the - # ASCII-lowercased form once. ascii_downcase only touches ASCII, so - # emoji/Cyrillic keywords are matched as exact byte-substrings. + # case-folded form once. ucfold folds ASCII and Cyrillic; emoji/other + # scripts are matched as exact codepoint substrings. # NB: "include"/"exclude" are reserved jq keywords, hence $inc/$exc. - ([$include_keywords[]? | tostring | select(length > 0) | ascii_downcase]) as $inc - | ([$exclude_keywords[]? | tostring | select(length > 0) | ascii_downcase]) as $exc - # A node "matches" a normalised keyword list when its lowercased name + ([$include_keywords[]? | tostring | select(length > 0) | ucfold]) as $inc + | ([$exclude_keywords[]? | tostring | select(length > 0) | ucfold]) as $exc + # A node "matches" a normalised keyword list when its case-folded name # contains any of the keywords (substring via index, NO regex/Oniguruma). # Bind each keyword to $kw so index() receives the keyword, not the name. | def name_passes_keywords($lc): @@ -564,7 +578,7 @@ sing_box_cf_prepare_subscription_batch() { | [$all_candidates[] | . as $ob | (($ob.remark // $ob.tag // "") | tostring) as $name - | select(name_passes_keywords($name | ascii_downcase)) + | select(name_passes_keywords($name | ucfold)) ] as $candidates | ($candidates | length) as $total # Statically reject outbounds the current sing-box build cannot load. diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 07f0ee84..563e176d 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1218,7 +1218,7 @@ fi # Drive sing_box_cf_prepare_subscription_batch directly with a synthetic # subscription JSON and assert kept counts/names for include/exclude lists. # Matching: substring, OR across keywords, ASCII case-insensitive, byte-exact -# for non-ASCII (emoji/Cyrillic). No jq regex (index + ascii_downcase only). +# for non-folded scripts (emoji/etc). No jq regex (index + inline ucfold only). caseJ_cfg='{"outbounds":[]}' caseJ_sub="/tmp/netshift-fb-caseJ-$$.json" cat > "$caseJ_sub" << 'JSUB' @@ -1311,6 +1311,92 @@ else fi rm -f "$caseJ_sub" +# ── CASE K: Cyrillic + Ё/ё case-fold (task-010) ───────────────────── +# The keyword filter must fold ASCII AND Cyrillic (inline ucfold), so a +# mixed-case Cyrillic keyword matches a mixed-case Cyrillic server name. +# Emoji keywords still match by exact codepoints; ASCII is unaffected. +caseK_sub="/tmp/netshift-fb-caseK-$$.json" +cat > "$caseK_sub" << 'KSUB' +{ + "outbounds": [ + {"type": "shadowsocks", "tag": "🇩🇪 Германия", "server": "a.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"}, + {"type": "shadowsocks", "tag": "🇵🇱 Польша", "server": "b.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"}, + {"type": "shadowsocks", "tag": "🇰🇿 Казахстан", "server": "c.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"}, + {"type": "shadowsocks", "tag": "Орёл", "server": "d.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"}, + {"type": "shadowsocks", "tag": "US grpc", "server": "e.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"} + ] +} +KSUB + +caseK_count() { + sing_box_cf_prepare_subscription_batch "$caseJ_cfg" "$caseK_sub" "$1" "$2" | + jq -r '.count // -1' +} +caseK_names() { + sing_box_cf_prepare_subscription_batch "$caseJ_cfg" "$caseK_sub" "$1" "$2" | + jq -r '(.names // []) | join(",")' +} + +# (1) include mixed-case Cyrillic ["ГеРма"] keeps Германия (was 0 before fix). +caseK_mixed_count="$(caseK_count '["ГеРма"]' '[]')" +caseK_mixed_names="$(caseK_names '["ГеРма"]' '[]')" +if [ "$caseK_mixed_count" = "1" ] && [ "$caseK_mixed_names" = "🇩🇪 Германия" ]; then + echo 'fb-caseK-cyrillic-mixed-include:OK' +else + echo "fb-caseK-cyrillic-mixed-include(count=$caseK_mixed_count names='$caseK_mixed_names'):FAIL" +fi + +# (2) lower ["германия"] and upper ["ГЕРМАНИЯ"] both keep Германия. +caseK_lower_count="$(caseK_count '["германия"]' '[]')" +caseK_upper_count="$(caseK_count '["ГЕРМАНИЯ"]' '[]')" +if [ "$caseK_lower_count" = "1" ] && [ "$caseK_upper_count" = "1" ]; then + echo 'fb-caseK-cyrillic-lower-upper-include:OK' +else + echo "fb-caseK-cyrillic-lower-upper-include(lower=$caseK_lower_count upper=$caseK_upper_count):FAIL" +fi + +# (3) exclude ["польша"] (lower) drops Польша (upper-P name) regardless of case. +caseK_exc_count="$(caseK_count '[]' '["польша"]')" +caseK_exc_names="$(caseK_names '[]' '["польша"]')" +case "$caseK_exc_names" in + *Польша*) caseK_exc_has_pl=1 ;; + *) caseK_exc_has_pl=0 ;; +esac +if [ "$caseK_exc_count" = "4" ] && [ "$caseK_exc_has_pl" = "0" ]; then + echo 'fb-caseK-cyrillic-exclude:OK' +else + echo "fb-caseK-cyrillic-exclude(count=$caseK_exc_count names='$caseK_exc_names'):FAIL" +fi + +# (4) Ё/ё fold: name "Орёл" matched by lower "орёл" and upper "ОРЁЛ". +caseK_yo_lower="$(caseK_count '["орёл"]' '[]')" +caseK_yo_upper="$(caseK_count '["ОРЁЛ"]' '[]')" +caseK_yo_names="$(caseK_names '["ОРЁЛ"]' '[]')" +if [ "$caseK_yo_lower" = "1" ] && [ "$caseK_yo_upper" = "1" ] && [ "$caseK_yo_names" = "Орёл" ]; then + echo 'fb-caseK-yo-fold:OK' +else + echo "fb-caseK-yo-fold(lower=$caseK_yo_lower upper=$caseK_yo_upper names='$caseK_yo_names'):FAIL" +fi + +# (5) emoji keyword ["🇰🇿"] keeps Казахстан by exact codepoint match. +caseK_emoji_count="$(caseK_count '["🇰🇿"]' '[]')" +caseK_emoji_names="$(caseK_names '["🇰🇿"]' '[]')" +if [ "$caseK_emoji_count" = "1" ] && [ "$caseK_emoji_names" = "🇰🇿 Казахстан" ]; then + echo 'fb-caseK-emoji-flag-include:OK' +else + echo "fb-caseK-emoji-flag-include(count=$caseK_emoji_count names='$caseK_emoji_names'):FAIL" +fi + +# (6) ASCII no regression: include ["GRPC"] still keeps the "US grpc" node. +caseK_ascii_count="$(caseK_count '["GRPC"]' '[]')" +caseK_ascii_names="$(caseK_names '["GRPC"]' '[]')" +if [ "$caseK_ascii_count" = "1" ] && [ "$caseK_ascii_names" = "US grpc" ]; then + echo 'fb-caseK-ascii-no-regression:OK' +else + echo "fb-caseK-ascii-no-regression(count=$caseK_ascii_count names='$caseK_ascii_names'):FAIL" +fi +rm -f "$caseK_sub" + echo 'DONE' FBEOF From c39c66f89c5343cc27e8bd755f9ca1e602e8e9b9 Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Fri, 5 Jun 2026 12:53:20 +0300 Subject: [PATCH 49/75] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D1=87=D0=B8=D1=81=D1=82=D0=BA=D1=83=20=D0=BA=D0=B5=D1=88=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BA=D0=B8,=20=D0=B5?= =?UTF-8?q?=D1=81=D0=BB=D0=B8=20=D0=B2=D0=BE=D0=B7=D0=BD=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=20dead=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/shell-backend-developer.md | 43 ++++ netshift/files/usr/bin/netshift | 40 +++- tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 190 +++++++++++++++++- 4 files changed, 269 insertions(+), 6 deletions(-) diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index fc130cc6..00618f01 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -264,3 +264,46 @@ findings; keep under ~200 lines. `Орёл`, etc.) in the heredoc are fine; assert via `.count`/`.names`. Used a `case "$x" in *Польша*)` membership check rather than exact-name compare for the exclude case (order-independent). All ran green in-container. + +## task-011: keyword filter must not poison the subscription rejected-hash + +- Root cause of the hardware re-download loop: `mark_subscription_outbound_unavailable` + (`bin/netshift`) md5'd the VALID `<section>.json` and wrote it to `.rejected` + even when `kept=0` was caused purely by the user's keyword filter (a setting, + not a bad feed). Then `subscription_cache_is_usable` — which had already passed + `validate_subscription_file` — still returned 1 on the hash match, forcing a + re-download; `download_subscription_into_cache` saw tmp_hash==rejected_hash and + `return 14` (unchanged+rejected) → infinite retry. The poison also survived + loosening the filter (lived only in `.rejected`). +- Fix A: 2nd arg `keyword_filter_active="${2:-0}"`. When 1: NEVER compute/write + the hash, `rm -f` the `.rejected` (self-heals a previously poisoned hash), still + set unavailable state + `subscription_startup_blocked=1`, warn that the FILTER + (not the feed) emptied the set. When 0: unchanged (genuine outbound-less body + still recorded → flash-loop guard kept). Caller at the `subscription)` branch + passes `$subscription_keyword_filter_active` (set 0/1 just above from the two + UCI keyword lists). +- Fix B: in `subscription_cache_is_usable`, after `validate_subscription_file`, + run a jq -e "has >=1 proxy outbound" check (same predicate as the batch: + `[.outbounds[]? | select(.type != "selector" and ... != "block")] | length > 0`, + NO Oniguruma) → if true `return 0` (usable) regardless of `.rejected`. The + rejected-hash veto now only fires on a validated-but-outbound-less body. NB: + `validate_subscription_file` ALREADY requires length>0, so a 0-proxy body fails + validation first — B is belt-and-suspenders + self-documenting, and robust if + validation ever loosens. Did NOT touch `download_subscription_into_cache`'s own + rejected logic (spec: once A/B stop writing+vetoing, a valid body has no + `.rejected` so return 14 can't fire for it). +- **Testing functions that live in `bin/netshift` (not a lib):** can't source the + file (it runs the dispatcher + needs LuCI `/lib/functions.sh`). Pattern that + works: a generated driver that (1) stubs the few helpers the target calls + (`log`, the `get_subscription_*_path` builders), (2) sources `helpers.sh` for + the real `validate_subscription_file`, (3) extracts JUST the target functions + verbatim with awk and `eval`s them: + `eval "$(awk '/^fname\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "$bin")"`. Relies on + top-level functions closing with a column-0 `}` and having no nested column-0 + `}` (case/if/while bodies don't). Keeps the test against shipped code, not a copy. +- New top-level smoke test `test_rejected_hash` (alias `rejected`): 6 cases + (A-no-write+clear, A-recovery, B-not-vetoed, A-protected-no-proxy-still-vetoed, + regression-usable, A-arg0-genuine-recorded). Registered in `all)`, case alias, + usage "Available:" line, docker-compose comment. Same name:OK/FAIL parse + the + subshell-pipe PASS-counter quirk as test_subscription (suite `Results:` total + omits piped-while passes; the per-test ✓ marks are the source of truth). diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 963e7bc6..5955953b 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -228,6 +228,7 @@ migrate_subscription_cache_from_tmp() { mark_subscription_outbound_unavailable() { local section="$1" + local keyword_filter_active="${2:-0}" local subscription_json_path rejected_cache_path rejected_hash case " $SUBSCRIPTION_UNAVAILABLE_SECTIONS " in @@ -235,12 +236,26 @@ mark_subscription_outbound_unavailable() { *) SUBSCRIPTION_UNAVAILABLE_SECTIONS="$SUBSCRIPTION_UNAVAILABLE_SECTIONS $section" ;; esac + subscription_json_path="$(get_subscription_json_path "$section")" + rejected_cache_path="$(get_subscription_rejected_cache_path "$section")" + + if [ "$keyword_filter_active" -eq 1 ]; then + # The empty result came from the user's keyword filter, not from a bad + # feed: the body itself may be perfectly valid. Recording its hash here + # would poison the cache and wedge future downloads (return 14 loop), and + # would also survive the user loosening the filter. So never write the + # rejected-hash for this case, and proactively clear any stale one so a + # body poisoned by an earlier empty-filter run self-heals on this pass. + rm -f "$rejected_cache_path" + log "Subscription keyword filter for section '$section' removed all nodes; matching traffic for this section will be rejected until the filter is loosened or a matching node appears (the feed itself is not rejected)" "warn" + subscription_startup_blocked=1 + return 0 + fi + log "Subscription cache for section '$section' is unavailable; matching traffic for this section will be rejected until refresh succeeds" "warn" # A structurally valid subscription can still contain no sing-box usable # proxy outbounds. Remember its hash so the retry worker does not persist, # restart and reject exactly the same unusable feed in a flash-writing loop. - subscription_json_path="$(get_subscription_json_path "$section")" - rejected_cache_path="$(get_subscription_rejected_cache_path "$section")" rejected_hash="$(md5sum "$subscription_json_path" 2>/dev/null | awk '{print $1}')" if [ -n "$rejected_hash" ] && [ "$(cat "$rejected_cache_path" 2>/dev/null)" != "$rejected_hash" ]; then printf '%s' "$rejected_hash" > "${rejected_cache_path}.tmp.$$" && mv "${rejected_cache_path}.tmp.$$" "$rejected_cache_path" @@ -306,12 +321,29 @@ get_subscription_download_proxy_address() { subscription_cache_is_usable() { local subscription_json_path="$1" - local rejected_cache_path current_hash rejected_hash + local rejected_cache_path current_hash rejected_hash has_proxy_outbound [ -s "$subscription_json_path" ] || return 1 validate_subscription_file "$subscription_json_path" || return 1 + # The rejected-hash veto must only be able to reject a body that does NOT + # actually contain usable proxy outbounds (the genuine flash-loop guard). A + # structurally valid body with >=1 proxy outbound is a real subscription and + # is always usable, regardless of any stale rejected-hash (e.g. one poisoned + # by an over-strict keyword filter before task-011). Same predicate as the + # batch (candidate = not selector/urltest/direct/dns/block); no Oniguruma. + has_proxy_outbound="$(jq -e ' + [.outbounds[]? | select( + .type != "selector" and + .type != "urltest" and + .type != "direct" and + .type != "dns" and + .type != "block" + )] | length > 0 + ' "$subscription_json_path" >/dev/null 2>&1 && echo 1 || echo 0)" + [ "$has_proxy_outbound" = "1" ] && return 0 + rejected_cache_path="${subscription_json_path%.json}.rejected" if [ -s "$rejected_cache_path" ]; then current_hash="$(md5sum "$subscription_json_path" 2>/dev/null | awk '{print $1}')" @@ -1638,7 +1670,7 @@ configure_outbound_handler() { # where add_subscription_outbounds actually ran the filter), so we # do not re-warn here to avoid misattribution / double-warning. log "Subscription cache for section '$section' is unavailable or empty; using a temporary blocked outbound" "warn" - mark_subscription_outbound_unavailable "$section" + mark_subscription_outbound_unavailable "$section" "$subscription_keyword_filter_active" else selector_tag="$(get_outbound_tag_by_section "$section")" subscription_outbound_tags_json="$SUBSCRIPTION_OUTBOUND_TAGS_JSON" diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 45739ec0..1a0cbb98 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -9,7 +9,7 @@ # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, -# diagnostics, subscription, jobstate, selfheal +# diagnostics, subscription, rejected, jobstate, selfheal # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 563e176d..4e02abe9 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1994,6 +1994,192 @@ CURL5EOF rm -rf "$work" } +# ───────────────────────────────────────────────────────────────── +# Test: Subscription rejected-hash validity (task-011) +# ───────────────────────────────────────────────────────────────── +# Verifies the keyword-filter no longer poisons the per-section .rejected hash +# and that a structurally valid body with >=1 proxy outbound is never vetoed by +# a stale rejected-hash, while a genuinely outbound-less body still is. The two +# functions under test (mark_subscription_outbound_unavailable, +# subscription_cache_is_usable) live in /usr/bin/netshift, not a sourceable lib, +# so a tiny driver extracts JUST those two functions verbatim from the live bin +# (awk between the `name() {` line and the matching column-0 `}`), stubs the few +# helpers they call (log + the path builders), sources helpers.sh for the real +# validate_subscription_file, and re-pins SUBSCRIPTION_CACHE_FOLDER to a temp +# dir. Tokens use the same name:OK/FAIL convention as test_subscription. +test_rejected_hash() { + header "Subscription Rejected-Hash Validity (task-011)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local bin="${NETSHIFT_SRC}/usr/bin/netshift" + local helpers="${NETSHIFT_LIB_DIR}/helpers.sh" + if [ ! -r "$bin" ] || [ ! -r "$helpers" ]; then + skip "netshift bin / helpers.sh not found" + return + fi + + local drv="/tmp/netshift-rejected-$$.sh" + cat > "$drv" << 'RHEOF' +# Isolated cache dir for this run (the path builders read SUBSCRIPTION_CACHE_FOLDER). +SUBSCRIPTION_CACHE_FOLDER="${RH_CACHE_DIR:-/tmp/netshift-rejected-cache}" +mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" + +# Quiet stubs for the logger used by the functions under test. +log() { :; } +echolog() { :; } +nolog() { :; } + +# Path builders are tiny; stub them exactly like the bin so the functions +# resolve the temp cache dir. +get_subscription_json_path() { echo "$SUBSCRIPTION_CACHE_FOLDER/${1}.json"; } +get_subscription_rejected_cache_path() { echo "$SUBSCRIPTION_CACHE_FOLDER/${1}.rejected"; } + +# Real validate_subscription_file from helpers.sh (no other deps needed). +. "HELPERS_PATH" + +# Pull the two functions under test VERBATIM out of the live bin so the test +# exercises the shipped code, not a copy. awk grabs from the function opener to +# its matching column-0 closing brace. +eval "$(awk '/^mark_subscription_outbound_unavailable\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")" +eval "$(awk '/^subscription_cache_is_usable\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")" + +# Globals the functions touch. +SUBSCRIPTION_UNAVAILABLE_SECTIONS="" +subscription_startup_blocked=0 + +valid_body='{ + "outbounds": [ + {"type": "shadowsocks", "tag": "ss-01", "server": "a.example.com", "server_port": 443, "method": "aes-256-gcm", "password": "p"}, + {"type": "selector", "tag": "select", "outbounds": ["ss-01"]} + ] +}' + +# ── CASE 1: A — over-strict keyword filter (kept=0) must NOT write .rejected, +# and must remove a pre-existing one. ───────────────────────────── +s1="sec1" +printf '%s' "$valid_body" > "$(get_subscription_json_path "$s1")" +# Pre-poison with this body's hash; arg=1 (keyword filter) must clear it. +md5sum "$(get_subscription_json_path "$s1")" | awk '{print $1}' \ + > "$(get_subscription_rejected_cache_path "$s1")" +mark_subscription_outbound_unavailable "$s1" 1 +if [ ! -e "$(get_subscription_rejected_cache_path "$s1")" ]; then + echo 'rh-case1-filter-no-rejected:OK' +else + echo 'rh-case1-filter-no-rejected:FAIL' +fi +if [ "$subscription_startup_blocked" = "1" ]; then + echo 'rh-case1-blocked-state-set:OK' +else + echo 'rh-case1-blocked-state-set:FAIL' +fi + +# ── CASE 2: A-recovery — pre-existing .rejected == a valid body hash, call with +# arg=1, assert .rejected gone (self-heal). ──────────────────────── +s2="sec2" +printf '%s' "$valid_body" > "$(get_subscription_json_path "$s2")" +md5sum "$(get_subscription_json_path "$s2")" | awk '{print $1}' \ + > "$(get_subscription_rejected_cache_path "$s2")" +[ -s "$(get_subscription_rejected_cache_path "$s2")" ] && pre2=1 || pre2=0 +mark_subscription_outbound_unavailable "$s2" 1 +if [ "$pre2" = "1" ] && [ ! -e "$(get_subscription_rejected_cache_path "$s2")" ]; then + echo 'rh-case2-recovery-rejected-removed:OK' +else + echo 'rh-case2-recovery-rejected-removed:FAIL' +fi + +# ── CASE 3: B — valid body with >=1 proxy outbound + .rejected == its hash ⇒ +# subscription_cache_is_usable returns 0 (usable). ───────────────── +s3="sec3" +s3_json="$(get_subscription_json_path "$s3")" +printf '%s' "$valid_body" > "$s3_json" +md5sum "$s3_json" | awk '{print $1}' > "$(get_subscription_rejected_cache_path "$s3")" +if subscription_cache_is_usable "$s3_json"; then + echo 'rh-case3-valid-body-not-vetoed:OK' +else + echo 'rh-case3-valid-body-not-vetoed:FAIL' +fi + +# ── CASE 4: A-protected — a JSON body with ZERO proxy outbounds whose hash is in +# .rejected ⇒ still vetoed (return 1). validate_subscription_file +# itself requires >=1 proxy outbound, so an outbound-less body is +# rejected at validation; this case proves the guard still holds. ── +s4="sec4" +s4_json="$(get_subscription_json_path "$s4")" +cat > "$s4_json" << 'NOPROXY' +{ + "outbounds": [ + {"type": "selector", "tag": "select", "outbounds": []}, + {"type": "direct", "tag": "direct"}, + {"type": "block", "tag": "block"} + ] +} +NOPROXY +md5sum "$s4_json" | awk '{print $1}' > "$(get_subscription_rejected_cache_path "$s4")" +if subscription_cache_is_usable "$s4_json"; then + echo 'rh-case4-no-proxy-body-vetoed:FAIL' +else + echo 'rh-case4-no-proxy-body-vetoed:OK' +fi + +# ── CASE 5: Regression — a normal valid body, no .rejected ⇒ usable (0). ────── +s5="sec5" +s5_json="$(get_subscription_json_path "$s5")" +printf '%s' "$valid_body" > "$s5_json" +rm -f "$(get_subscription_rejected_cache_path "$s5")" +if subscription_cache_is_usable "$s5_json"; then + echo 'rh-case5-normal-valid-usable:OK' +else + echo 'rh-case5-normal-valid-usable:FAIL' +fi + +# ── CASE 6: A — keyword_filter_active=0 (default) still records the rejected +# hash for a genuinely outbound-less body (flash-loop guard kept). ── +s6="sec6" +s6_json="$(get_subscription_json_path "$s6")" +cat > "$s6_json" << 'NOPROXY' +{ + "outbounds": [ + {"type": "direct", "tag": "direct"}, + {"type": "block", "tag": "block"} + ] +} +NOPROXY +rm -f "$(get_subscription_rejected_cache_path "$s6")" +mark_subscription_outbound_unavailable "$s6" 0 +expect6="$(md5sum "$s6_json" | awk '{print $1}')" +got6="$(cat "$(get_subscription_rejected_cache_path "$s6")" 2>/dev/null)" +if [ -s "$(get_subscription_rejected_cache_path "$s6")" ] && [ "$got6" = "$expect6" ]; then + echo 'rh-case6-genuine-unusable-recorded:OK' +else + echo 'rh-case6-genuine-unusable-recorded:FAIL' +fi + +echo 'DONE' +RHEOF + + sed -i "s|HELPERS_PATH|$helpers|g; s|BIN_PATH|$bin|g" "$drv" + + local rhcache="/tmp/netshift-rejected-cache-$$" + rm -rf "$rhcache" + + RH_CACHE_DIR="$rhcache" ash "$drv" 2>/dev/null | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + *:SKIP) skip "$line" ;; + DONE) ;; + *) ;; + esac + done + + rm -rf "$rhcache" + rm -f "$drv" +} + # ───────────────────────────────────────────────────────────────── # Main # ───────────────────────────────────────────────────────────────── @@ -2018,6 +2204,7 @@ main() { test_nft test_diagnostics test_subscription + test_rejected_hash test_jobstate test_selfheal ;; @@ -2028,6 +2215,7 @@ main() { nft) test_nft ;; diagnostics) test_diagnostics ;; subscription) test_subscription ;; + rejected) test_rejected_hash ;; jobstate) test_jobstate ;; selfheal) test_selfheal ;; jq) test_jq_helpers ;; @@ -2035,7 +2223,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription jobstate selfheal" + echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription rejected jobstate selfheal" exit 1 ;; esac From 4c606e3c19e6760a47cc203932d18350163edc7b Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Fri, 5 Jun 2026 13:29:46 +0300 Subject: [PATCH 50/75] =?UTF-8?q?=D0=BF=D0=BE=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20vmess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/luci-frontend-developer.md | 18 ++++++++++++ .../memory/shell-backend-developer.md | 23 +++++++++++++++ .../validators/tests/validateVmessUrl.test.js | 28 +++++++++++++++++++ .../src/validators/validateVmessUrl.ts | 17 ++++++++--- .../resources/view/netshift/main.js | 7 +++-- netshift/files/usr/lib/helpers.sh | 5 ++++ tests/entrypoint.sh | 22 +++++++++++++++ 7 files changed, 113 insertions(+), 7 deletions(-) diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 4d76e1a5..0ee46bf5 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -168,3 +168,21 @@ append findings; keep under ~200 lines. (`src\\validators\\...`) — generating it on Windows does NOT churn separators. - The proxy-link help string `"vless://, ... links"` is duplicated 3× in `section.js` (proxy_string + selector + urltest fields) — use edit replaceAll. + +## VMess `#fragment` strip (task-012) + +- `vmess://<base64(JSON)>#name` — the `#…` is the server display-name remark + (same as vless/ss/trojan), but for VMess the name also lives in JSON `ps`. + V2RayN base64 NEVER contains `#`, so cut at the FIRST `#` before decode. +- `validateVmessUrl` order MATTERS: derive `body = url.slice('vmess://'.length)` + → `b64 = body.split('#')[0]` → THEN run the `/\s/` whitespace check on `b64` + (NOT the full url), THEN pad/`atob` `b64`. This lets a `#name with spaces` + fragment validate while still rejecting whitespace inside the base64 body. + (The old code ran `/\s/` on the full url and padded `body` incl. fragment → + emoji/Cyrillic in `#🇳🇱Ne` corrupted base64 → "malformed base64".) +- Real-user regression fixture is the long `eyJ…In0=#🇳🇱Ne` literal in the test; + keep a malformed-base64 negative case WITHOUT a `#` (`vmess://@@@@`) so it + still fails for the right reason, and a `vmess://<b64> ` (trailing space, no + `#`) case proving base64-body whitespace is still rejected. +- main.js diff for this fix is exactly the `validateVmessUrl` function body + (body/b64 split + whitespace-on-b64 + pad b64) — expected-only. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 00618f01..ea8d0d79 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -307,3 +307,26 @@ findings; keep under ~200 lines. usage "Available:" line, docker-compose comment. Same name:OK/FAIL parse + the subshell-pipe PASS-counter quirk as test_subscription (suite `Results:` total omits piped-while passes; the per-test ✓ marks are the source of truth). + +## task-012: vmess:// '#fragment' strip before base64 decode + +- Root cause: the `vmess)` case in `sing_box_config_facade.sh` passes the RAW + pre-url_decode link (`$raw_url`, kept that way by task-005 S1 to preserve `+`), + which STILL carries the `#fragment` (server display name, e.g. `#🇳🇱Ne`). + `vmess_link_to_json` only did `payload="${url#vmess://}"`, so the `#`/emoji/ + Cyrillic bytes corrupted the base64 → decode failed → fatal. facade:72's + `url_strip_fragment` only touched the separate `$url`, NOT `$raw_url`, so the + strip MUST live inside `vmess_link_to_json`. +- Fix (helpers.sh, ONE line): right after `payload="${url#vmess://}"` add + `payload="${payload%%#*}"` (POSIX longest-`#…`-suffix strip). Safe because the + base64 body never contains `#`; fragment-less payload = no-op. Existing + whitespace-strip (`tr -d ' \011\012\015'`, NOT `[:space:]`) + `=` pad loop + + `base64_decode` run unchanged on the fragment-free payload. Did NOT touch the + facade / reintroduce url_decode. VMess canonical name still comes from JSON + `ps`; we only drop the fragment, do not adopt it as the name. +- Smoke: extended the existing vmess facade block in `test_sing_box_config` (`sb` + category — no new top-level test/registration) with a `vmess-frag-*` case: + `vmess://<base64(JSON)>#🇳🇱Ne`, sanity-check the link has `#`, then assert + server/uuid/transport/tls on the generated outbound. The existing ws/tcp/plus + cases (no `#`) double as the no-fragment regression. shellcheck -S error clean; + `all` = 76 passed / 0 failed. diff --git a/fe-app-netshift/src/validators/tests/validateVmessUrl.test.js b/fe-app-netshift/src/validators/tests/validateVmessUrl.test.js index 2057e2f2..2277359e 100644 --- a/fe-app-netshift/src/validators/tests/validateVmessUrl.test.js +++ b/fe-app-netshift/src/validators/tests/validateVmessUrl.test.js @@ -31,12 +31,22 @@ const plusB64 = b64({ ...baseConfig, ps: '>>>' }); // An unpadded base64 variant of a valid config (strip trailing '=' padding). const unpaddedBody = b64(baseConfig).replace(/=+$/, ''); +// The user's REAL key: V2RayN base64(JSON) followed by a "#🇳🇱Ne" display-name +// fragment. The fragment (emoji/Cyrillic/etc.) used to corrupt the base64 and +// throw "malformed base64"; stripping it before decode must validate it. +const realUserKey = + 'vmess://eyJhZGQiOiJyZW5kZXJlci1zdHJlYW0tMS00MTEubWlycmEubm93IiwiYWlkIjoiMCIsImFsbG93SW5zZWN1cmUiOiIwIiwiYWxsb3dfaW5zZWN1cmUiOiIwIiwiaG9zdCI6InJlbmRlcmVyLXN0cmVhbS0xLTQxMS5taXJyYS5ub3ciLCJpZCI6ImRmOWM5MzU1LWIwMmMtNGMxMi05MDlkLTBkYmViNzI1ZDUyYiIsImluc2VjdXJlIjoiMCIsIm5ldCI6IndzIiwicGF0aCI6Ii9hcGkvdjEvZ3B1LXN0cmVhbS9zb2NrZXQiLCJwb3J0IjoiNDQzIiwicHMiOiLwn4ez8J+HsSBUaGUgTmV0aGVybGFuZHMgfCBbKkNJRFJdIiwic2VjdXJpdHkiOiJhdXRvIiwic25pIjoicmVuZGVyZXItc3RyZWFtLTEtNDExLm1pcnJhLm5vdyIsInRscyI6InRscyIsInYiOiIyIiwidHlwZSI6Im5vbmUiLCJzY3kiOiJhdXRvIiwiYWxwbiI6IiIsImZwIjoiIn0=#🇳🇱Ne'; + const validUrls = [ ['basic add/port/id', vmess({ add: '1.2.3.4', port: 443, id: 'uuid-1' })], ['full config with net:ws tls:tls', vmess(baseConfig)], ['port as numeric string', vmess({ ...baseConfig, port: '8443' })], ['base64 body containing "+"', `vmess://${plusB64}`], ['unpadded base64 body', `vmess://${unpaddedBody}`], + ["user's real key with #🇳🇱Ne fragment", realUserKey], + ['fragment label after #', `${vmess(baseConfig)}#label`], + ['fragment with spaces after #', `${vmess(baseConfig)}#name with spaces`], + ['no fragment (no regression)', vmess(baseConfig)], ]; const invalidUrls = [ @@ -119,4 +129,22 @@ describe('validateVmessUrl', () => { it('confirms the "+"-containing base64 fixture really contains "+"', () => { expect(plusB64).toContain('+'); }); + + it("validates the user's real #🇳🇱Ne-fragment key", () => { + const res = validateVmessUrl(realUserKey); + expect(res.valid).toBe(true); + expect(res.message).toBe('Valid'); + }); + + it('strips a fragment with spaces (whitespace check runs on base64 only)', () => { + const res = validateVmessUrl(`${vmess(baseConfig)}#name with spaces`); + expect(res.valid).toBe(true); + expect(res.message).toBe('Valid'); + }); + + it('still rejects whitespace inside the base64 body (no fragment)', () => { + const res = validateVmessUrl(`vmess://${b64(baseConfig)} `); + expect(res.valid).toBe(false); + expect(res.message).toBe('Invalid VMess URL: must not contain spaces'); + }); }); diff --git a/fe-app-netshift/src/validators/validateVmessUrl.ts b/fe-app-netshift/src/validators/validateVmessUrl.ts index 91897200..c9c28c7a 100644 --- a/fe-app-netshift/src/validators/validateVmessUrl.ts +++ b/fe-app-netshift/src/validators/validateVmessUrl.ts @@ -8,19 +8,28 @@ export function validateVmessUrl(url: string): ValidationResult { }; } - if (/\s/.test(url)) { + const body = url.slice('vmess://'.length); + + // The optional `#fragment` is the server display name (like #name in + // vless/ss/trojan); the canonical name also lives in the JSON `ps` field. + // Strip it BEFORE base64 decode (cut at the first '#'); the base64 body + // never contains '#', so this is safe and matches the other schemes. + const b64 = body.split('#')[0]; + + // Whitespace ordering: validate AFTER stripping the fragment, and only on + // the base64 part. The base64 itself must contain no whitespace, but a + // display-name fragment (e.g. "#The Netherlands") legitimately may. + if (/\s/.test(b64)) { return { valid: false, message: _('Invalid VMess URL: must not contain spaces'), }; } - const body = url.slice('vmess://'.length); - // VMess (V2RayN) is vmess:// + base64(JSON), not a user@host URL. // Tolerate unpadded base64 by right-padding to a multiple of 4, matching // the backend fix. - const padded = body + '='.repeat((4 - (body.length % 4)) % 4); + const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4); let decoded: string; try { diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 57e7bfa3..681ea53b 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -547,14 +547,15 @@ function validateVmessUrl(url) { message: _("Invalid VMess URL: must start with vmess://") }; } - if (/\s/.test(url)) { + const body = url.slice("vmess://".length); + const b64 = body.split("#")[0]; + if (/\s/.test(b64)) { return { valid: false, message: _("Invalid VMess URL: must not contain spaces") }; } - const body = url.slice("vmess://".length); - const padded = body + "=".repeat((4 - body.length % 4) % 4); + const padded = b64 + "=".repeat((4 - b64.length % 4) % 4); let decoded; try { decoded = atob(padded); diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index d8c779d9..86ecafbf 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -236,6 +236,11 @@ vmess_link_to_json() { local payload decoded pad_len payload="${url#vmess://}" + # Strip a trailing '#fragment' (server display name / remark, like vless/ss/ + # trojan). The base64 body never contains '#', so cutting at the FIRST '#' + # is safe; a fragment-less payload is a no-op. The canonical VMess name lives + # in the decoded JSON `ps` field, so we only need to drop the fragment here. + payload="${payload%%#*}" [ -n "$payload" ] || return 0 # Normalize: strip whitespace (space, tab, CR, LF via octal escapes — busybox diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 4e02abe9..68bca090 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -442,6 +442,17 @@ ws_link="vmess://$(printf '%s' "$ws_json" | base64 | tr -d '\n')" tcp_json='{"v":"2","ps":"node-tcp","add":"tcp.example.com","port":"8080","id":"99999999-8888-7777-6666-555555555555","aid":"0","scy":"auto","net":"tcp","host":"","path":"","tls":"","sni":"","alpn":"","fp":""}' tcp_link="vmess://$(printf '%s' "$tcp_json" | base64 | tr -d '\n')" +# task-012: a key with a trailing '#fragment' (server display name / remark, +# like the user's real key `...In0=#🇳🇱Ne`). The '#' + emoji/Cyrillic bytes must +# be STRIPPED before base64 decode; the canonical name still comes from `ps`. +frag_json='{"v":"2","ps":"node-frag","add":"frag.example.com","port":"443","id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","aid":"0","scy":"auto","net":"ws","host":"frag.example.com","path":"/fragpath","tls":"tls","sni":"frag.example.com","alpn":"","fp":""}' +frag_link="vmess://$(printf '%s' "$frag_json" | base64 | tr -d '\n')#🇳🇱Ne" +# Sanity: confirm the crafted link actually carries a '#fragment'. +case "$frag_link" in +*#*) echo 'vmess-frag-link-has-hash:OK' ;; +*) echo 'vmess-frag-link-has-hash:FAIL' ;; +esac + # REGRESSION (S1): a key whose STANDARD base64 body DELIBERATELY contains a '+'. # The "node>>" ps label (bytes 0x3E 0x3E) forces a base64 group that maps to # '+' (alphabet index 62). If the facade url_decode'd the link before decoding, @@ -485,6 +496,17 @@ echo "$out_plus" | jq -e '.outbounds[0].server == "plus.example.com"' >/dev/null echo "$out_plus" | jq -e '.outbounds[0].server_port == 2053' >/dev/null 2>&1 && echo 'vmess-plus-port:OK' || echo 'vmess-plus-port:FAIL' echo "$out_plus" | jq -e '.outbounds[0].uuid == "abcdef00-1111-2222-3333-444455556666"' >/dev/null 2>&1 && echo 'vmess-plus-uuid:OK' || echo 'vmess-plus-uuid:FAIL' +# ── task-012: '#fragment' link must parse (fragment stripped before decode) ── +out_frag=$(sing_box_cf_add_proxy_outbound "$base_config" "vmess_frag" "$frag_link" "0") +echo "$out_frag" | jq -e '.outbounds[0].type == "vmess"' >/dev/null 2>&1 && echo 'vmess-frag-type:OK' || echo 'vmess-frag-type:FAIL' +echo "$out_frag" | jq -e '.outbounds[0].server == "frag.example.com"' >/dev/null 2>&1 && echo 'vmess-frag-server:OK' || echo 'vmess-frag-server:FAIL' +echo "$out_frag" | jq -e '.outbounds[0].server_port == 443' >/dev/null 2>&1 && echo 'vmess-frag-port:OK' || echo 'vmess-frag-port:FAIL' +echo "$out_frag" | jq -e '.outbounds[0].uuid == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"' >/dev/null 2>&1 && echo 'vmess-frag-uuid:OK' || echo 'vmess-frag-uuid:FAIL' +echo "$out_frag" | jq -e '.outbounds[0].transport.type == "ws"' >/dev/null 2>&1 && echo 'vmess-frag-transport:OK' || echo 'vmess-frag-transport:FAIL' +echo "$out_frag" | jq -e '.outbounds[0].transport.path == "/fragpath"' >/dev/null 2>&1 && echo 'vmess-frag-path:OK' || echo 'vmess-frag-path:FAIL' +echo "$out_frag" | jq -e '.outbounds[0].tls.enabled == true' >/dev/null 2>&1 && echo 'vmess-frag-tls:OK' || echo 'vmess-frag-tls:FAIL' +echo "$out_frag" | jq -e '.outbounds[0].tls.server_name == "frag.example.com"' >/dev/null 2>&1 && echo 'vmess-frag-sni:OK' || echo 'vmess-frag-sni:FAIL' + # ── Extended OFF: gate returns config UNCHANGED (no vmess outbound) ── is_sing_box_extended() { return 1; } out_gate=$(sing_box_cf_add_proxy_outbound "$base_config" "vmess_gate" "$ws_link" "0") From 7783f3c27d2ea0610fbc728d07d2219713faa42e Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Fri, 5 Jun 2026 21:11:20 +0300 Subject: [PATCH 51/75] =?UTF-8?q?=D0=BF=D0=BE=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D1=83=20=D0=B4=D0=BB=D1=8F=20extended=20=D1=8F?= =?UTF-8?q?=D0=B4=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 32 +++++++++++++++++++ .../memory/shell-backend-developer.md | 27 ++++++++++++++++ netshift/files/usr/bin/netshift | 15 +++++++-- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 0ef9d2a5..74f0268b 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -183,6 +183,38 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> gate generation behind `is_sing_box_extended` and fail safe (warn + skip) when stock sing-box is installed, exactly like xhttp does today. +## sing-box-extended version diagnostic (task-013 — done 2026-06-05) + +- BUG: `check_sing_box` (usr/bin/netshift ~3276) showed "❌ version not compatible" + on the extended core. TWO coupled defects: + 1. `awk '{print $3}'` on `sing-box version 1.13.12-extended-2.3.2` → patch via + `cut -d. -f3` = `12-extended-2` (non-numeric) → `[: bad number`. + 2. The compare `if [ A ] || [ B ] && [ C ] || [ D ] && [ E ] && [ F ]` was + UNGROUPED. POSIX `&&`/`||` are EQUAL-precedence, LEFT-associative, so it + parses `(((((A||B)&&C)||D)&&E)&&F)` — the trailing E/F gate EVERY branch, + so 1.13.x AND 2.0.0 evaluate as not-compatible even with a numeric patch. +- FIX (Variant 2, operator-chosen): strip suffix `version=${version%%-*}` (gives + honest semver; extended author only bumps the trailing `-extended-X.Y.Z`, + leading major.minor.patch is true upstream sing-box) + regroup each AND-term in + `{ ...; }`. Kept threshold 1.12.4 + printed text. Did NOT touch check_requirements + (uses sort -V, already extended-safe). 1-file change, gates green. +- LANDMINE for future tasks: any `[ ] || [ ] && [ ]` chain in this repo without + `{ ...; }` grouping is suspect — equal precedence means trailing AND-terms leak + into prior OR-branches. Group every AND-term. (My first decomposition wrongly + assumed the strip alone fixed it; the dev caught the precedence bug on live + reasoning — TRUST dev "second defect" flags, re-derive the truth table myself.) +- Extended core real output (operator hardware, captured for the epic): version + `1.13.12-extended-2.3.2`, Tags include `with_quic,with_wireguard,with_utls, + with_masque,with_mtproxy,with_openvpn,with_trusttunnel,with_sudoku, + with_naive_outbound,with_gvisor`. So the shtorm-7 build SHIPS the build-tags for + nearly all of epic Tiers 1–3 (tuic/hysteria need with_quic ✅, AWG needs + with_wireguard ✅, sudoku/trusttunnel/openvpn ✅) — CX-4 build-tag uncertainty is + largely resolved EMPIRICALLY for this build; still gate generation behind + is_sing_box_extended + tolerate a per-protocol `sing-box check` rejection. +- SECOND hardcode of the version threshold confirmed: check_sing_box hardcodes + "1.12.4" (major/minor/patch literals + text) while SB_REQUIRED_VERSION=1.12.0 in + constants.sh. Known rassinkhron; left as-is per operator (out of task-013 scope). + ## Subscription keyword filter — Cyrillic case bug (task-010, found on hardware 2026-06) - REAL bug (not version skew): the keyword filter's "case-insensitive" claim only diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index ea8d0d79..dcc8c882 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -330,3 +330,30 @@ findings; keep under ~200 lines. server/uuid/transport/tls on the generated outbound. The existing ws/tcp/plus cases (no `#`) double as the no-fragment regression. shellcheck -S error clean; `all` = 76 passed / 0 failed. + +## task-013: sing-box-extended version diagnostic (build-suffix strip) + +- Root cause: `check_sing_box()` (`bin/netshift`, ~:3276) does + `version=$(sing-box version | awk '{print $3}')` then `patch=$(... cut -d. -f3)`. + Extended core prints `1.13.12-extended-2.3.2`, so `patch` became + `12-extended-2` → non-numeric → `[ "$patch" -ge 4 ]` errors `bad number` → + `❌ not compatible`. Stock cores have numeric patch so they passed. +- Fix (Variant A′, ONE line + comment): right after the existing + `version=$(echo "$version" | sed 's/^v//')`, add `version=${version%%-*}` + (POSIX longest-`-…`-suffix strip; no fork/jq/regex). `1.13.12-extended-2.3.2` + → `1.13.12`; stock `1.12.0` has no `-` so unchanged; also tolerates future + `-beta`/`-rc`. `major`/`minor`/`patch` are already `local`; no new vars. +- **OUT-OF-SCOPE PRE-EXISTING BUG (left untouched per spec, but flag it):** the + comparison chain `if [ "$major" -gt 1 ] || [ "$major" -eq 1 ] && [ "$minor" + -gt 12 ] || ... && [ "$patch" -ge 4 ]` has wrong precedence — POSIX `[]` + `&&`/`||` are equal-precedence left-associative, so it evaluates as + `(...) && [ "$patch" -ge 4 ]`, making the final patch test gate EVERY branch. + Result: `1.13.12` and even `2.0.0` evaluate to version_ok=0 (only `1.12.x>=4` + passes). The spec (task-013) explicitly says do NOT rewrite the chain — it + only fixes the non-numeric `bad number` crash. So the extended diagnostic no + longer errors, but a TRUE fix of "newer than 1.12.4 ⇒ compatible" needs a + follow-up task to correct the chain (e.g. parenthesize each branch in a + single `[ ]` per term or use `sort -V` like `check_requirements` does). +- Smoke: NO new test (pure string strip, no new control flow — per spec). Reran + `shellcheck -S error` clean on `bin/netshift`; `smoke-tests all` = 76 passed / + 0 failed. diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 5955953b..5d65a9c8 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -3276,6 +3276,11 @@ check_sing_box() { version=$(sing-box version 2> /dev/null | head -n 1 | awk '{print $3}') if [ -n "$version" ]; then version=$(echo "$version" | sed 's/^v//') + # Extended cores report e.g. "1.13.12-extended-2.3.2"; the author only changes + # the trailing "-extended-X.Y.Z" while the leading semver is the true upstream + # sing-box version. Strip everything from the first '-' so the numeric compare + # below works for both stock and extended builds. + version=${version%%-*} local major local minor local patch @@ -3283,10 +3288,14 @@ check_sing_box() { minor=$(echo "$version" | cut -d. -f2) patch=$(echo "$version" | cut -d. -f3) - # Compare version: must be >= 1.12.4 + # Compare version: must be >= 1.12.4. Each AND-term is grouped in + # { ...; } so the || branches are independent — POSIX list operators + # && / || are equal-precedence/left-associative, so without grouping + # the trailing minor/patch tests would wrongly gate every branch + # (e.g. 1.13.x and 2.0.0 would evaluate as not-compatible). if [ "$major" -gt 1 ] || - [ "$major" -eq 1 ] && [ "$minor" -gt 12 ] || - [ "$major" -eq 1 ] && [ "$minor" -eq 12 ] && [ "$patch" -ge 4 ]; then + { [ "$major" -eq 1 ] && [ "$minor" -gt 12 ]; } || + { [ "$major" -eq 1 ] && [ "$minor" -eq 12 ] && [ "$patch" -ge 4 ]; }; then sing_box_version_ok=1 fi fi From 7c7f7b15a0ddb0b95338849ea34161ed29048fb6 Mon Sep 17 00:00:00 2001 From: yandexru45 <sukadark228@gmail.com> Date: Fri, 5 Jun 2026 22:47:05 +0300 Subject: [PATCH 52/75] =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=84?= =?UTF-8?q?=D0=B8=D1=87=D0=B0:=20DNS=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BA=D1=81=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/luci-frontend-developer.md | 37 ++++ .../memory/shell-backend-developer.md | 43 ++++ fe-app-netshift/locales/calls.json | 147 ++++++++------ fe-app-netshift/locales/netshift.pot | 136 +++++++------ fe-app-netshift/locales/netshift.ru.po | 19 +- .../tabs/diagnostic/checks/runDnsCheck.ts | 11 ++ fe-app-netshift/src/netshift/types.ts | 1 + .../resources/view/netshift/main.js | 10 + .../resources/view/netshift/settings.js | 45 +++++ luci-app-netshift/po/ru/netshift.po | 19 +- luci-app-netshift/po/templates/netshift.pot | 136 +++++++------ netshift/files/etc/config/netshift | 2 + netshift/files/usr/bin/netshift | 76 ++++++- tests/docker-compose.yml | 3 +- tests/entrypoint.sh | 186 +++++++++++++++++- 15 files changed, 691 insertions(+), 180 deletions(-) diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 0ee46bf5..2ac6cab3 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -186,3 +186,40 @@ append findings; keep under ~200 lines. `#`) case proving base64-body whitespace is still rejected. - main.js diff for this fix is exactly the `validateVmessUrl` function body (body/b64 split + whitespace-on-b64 + pad b64) — expected-only. + +## settings.js section-picker pattern + DNS-via-outbound (task-015) + +- `settings.js` is a HAND-WRITTEN LuCI view (NOT bundled by tsup → editing it + yields NO main.js diff). The reusable "pick a proxy/vpn section" dropdown + pattern: `form.ListValue` + `o.depends(<flag>,'1')` + `o.cfgvalue` reading + `uci.get('netshift',section_id,<opt>)` + custom `o.load` that walks + `this.map?.data?.state?.values?.netshift ?? {}`, pushes secName to + `this.keylist`/`this.vallist` when `sec['.type']==='section'` AND + `connection_type` is NOT `block`/`exclusion`, returns `Promise.resolve()`. + Mirrors `download_lists_via_proxy_section` (settings.js ~294-321). +- `dns_outbound_section` uses `rmempty=true` (empty = backend falls back to + first outbound) — intentionally differs from the download-proxy clone's + `rmempty=false`. Don't "normalize" it. +- R3 trap: the spec called the diagnostic field "type-only ⇒ no main.js diff", + but adding the field to `types.ts` is type-only (erased at build) WHILE the + paired `runDnsCheck.ts` `insertIf` render IS RUNTIME CODE → it DOES produce a + legit main.js diff (the 10-line insertIf block). That regenerated main.js is + the deliverable; a second build is idempotent (no further diff). "No diff" + only holds if you skip the runDnsCheck.ts edit. +- locales: ran `node {extract-calls,generate-pot,generate-po ru, + distribute-locales}.js` (NOT yarn → no corepack). New ru text goes in the + SOURCE `locales/netshift.ru.po` empty `msgstr`, then `distribute-locales.js` + copies to `po/ru/netshift.po` + `po/templates/netshift.pot`. Regen touches + calls.json/pot broadly (line-ref reshuffle + POT-Creation-Date header) but is + PURELY ADDITIVE — verified at msgid level: 5 added, 0 removed. +- ENCODING TRAP (Windows/PS5.1): `git show HEAD:file > tmp` re-encodes the blob + to UTF-16 and MANGLES UTF-8 (emoji/Cyrillic) — gives FALSE "removed/added" + noise. Use `cmd /c "git ... > %TEMP%\f"` (preserves raw bytes) OR compare via + `git diff` directly. The committed .pot/.po ARE valid UTF-8; don't panic. +- Console prints po/pot Cyrillic as mojibake — that's PS display only; verify + on-disk via `node -e "require('./locales/calls.json')...includes(...)"` or + UTF-8 byte reads, not the terminal. +- New i18n keys (task-015) + ru: "Route main DNS through proxy/VPN"→"Основной + DNS через прокси/VPN"; "DNS outbound section"→"Секция outbound для DNS"; + "Main DNS via outbound"→"Основной DNS через outbound"; long descriptions + translated equivalently. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index dcc8c882..ed0a29c6 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -357,3 +357,46 @@ findings; keep under ~200 lines. - Smoke: NO new test (pure string strip, no new control flow — per spec). Reran `shellcheck -S error` clean on `bin/netshift`; `smoke-tests all` = 76 passed / 0 failed. + +## task-014: route the MAIN DNS server through a proxy outbound (detour) + +- The cm/cf DNS primitives ALREADY accept `detour` as the last arg and merge it + conditionally (`+ (if $detour != "" then {detour:$detour} else {} end)`): + `sing_box_cf_add_dns_server` $6, `sing_box_cm_add_udp/tls_dns_server` $6, + `_add_https_dns_server` $8. So an EMPTY detour tag => byte-identical to the + pre-feature output (proven in smoke via `jq -cS` object compare of the + empty-tag main server vs a no-detour-arg call). Do NOT touch cm/cf for this. +- New helper `_get_dns_detour_tag()` (bin/netshift, next to + `_determine_first_outbound_section`/`get_first_outbound_section`) echoes the + tag or "" = direct. NEVER `exit`; every fallback logs `warn` and degrades to + direct. Cascade: (1) `dns_via_outbound`!=1 -> "" silent; (2) explicit + `dns_outbound_section` valid + `section_has_configured_outbound` -> it, else + warn(if non-empty) + `get_first_outbound_section`; (3) no candidate -> warn+""; + (4) candidate connection_type block/exclusion -> warn+""; (5) + `subscription_outbound_is_unavailable` -> warn+"" (self-heal on fresh boot / + failed sub); (6) else `get_outbound_tag_by_section "$candidate"`. Mirrors + `get_subscription_download_proxy_address` (toggle + section + fail-safe). +- Wired ONLY into the main `SB_DNS_SERVER_TAG` server in `sing_box_configure_dns` + (6th arg). Bootstrap (`SB_BOOTSTRAP_SERVER_TAG`) + FakeIP stay direct on + purpose (chicken-and-egg: bootstrap resolves the DoH/DoT host before the tunnel + is up; it's also the `domain_resolver` for a hostname main DNS). Two new UCI + opts documented (commented) in `etc/config/netshift`: `dns_via_outbound`(bool, + default 0) + `dns_outbound_section`. Read with `config_get_bool`/`config_get` + + safe defaults — never required live. +- Did Req 4 (low-risk, observable): `check_dns_available` JSON gains + `"dns_via_outbound_tag"` (via `_get_dns_detour_tag`); `global_check` prints + `ℹ️ Main DNS via outbound: <tag>` or `ℹ️ Main DNS: direct` (valid-UTF-8 emoji). +- **LuCI `config_get` always returns 0** (assign-and-succeed even when the option + is unset, leaving the var empty). So step-2's `config_get ... && [ -n "$var" ]` + detects a non-existent section purely via the EMPTY connection_type, not via rc. + Test stubs must mimic this (assign-then-`return 0`). +- New top-level smoke test `test_dns_via_outbound` (alias `dnsdetour`): builds + on/off configs through the real cf/cm path (asserts main-has-detour, + bootstrap/fakeip no-detour, off no-detour, off byte-parity, both pass live + `sing-box check`), then awk-extracts `_get_dns_detour_tag` VERBATIM from the bin + and runs the 8-case cascade table with stubbed UCI + reused helpers. Registered + in `all)` + case alias + usage "Available:" line + docker-compose comment. + shellcheck -S error clean on bin + libs + install.sh; `smoke-tests all` = 76 + passed / 0 failed (suite total unchanged because the per-line `pass` runs in a + piped `while` subshell — same counter quirk as test_subscription; the per-test + ✓ marks are the source of truth, here 15 green for dnsdetour). diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index 0405a1c5..891f86af 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -59,7 +59,7 @@ "call": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "key": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:247" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:292" ] }, { @@ -122,14 +122,14 @@ "call": "Cache File Path", "key": "Cache File Path", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:348" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:393" ] }, { "call": "Cache file path cannot be empty", "key": "Cache file path cannot be empty", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:362" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:407" ] }, { @@ -199,7 +199,7 @@ "call": "Config File Path", "key": "Config File Path", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:335" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:380" ] }, { @@ -277,21 +277,21 @@ "call": "Delay in milliseconds before reloading NetShift after interface UP", "key": "Delay in milliseconds before reloading NetShift after interface UP", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:222" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:267" ] }, { "call": "Delay value cannot be empty", "key": "Delay value cannot be empty", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:229" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:274" ] }, { "call": "DHCP has DNS server", "key": "DHCP has DNS server", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:82" + "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:93" ] }, { @@ -312,14 +312,14 @@ "call": "Disable QUIC", "key": "Disable QUIC", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:265" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:310" ] }, { "call": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "key": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:266" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:311" ] }, { @@ -334,7 +334,14 @@ "call": "DNS on router", "key": "DNS on router", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:77" + "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:88" + ] + }, + { + "call": "DNS outbound section", + "key": "DNS outbound section", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:79" ] }, { @@ -365,7 +372,7 @@ "call": "DNS Rewrite TTL", "key": "DNS Rewrite TTL", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:68" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:113" ] }, { @@ -401,7 +408,7 @@ "call": "Dont Touch My DHCP!", "key": "Dont Touch My DHCP!", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:326" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:371" ] }, { @@ -423,22 +430,22 @@ "call": "Download Lists via Proxy/VPN", "key": "Download Lists via Proxy/VPN", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:288" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:333" ] }, { "call": "Download Lists via specific proxy section", "key": "Download Lists via specific proxy section", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:297" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:342" ] }, { "call": "Downloading all lists via specific Proxy/VPN", "key": "Downloading all lists via specific Proxy/VPN", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:289", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:298" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:334", + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:343" ] }, { @@ -488,7 +495,7 @@ "call": "Enable Output Network Interface", "key": "Enable Output Network Interface", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:126" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:171" ] }, { @@ -502,14 +509,14 @@ "call": "Enable YACD", "key": "Enable YACD", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:237" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:282" ] }, { "call": "Enable YACD WAN Access", "key": "Enable YACD WAN Access", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:246" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:291" ] }, { @@ -621,14 +628,14 @@ "call": "Exclude NTP", "key": "Exclude NTP", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:402" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:447" ] }, { "call": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "key": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:403" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:448" ] }, { @@ -729,21 +736,21 @@ "call": "Interface Monitoring", "key": "Interface Monitoring", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:189" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:234" ] }, { "call": "Interface Monitoring Delay", "key": "Interface Monitoring Delay", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:221" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:266" ] }, { "call": "Interface monitoring for Bad WAN", "key": "Interface monitoring for Bad WAN", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:190" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:235" ] }, { @@ -1053,43 +1060,43 @@ "call": "Invalid VMess URL: invalid port", "key": "Invalid VMess URL: invalid port", "places": [ - "src\\validators\\validateVmessUrl.ts:73" + "src\\validators\\validateVmessUrl.ts:82" ] }, { "call": "Invalid VMess URL: malformed base64", "key": "Invalid VMess URL: malformed base64", "places": [ - "src\\validators\\validateVmessUrl.ts:31" + "src\\validators\\validateVmessUrl.ts:40" ] }, { "call": "Invalid VMess URL: malformed JSON", "key": "Invalid VMess URL: malformed JSON", "places": [ - "src\\validators\\validateVmessUrl.ts:41", - "src\\validators\\validateVmessUrl.ts:48" + "src\\validators\\validateVmessUrl.ts:50", + "src\\validators\\validateVmessUrl.ts:57" ] }, { "call": "Invalid VMess URL: missing address", "key": "Invalid VMess URL: missing address", "places": [ - "src\\validators\\validateVmessUrl.ts:57" + "src\\validators\\validateVmessUrl.ts:66" ] }, { "call": "Invalid VMess URL: missing id", "key": "Invalid VMess URL: missing id", "places": [ - "src\\validators\\validateVmessUrl.ts:64" + "src\\validators\\validateVmessUrl.ts:73" ] }, { "call": "Invalid VMess URL: must not contain spaces", "key": "Invalid VMess URL: must not contain spaces", "places": [ - "src\\validators\\validateVmessUrl.ts:14" + "src\\validators\\validateVmessUrl.ts:25" ] }, { @@ -1131,7 +1138,7 @@ "call": "List Update Frequency", "key": "List Update Frequency", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:276" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:321" ] }, { @@ -1152,7 +1159,7 @@ "call": "Log Level", "key": "Log Level", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:384" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:429" ] }, { @@ -1162,6 +1169,13 @@ "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:72" ] }, + { + "call": "Main DNS via outbound", + "key": "Main DNS via outbound", + "places": [ + "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:81" + ] + }, { "call": "Memory Usage", "key": "Memory Usage", @@ -1180,7 +1194,7 @@ "call": "Monitored Interfaces", "key": "Monitored Interfaces", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:198" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:243" ] }, { @@ -1208,7 +1222,7 @@ "call": "NetShift will not modify your DHCP configuration", "key": "NetShift will not modify your DHCP configuration", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:327" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:372" ] }, { @@ -1284,7 +1298,7 @@ "call": "Output Network Interface", "key": "Output Network Interface", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:135" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:180" ] }, { @@ -1298,21 +1312,21 @@ "call": "Path must be absolute (start with /)", "key": "Path must be absolute (start with /)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:366" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:411" ] }, { "call": "Path must contain at least one directory (like /tmp/cache.db)", "key": "Path must contain at least one directory (like /tmp/cache.db)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:375" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:420" ] }, { "call": "Path must end with cache.db", "key": "Path must end with cache.db", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:370" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:415" ] }, { @@ -1382,6 +1396,13 @@ "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:53" ] }, + { + "call": "Route main DNS through proxy/VPN", + "key": "Route main DNS through proxy/VPN", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:68" + ] + }, { "call": "Router DNS is not routed through sing-box", "key": "Router DNS is not routed through sing-box", @@ -1400,7 +1421,7 @@ "call": "Routing Excluded IPs", "key": "Routing Excluded IPs", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:413" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:458" ] }, { @@ -1463,7 +1484,7 @@ "call": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "key": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:257" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:302" ] }, { @@ -1498,7 +1519,7 @@ "call": "Select how often the domain or subnet lists are updated automatically", "key": "Select how often the domain or subnet lists are updated automatically", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:277" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:322" ] }, { @@ -1527,14 +1548,14 @@ "call": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "key": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:349" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:394" ] }, { "call": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "key": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:336" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:381" ] }, { @@ -1562,28 +1583,28 @@ "call": "Select the log level for sing-box", "key": "Select the log level for sing-box", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:385" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:430" ] }, { "call": "Select the network interface from which the traffic will originate", "key": "Select the network interface from which the traffic will originate", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:90" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:135" ] }, { "call": "Select the network interface to which the traffic will originate", "key": "Select the network interface to which the traffic will originate", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:136" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:181" ] }, { "call": "Select the WAN interfaces to be monitored", "key": "Select the WAN interfaces to be monitored", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:199" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:244" ] }, { @@ -1600,6 +1621,13 @@ "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:155" ] }, + { + "call": "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct.", + "key": "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct.", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:69" + ] + }, { "call": "Services info", "key": "Services info", @@ -1682,14 +1710,14 @@ "call": "Source Network Interface", "key": "Source Network Interface", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:89" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:134" ] }, { "call": "Specify a local IP address to be excluded from routing", "key": "Specify a local IP address to be excluded from routing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:414" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:459" ] }, { @@ -1838,7 +1866,7 @@ "call": "Time in seconds for DNS record caching (default: 60)", "key": "Time in seconds for DNS record caching (default: 60)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:69" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:114" ] }, { @@ -1866,14 +1894,14 @@ "call": "TTL must be a positive number", "key": "TTL must be a positive number", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:80" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:125" ] }, { "call": "TTL value cannot be empty", "key": "TTL value cannot be empty", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:75" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:120" ] }, { @@ -2028,7 +2056,7 @@ "src\\validators\\validateTrojanUrl.ts:59", "src\\validators\\validateUrl.ts:28", "src\\validators\\validateVlessUrl.ts:108", - "src\\validators\\validateVmessUrl.ts:77" + "src\\validators\\validateVmessUrl.ts:86" ] }, { @@ -2077,18 +2105,25 @@ "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:424" ] }, + { + "call": "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound.", + "key": "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound.", + "places": [ + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:80" + ] + }, { "call": "YACD Secret Key", "key": "YACD Secret Key", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:256" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:301" ] }, { "call": "You can select Output Network Interface, by default autodetect", "key": "You can select Output Network Interface, by default autodetect", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:127" + "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:172" ] } ] \ No newline at end of file diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index ecafb2d4..37c06163 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 06:27+0300\n" -"PO-Revision-Date: 2026-06-05 06:27+0300\n" +"POT-Creation-Date: 2026-06-05 18:43+0300\n" +"PO-Revision-Date: 2026-06-05 18:43+0300\n" "Last-Translator: yandexru45 <sukadark228@gmail.com>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -48,7 +48,7 @@ msgstr "" msgid "Additional marking rules found" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:247 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:292 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" @@ -84,11 +84,11 @@ msgstr "" msgid "Browser is using FakeIP correctly" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:348 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:393 msgid "Cache File Path" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:362 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:407 msgid "Cache file path cannot be empty" msgstr "" @@ -131,7 +131,7 @@ msgstr "" msgid "Community Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:335 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:380 msgid "Config File Path" msgstr "" @@ -176,15 +176,15 @@ msgstr "" msgid "Dashboard currently unavailable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:222 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:267 msgid "Delay in milliseconds before reloading NetShift after interface UP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:229 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:274 msgid "Delay value cannot be empty" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:82 +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:93 msgid "DHCP has DNS server" msgstr "" @@ -196,11 +196,11 @@ msgstr "" msgid "Disable autostart" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:265 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:310 msgid "Disable QUIC" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:266 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:311 msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" @@ -209,10 +209,14 @@ msgstr "" msgid "Disabled" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:77 +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:88 msgid "DNS on router" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:79 +msgid "DNS outbound section" +msgstr "" + #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:337 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:15 msgid "DNS over HTTPS (DoH)" @@ -228,7 +232,7 @@ msgstr "" msgid "DNS Protocol Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:68 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:113 msgid "DNS Rewrite TTL" msgstr "" @@ -249,7 +253,7 @@ msgstr "" msgid "Domain Resolver" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:326 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:371 msgid "Dont Touch My DHCP!" msgstr "" @@ -262,16 +266,16 @@ msgstr "" msgid "Download" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:288 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:333 msgid "Download Lists via Proxy/VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:297 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:342 msgid "Download Lists via specific proxy section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:289 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:298 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:334 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:343 msgid "Downloading all lists via specific Proxy/VPN" msgstr "" @@ -300,7 +304,7 @@ msgstr "" msgid "Enable Mixed Proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:126 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:171 msgid "Enable Output Network Interface" msgstr "" @@ -308,11 +312,11 @@ msgstr "" msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:237 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:282 msgid "Enable YACD" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:246 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:291 msgid "Enable YACD WAN Access" msgstr "" @@ -376,11 +380,11 @@ msgstr "" msgid "Every hour" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:402 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:447 msgid "Exclude NTP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:403 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:448 msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" @@ -442,15 +446,15 @@ msgstr "" msgid "Install stable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:189 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:234 msgid "Interface Monitoring" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:221 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:266 msgid "Interface Monitoring Delay" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:190 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:235 msgid "Interface monitoring for Bad WAN" msgstr "" @@ -628,28 +632,28 @@ msgstr "" msgid "Invalid VLESS URL: parsing failed" msgstr "" -#: src\validators\validateVmessUrl.ts:73 +#: src\validators\validateVmessUrl.ts:82 msgid "Invalid VMess URL: invalid port" msgstr "" -#: src\validators\validateVmessUrl.ts:31 +#: src\validators\validateVmessUrl.ts:40 msgid "Invalid VMess URL: malformed base64" msgstr "" -#: src\validators\validateVmessUrl.ts:41 -#: src\validators\validateVmessUrl.ts:48 +#: src\validators\validateVmessUrl.ts:50 +#: src\validators\validateVmessUrl.ts:57 msgid "Invalid VMess URL: malformed JSON" msgstr "" -#: src\validators\validateVmessUrl.ts:57 +#: src\validators\validateVmessUrl.ts:66 msgid "Invalid VMess URL: missing address" msgstr "" -#: src\validators\validateVmessUrl.ts:64 +#: src\validators\validateVmessUrl.ts:73 msgid "Invalid VMess URL: missing id" msgstr "" -#: src\validators\validateVmessUrl.ts:14 +#: src\validators\validateVmessUrl.ts:25 msgid "Invalid VMess URL: must not contain spaces" msgstr "" @@ -673,7 +677,7 @@ msgstr "" msgid "Latest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:276 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:321 msgid "List Update Frequency" msgstr "" @@ -685,7 +689,7 @@ msgstr "" msgid "Local Subnet Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:384 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:429 msgid "Log Level" msgstr "" @@ -693,6 +697,10 @@ msgstr "" msgid "Main DNS" msgstr "" +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:81 +msgid "Main DNS via outbound" +msgstr "" + #: src\netshift\tabs\dashboard\initController.ts:311 msgid "Memory Usage" msgstr "" @@ -701,7 +709,7 @@ msgstr "" msgid "Mixed Proxy Port" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:198 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:243 msgid "Monitored Interfaces" msgstr "" @@ -717,7 +725,7 @@ msgstr "" msgid "NetShift Settings" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:327 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:372 msgid "NetShift will not modify your DHCP configuration" msgstr "" @@ -763,7 +771,7 @@ msgstr "" msgid "Outdated" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:135 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:180 msgid "Output Network Interface" msgstr "" @@ -771,15 +779,15 @@ msgstr "" msgid "Path cannot be empty" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:366 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:411 msgid "Path must be absolute (start with /)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:375 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:420 msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:370 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:415 msgid "Path must end with cache.db" msgstr "" @@ -823,6 +831,10 @@ msgstr "" msgid "Restart NetShift" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:68 +msgid "Route main DNS through proxy/VPN" +msgstr "" + #: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:51 msgid "Router DNS is not routed through sing-box" msgstr "" @@ -831,7 +843,7 @@ msgstr "" msgid "Router DNS is routed through sing-box" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:413 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:458 msgid "Routing Excluded IPs" msgstr "" @@ -867,7 +879,7 @@ msgstr "" msgid "Russia inside restrictions" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:257 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:302 msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" @@ -887,7 +899,7 @@ msgstr "" msgid "Select DNS protocol to use" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:277 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:322 msgid "Select how often the domain or subnet lists are updated automatically" msgstr "" @@ -904,11 +916,11 @@ msgstr "" msgid "Select or enter DNS server address" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:349 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:394 msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:336 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:381 msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" @@ -924,19 +936,19 @@ msgstr "" msgid "Select the list type for adding custom subnets" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:385 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:430 msgid "Select the log level for sing-box" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:90 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:135 msgid "Select the network interface from which the traffic will originate" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:136 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:181 msgid "Select the network interface to which the traffic will originate" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:199 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:244 msgid "Select the WAN interfaces to be monitored" msgstr "" @@ -948,6 +960,10 @@ msgstr "" msgid "Selector Proxy Links" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:69 +msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." +msgstr "" + #: src\netshift\tabs\dashboard\initController.ts:340 msgid "Services info" msgstr "" @@ -993,11 +1009,11 @@ msgstr "" msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:89 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:134 msgid "Source Network Interface" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:414 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:459 msgid "Specify a local IP address to be excluded from routing" msgstr "" @@ -1083,7 +1099,7 @@ msgstr "" msgid "The URL used to test server connectivity" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:69 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:114 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" @@ -1099,11 +1115,11 @@ msgstr "" msgid "Troubleshooting" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:80 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:125 msgid "TTL must be a positive number" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:75 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:120 msgid "TTL value cannot be empty" msgstr "" @@ -1201,7 +1217,7 @@ msgstr "" #: src\validators\validateTrojanUrl.ts:59 #: src\validators\validateUrl.ts:28 #: src\validators\validateVlessUrl.ts:108 -#: src\validators\validateVmessUrl.ts:77 +#: src\validators\validateVmessUrl.ts:86 msgid "Valid" msgstr "" @@ -1233,10 +1249,14 @@ msgstr "" msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:256 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:80 +msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:301 msgid "YACD Secret Key" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:127 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:172 msgid "You can select Output Network Interface, by default autodetect" msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 2aa7144c..e43b8111 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 09:28+0300\n" -"PO-Revision-Date: 2026-06-05 09:28+0300\n" +"POT-Creation-Date: 2026-06-05 21:43+0300\n" +"PO-Revision-Date: 2026-06-05 21:43+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -158,6 +158,9 @@ msgstr "Отключено" msgid "DNS on router" msgstr "DNS на роутере" +msgid "DNS outbound section" +msgstr "Секция outbound для DNS" + msgid "DNS over HTTPS (DoH)" msgstr "DNS через HTTPS (DoH)" @@ -503,6 +506,9 @@ msgstr "Уровень логов" msgid "Main DNS" msgstr "Основной DNS" +msgid "Main DNS via outbound" +msgstr "Основной DNS через outbound" + msgid "Memory Usage" msgstr "Использование памяти" @@ -593,6 +599,9 @@ msgstr "Разрешение реальных IP-адресов" msgid "Restart NetShift" msgstr "" +msgid "Route main DNS through proxy/VPN" +msgstr "Основной DNS через прокси/VPN" + msgid "Router DNS is not routed through sing-box" msgstr "DNS роутера не проходит через sing-box" @@ -686,6 +695,9 @@ msgstr "Selector" msgid "Selector Proxy Links" msgstr "Ссылки прокси для Selector" +msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." +msgstr "Отправлять запросы к основному DNS через outbound прокси/VPN вместо прямого подключения. Bootstrap DNS всегда остаётся прямым." + msgid "Services info" msgstr "Информация о сервисах" @@ -878,6 +890,9 @@ msgstr "Предупреждение: %s нельзя использовать msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных." +msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." +msgstr "Какая секция прокси/VPN обслуживает DNS. Оставьте пустым, чтобы использовать первый настроенный outbound." + msgid "YACD Secret Key" msgstr "Секретный ключ YACD" diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runDnsCheck.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runDnsCheck.ts index 116e4692..36abebce 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runDnsCheck.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runDnsCheck.ts @@ -72,6 +72,17 @@ export async function runDnsCheck() { key: _('Main DNS'), value: `${data.dns_server} [${data.dns_type}]`, }, + ...insertIf<IDiagnosticsChecksItem>( + typeof data.dns_via_outbound_tag === 'string' && + data.dns_via_outbound_tag.length > 0, + [ + { + state: 'success', + key: _('Main DNS via outbound'), + value: data.dns_via_outbound_tag ?? '', + }, + ], + ), { state: data.dns_on_router ? 'success' : 'error', key: _('DNS on router'), diff --git a/fe-app-netshift/src/netshift/types.ts b/fe-app-netshift/src/netshift/types.ts index 21efd368..46120573 100644 --- a/fe-app-netshift/src/netshift/types.ts +++ b/fe-app-netshift/src/netshift/types.ts @@ -175,6 +175,7 @@ export namespace NetShift { bootstrap_dns_server: string; bootstrap_dns_status: 0 | 1; dhcp_config_status: 0 | 1; + dns_via_outbound_tag?: string; } export interface NftRulesCheckResult { diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 681ea53b..89f54c55 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -2828,6 +2828,16 @@ async function runDnsCheck() { key: _("Main DNS"), value: `${data.dns_server} [${data.dns_type}]` }, + ...insertIf( + typeof data.dns_via_outbound_tag === "string" && data.dns_via_outbound_tag.length > 0, + [ + { + state: "success", + key: _("Main DNS via outbound"), + value: data.dns_via_outbound_tag ?? "" + } + ] + ), { state: data.dns_on_router ? "success" : "error", key: _("DNS on router"), diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js index 8f1cc56f..7167bc6c 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js @@ -62,6 +62,51 @@ function createSettingsContent(section) { return validation.message; }; + o = section.option( + form.Flag, + "dns_via_outbound", + _("Route main DNS through proxy/VPN"), + _( + "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct.", + ), + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + form.ListValue, + "dns_outbound_section", + _("DNS outbound section"), + _( + "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound.", + ), + ); + o.rmempty = true; + o.depends("dns_via_outbound", "1"); + o.cfgvalue = function (section_id) { + return uci.get("netshift", section_id, "dns_outbound_section"); + }; + o.load = function () { + const sections = this.map?.data?.state?.values?.netshift ?? {}; + + this.keylist = []; + this.vallist = []; + + for (const secName in sections) { + const sec = sections[secName]; + if ( + sec[".type"] === "section" && + sec["connection_type"] !== "block" && + sec["connection_type"] !== "exclusion" + ) { + this.keylist.push(secName); + this.vallist.push(secName); + } + } + + return Promise.resolve(); + }; + o = section.option( form.Value, "dns_rewrite_ttl", diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 2aa7144c..e43b8111 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 09:28+0300\n" -"PO-Revision-Date: 2026-06-05 09:28+0300\n" +"POT-Creation-Date: 2026-06-05 21:43+0300\n" +"PO-Revision-Date: 2026-06-05 21:43+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -158,6 +158,9 @@ msgstr "Отключено" msgid "DNS on router" msgstr "DNS на роутере" +msgid "DNS outbound section" +msgstr "Секция outbound для DNS" + msgid "DNS over HTTPS (DoH)" msgstr "DNS через HTTPS (DoH)" @@ -503,6 +506,9 @@ msgstr "Уровень логов" msgid "Main DNS" msgstr "Основной DNS" +msgid "Main DNS via outbound" +msgstr "Основной DNS через outbound" + msgid "Memory Usage" msgstr "Использование памяти" @@ -593,6 +599,9 @@ msgstr "Разрешение реальных IP-адресов" msgid "Restart NetShift" msgstr "" +msgid "Route main DNS through proxy/VPN" +msgstr "Основной DNS через прокси/VPN" + msgid "Router DNS is not routed through sing-box" msgstr "DNS роутера не проходит через sing-box" @@ -686,6 +695,9 @@ msgstr "Selector" msgid "Selector Proxy Links" msgstr "Ссылки прокси для Selector" +msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." +msgstr "Отправлять запросы к основному DNS через outbound прокси/VPN вместо прямого подключения. Bootstrap DNS всегда остаётся прямым." + msgid "Services info" msgstr "Информация о сервисах" @@ -878,6 +890,9 @@ msgstr "Предупреждение: %s нельзя использовать msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных." +msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." +msgstr "Какая секция прокси/VPN обслуживает DNS. Оставьте пустым, чтобы использовать первый настроенный outbound." + msgid "YACD Secret Key" msgstr "Секретный ключ YACD" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index ecafb2d4..37c06163 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 06:27+0300\n" -"PO-Revision-Date: 2026-06-05 06:27+0300\n" +"POT-Creation-Date: 2026-06-05 18:43+0300\n" +"PO-Revision-Date: 2026-06-05 18:43+0300\n" "Last-Translator: yandexru45 <sukadark228@gmail.com>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -48,7 +48,7 @@ msgstr "" msgid "Additional marking rules found" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:247 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:292 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" @@ -84,11 +84,11 @@ msgstr "" msgid "Browser is using FakeIP correctly" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:348 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:393 msgid "Cache File Path" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:362 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:407 msgid "Cache file path cannot be empty" msgstr "" @@ -131,7 +131,7 @@ msgstr "" msgid "Community Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:335 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:380 msgid "Config File Path" msgstr "" @@ -176,15 +176,15 @@ msgstr "" msgid "Dashboard currently unavailable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:222 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:267 msgid "Delay in milliseconds before reloading NetShift after interface UP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:229 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:274 msgid "Delay value cannot be empty" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:82 +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:93 msgid "DHCP has DNS server" msgstr "" @@ -196,11 +196,11 @@ msgstr "" msgid "Disable autostart" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:265 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:310 msgid "Disable QUIC" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:266 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:311 msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" @@ -209,10 +209,14 @@ msgstr "" msgid "Disabled" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:77 +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:88 msgid "DNS on router" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:79 +msgid "DNS outbound section" +msgstr "" + #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:337 #: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:15 msgid "DNS over HTTPS (DoH)" @@ -228,7 +232,7 @@ msgstr "" msgid "DNS Protocol Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:68 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:113 msgid "DNS Rewrite TTL" msgstr "" @@ -249,7 +253,7 @@ msgstr "" msgid "Domain Resolver" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:326 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:371 msgid "Dont Touch My DHCP!" msgstr "" @@ -262,16 +266,16 @@ msgstr "" msgid "Download" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:288 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:333 msgid "Download Lists via Proxy/VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:297 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:342 msgid "Download Lists via specific proxy section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:289 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:298 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:334 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:343 msgid "Downloading all lists via specific Proxy/VPN" msgstr "" @@ -300,7 +304,7 @@ msgstr "" msgid "Enable Mixed Proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:126 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:171 msgid "Enable Output Network Interface" msgstr "" @@ -308,11 +312,11 @@ msgstr "" msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:237 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:282 msgid "Enable YACD" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:246 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:291 msgid "Enable YACD WAN Access" msgstr "" @@ -376,11 +380,11 @@ msgstr "" msgid "Every hour" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:402 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:447 msgid "Exclude NTP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:403 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:448 msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" @@ -442,15 +446,15 @@ msgstr "" msgid "Install stable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:189 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:234 msgid "Interface Monitoring" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:221 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:266 msgid "Interface Monitoring Delay" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:190 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:235 msgid "Interface monitoring for Bad WAN" msgstr "" @@ -628,28 +632,28 @@ msgstr "" msgid "Invalid VLESS URL: parsing failed" msgstr "" -#: src\validators\validateVmessUrl.ts:73 +#: src\validators\validateVmessUrl.ts:82 msgid "Invalid VMess URL: invalid port" msgstr "" -#: src\validators\validateVmessUrl.ts:31 +#: src\validators\validateVmessUrl.ts:40 msgid "Invalid VMess URL: malformed base64" msgstr "" -#: src\validators\validateVmessUrl.ts:41 -#: src\validators\validateVmessUrl.ts:48 +#: src\validators\validateVmessUrl.ts:50 +#: src\validators\validateVmessUrl.ts:57 msgid "Invalid VMess URL: malformed JSON" msgstr "" -#: src\validators\validateVmessUrl.ts:57 +#: src\validators\validateVmessUrl.ts:66 msgid "Invalid VMess URL: missing address" msgstr "" -#: src\validators\validateVmessUrl.ts:64 +#: src\validators\validateVmessUrl.ts:73 msgid "Invalid VMess URL: missing id" msgstr "" -#: src\validators\validateVmessUrl.ts:14 +#: src\validators\validateVmessUrl.ts:25 msgid "Invalid VMess URL: must not contain spaces" msgstr "" @@ -673,7 +677,7 @@ msgstr "" msgid "Latest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:276 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:321 msgid "List Update Frequency" msgstr "" @@ -685,7 +689,7 @@ msgstr "" msgid "Local Subnet Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:384 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:429 msgid "Log Level" msgstr "" @@ -693,6 +697,10 @@ msgstr "" msgid "Main DNS" msgstr "" +#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:81 +msgid "Main DNS via outbound" +msgstr "" + #: src\netshift\tabs\dashboard\initController.ts:311 msgid "Memory Usage" msgstr "" @@ -701,7 +709,7 @@ msgstr "" msgid "Mixed Proxy Port" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:198 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:243 msgid "Monitored Interfaces" msgstr "" @@ -717,7 +725,7 @@ msgstr "" msgid "NetShift Settings" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:327 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:372 msgid "NetShift will not modify your DHCP configuration" msgstr "" @@ -763,7 +771,7 @@ msgstr "" msgid "Outdated" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:135 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:180 msgid "Output Network Interface" msgstr "" @@ -771,15 +779,15 @@ msgstr "" msgid "Path cannot be empty" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:366 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:411 msgid "Path must be absolute (start with /)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:375 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:420 msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:370 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:415 msgid "Path must end with cache.db" msgstr "" @@ -823,6 +831,10 @@ msgstr "" msgid "Restart NetShift" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:68 +msgid "Route main DNS through proxy/VPN" +msgstr "" + #: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:51 msgid "Router DNS is not routed through sing-box" msgstr "" @@ -831,7 +843,7 @@ msgstr "" msgid "Router DNS is routed through sing-box" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:413 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:458 msgid "Routing Excluded IPs" msgstr "" @@ -867,7 +879,7 @@ msgstr "" msgid "Russia inside restrictions" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:257 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:302 msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" @@ -887,7 +899,7 @@ msgstr "" msgid "Select DNS protocol to use" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:277 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:322 msgid "Select how often the domain or subnet lists are updated automatically" msgstr "" @@ -904,11 +916,11 @@ msgstr "" msgid "Select or enter DNS server address" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:349 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:394 msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:336 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:381 msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" @@ -924,19 +936,19 @@ msgstr "" msgid "Select the list type for adding custom subnets" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:385 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:430 msgid "Select the log level for sing-box" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:90 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:135 msgid "Select the network interface from which the traffic will originate" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:136 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:181 msgid "Select the network interface to which the traffic will originate" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:199 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:244 msgid "Select the WAN interfaces to be monitored" msgstr "" @@ -948,6 +960,10 @@ msgstr "" msgid "Selector Proxy Links" msgstr "" +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:69 +msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." +msgstr "" + #: src\netshift\tabs\dashboard\initController.ts:340 msgid "Services info" msgstr "" @@ -993,11 +1009,11 @@ msgstr "" msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:89 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:134 msgid "Source Network Interface" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:414 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:459 msgid "Specify a local IP address to be excluded from routing" msgstr "" @@ -1083,7 +1099,7 @@ msgstr "" msgid "The URL used to test server connectivity" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:69 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:114 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" @@ -1099,11 +1115,11 @@ msgstr "" msgid "Troubleshooting" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:80 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:125 msgid "TTL must be a positive number" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:75 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:120 msgid "TTL value cannot be empty" msgstr "" @@ -1201,7 +1217,7 @@ msgstr "" #: src\validators\validateTrojanUrl.ts:59 #: src\validators\validateUrl.ts:28 #: src\validators\validateVlessUrl.ts:108 -#: src\validators\validateVmessUrl.ts:77 +#: src\validators\validateVmessUrl.ts:86 msgid "Valid" msgstr "" @@ -1233,10 +1249,14 @@ msgstr "" msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:256 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:80 +msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." +msgstr "" + +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:301 msgid "YACD Secret Key" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:127 +#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:172 msgid "You can select Output Network Interface, by default autodetect" msgstr "" diff --git a/netshift/files/etc/config/netshift b/netshift/files/etc/config/netshift index b5cef360..00a1d29a 100644 --- a/netshift/files/etc/config/netshift +++ b/netshift/files/etc/config/netshift @@ -13,6 +13,8 @@ config settings 'settings' option disable_quic '0' option update_interval '1d' option download_lists_via_proxy '0' + #option dns_via_outbound '0' + #option dns_outbound_section 'main' option dont_touch_dhcp '0' option config_path '/etc/sing-box/config.json' option cache_path '/tmp/sing-box/cache.db' diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 5d65a9c8..2b5ab10e 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -1816,8 +1816,14 @@ sing_box_configure_dns() { dns_domain_resolver=$SB_BOOTSTRAP_SERVER_TAG fi + # Only the MAIN DNS server may carry a detour. Bootstrap stays direct (it + # resolves the DoH/DoT hostname before the tunnel is up) and FakeIP stays + # direct. An empty tag means no detour -> byte-identical to the off path. + local dns_detour_tag + dns_detour_tag="$(_get_dns_detour_tag)" + config=$(sing_box_cm_add_udp_dns_server "$config" "$SB_BOOTSTRAP_SERVER_TAG" "$bootstrap_dns_server" 53) - config=$(sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "$dns_domain_resolver") + config=$(sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "$dns_domain_resolver" "$dns_detour_tag") config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE") log "Adding DNS Rules" @@ -2654,6 +2660,59 @@ _determine_first_outbound_section() { fi } +# Resolve the outbound tag the MAIN sing-box DNS server should detour through. +# Echoes the tag, or an EMPTY string meaning "no detour / direct DNS" (the safe +# default). NEVER exits: every failure path logs the reason and falls back to +# direct DNS so the service always starts and resolves. Mirrors the toggle + +# optional section selector model of get_subscription_download_proxy_address. +_get_dns_detour_tag() { + local dns_via_outbound dns_outbound_section candidate + local candidate_connection_type + + # Step 1: feature off -> direct (silent). + config_get_bool dns_via_outbound "settings" "dns_via_outbound" 0 + [ "$dns_via_outbound" -eq 1 ] || return 0 + + config_get dns_outbound_section "settings" "dns_outbound_section" + + # Step 2: resolve the candidate section. + if [ -n "$dns_outbound_section" ] && \ + config_get candidate_connection_type "$dns_outbound_section" "connection_type" && \ + [ -n "$candidate_connection_type" ] && \ + section_has_configured_outbound "$dns_outbound_section"; then + candidate="$dns_outbound_section" + else + if [ -n "$dns_outbound_section" ]; then + log "DNS-via-outbound section '$dns_outbound_section' is invalid or has no configured outbound; falling back to the first outbound section" "warn" + fi + candidate="$(get_first_outbound_section)" + fi + + # Step 3: no candidate section at all -> direct. + if [ -z "$candidate" ]; then + log "DNS-via-outbound is enabled but no outbound section is configured; using direct DNS" "warn" + return 0 + fi + + # Step 4: block/exclusion sections produce no real outbound -> direct. + config_get candidate_connection_type "$candidate" "connection_type" + case "$candidate_connection_type" in + block | exclusion) + log "DNS-via-outbound candidate section '$candidate' has connection_type '$candidate_connection_type' (no real outbound); using direct DNS" "warn" + return 0 + ;; + esac + + # Step 5: subscription outbound not built yet / failed refresh -> direct (self-heal). + if subscription_outbound_is_unavailable "$candidate"; then + log "DNS-via-outbound selected section '$candidate' has no usable outbound yet; using direct DNS until it recovers" "warn" + return 0 + fi + + # Step 6: use the candidate section's outbound tag. + get_outbound_tag_by_section "$candidate" +} + get_first_outbound_section() { local first_section="" @@ -3150,7 +3209,11 @@ check_dns_available() { config_foreach check_dhcp_has_netshift_dns dnsmasq config_load "$NETSHIFT_CONFIG" - echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$display_dns_server\",\"dns_status\":$dns_status,\"dns_on_router\":$dns_on_router,\"bootstrap_dns_server\":\"$bootstrap_dns_server\",\"bootstrap_dns_status\":$bootstrap_dns_status,\"dhcp_config_status\":$dhcp_config_status}" | jq . + # Effective detour tag the main DNS would use (empty string = direct DNS). + local dns_via_outbound_tag + dns_via_outbound_tag="$(_get_dns_detour_tag)" + + echo "{\"dns_type\":\"$dns_type\",\"dns_server\":\"$display_dns_server\",\"dns_status\":$dns_status,\"dns_on_router\":$dns_on_router,\"bootstrap_dns_server\":\"$bootstrap_dns_server\",\"bootstrap_dns_status\":$bootstrap_dns_status,\"dhcp_config_status\":$dhcp_config_status,\"dns_via_outbound_tag\":\"$dns_via_outbound_tag\"}" | jq . } check_dhcp_has_netshift_dns() { @@ -3520,6 +3583,7 @@ global_check() { if [ -n "$dns_check_json" ]; then local dns_type dns_server dns_status dns_on_router bootstrap_dns_server bootstrap_dns_status dhcp_config_status + local dns_via_outbound_tag dns_type=$(echo "$dns_check_json" | jq -r '.dns_type // "unknown"') dns_server=$(echo "$dns_check_json" | jq -r '.dns_server // "unknown"') @@ -3528,6 +3592,7 @@ global_check() { bootstrap_dns_server=$(echo "$dns_check_json" | jq -r '.bootstrap_dns_server // ""') bootstrap_dns_status=$(echo "$dns_check_json" | jq -r '.bootstrap_dns_status // 0') dhcp_config_status=$(echo "$dns_check_json" | jq -r '.dhcp_config_status // 0') + dns_via_outbound_tag=$(echo "$dns_check_json" | jq -r '.dns_via_outbound_tag // ""') # Bootstrap DNS if [ -n "$bootstrap_dns_server" ]; then @@ -3545,6 +3610,13 @@ global_check() { print_global "❌ Main DNS: $dns_server [$dns_type]" fi + # Main DNS detour (DNS-via-outbound) + if [ -n "$dns_via_outbound_tag" ]; then + print_global "ℹ️ Main DNS via outbound: $dns_via_outbound_tag" + else + print_global "ℹ️ Main DNS: direct" + fi + # DNS on router if [ "$dns_on_router" -eq 1 ]; then print_global "✅ DNS on router" diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 1a0cbb98..a59020a2 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -9,7 +9,8 @@ # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, -# diagnostics, subscription, rejected, jobstate, selfheal +# diagnostics, subscription, rejected, jobstate, selfheal, +# dnsdetour # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 68bca090..8cde353f 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -2202,6 +2202,188 @@ RHEOF rm -f "$drv" } +# ───────────────────────────────────────────────────────────────── +# Test: DNS via outbound (task-014) — detour wiring + fail-safe cascade +# ───────────────────────────────────────────────────────────────── +test_dns_via_outbound() { + header "DNS via Outbound (task-014)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local facade_lib="${NETSHIFT_LIB_DIR}/sing_box_config_facade.sh" + local bin="${NETSHIFT_SRC}/usr/bin/netshift" + if [ ! -r "$facade_lib" ] || [ ! -r "$bin" ]; then + skip "facade lib / netshift bin not found" + return + fi + + # Bind bind-mounted sources to the runtime path the facade hardcodes. + mkdir -p /usr/lib/netshift + ln -sf "${NETSHIFT_LIB_DIR}/helpers.sh" /usr/lib/netshift/helpers.sh + ln -sf "${NETSHIFT_LIB_DIR}/sing_box_config_manager.sh" /usr/lib/netshift/sing_box_config_manager.sh + + local drv="/tmp/netshift-dnsdetour-$$.sh" + cat > "$drv" << 'DDEOF' +. "NETSHIFT_LIB/logging.sh" 2>/dev/null || log() { :; } +. "FACADE_LIB_PATH" + +# Minimal DNS skeleton like sing_box_cm_configure_dns produces. +base_config='{"dns":{"servers":[],"rules":[],"final":"dns-server","strategy":"prefer_ipv4","independent_cache":true},"outbounds":[{"type":"direct","tag":"direct-out"}]}' + +# Tags mirror the constants used in production. +BOOT="bootstrap" +MAIN="dns-server" +FAKE="fakeip" +DETOUR_TAG="main-out" + +# ── Build a config WITH a non-empty detour on the MAIN DNS only. ───────────── +cfg_on="$base_config" +cfg_on=$(sing_box_cm_add_udp_dns_server "$cfg_on" "$BOOT" "77.88.8.8" 53) +cfg_on=$(sing_box_cf_add_dns_server "$cfg_on" "udp" "$MAIN" "1.1.1.1" "" "$DETOUR_TAG") +cfg_on=$(sing_box_cm_add_fakeip_dns_server "$cfg_on" "$FAKE" "198.18.0.0/15") + +echo "$cfg_on" | jq -e --arg t "$MAIN" --arg d "$DETOUR_TAG" \ + '(.dns.servers[] | select(.tag==$t) | .detour) == $d' >/dev/null 2>&1 \ + && echo 'dns-on-main-has-detour:OK' || echo 'dns-on-main-has-detour:FAIL' +echo "$cfg_on" | jq -e --arg t "$BOOT" \ + '(.dns.servers[] | select(.tag==$t) | has("detour")) == false' >/dev/null 2>&1 \ + && echo 'dns-on-bootstrap-no-detour:OK' || echo 'dns-on-bootstrap-no-detour:FAIL' +echo "$cfg_on" | jq -e --arg t "$FAKE" \ + '(.dns.servers[] | select(.tag==$t) | has("detour")) == false' >/dev/null 2>&1 \ + && echo 'dns-on-fakeip-no-detour:OK' || echo 'dns-on-fakeip-no-detour:FAIL' + +# ── Build a config with an EMPTY detour (feature off) — no .detour key. ────── +cfg_off="$base_config" +cfg_off=$(sing_box_cm_add_udp_dns_server "$cfg_off" "$BOOT" "77.88.8.8" 53) +cfg_off=$(sing_box_cf_add_dns_server "$cfg_off" "udp" "$MAIN" "1.1.1.1" "" "") +cfg_off=$(sing_box_cm_add_fakeip_dns_server "$cfg_off" "$FAKE" "198.18.0.0/15") + +echo "$cfg_off" | jq -e --arg t "$MAIN" \ + '(.dns.servers[] | select(.tag==$t) | has("detour")) == false' >/dev/null 2>&1 \ + && echo 'dns-off-main-no-detour:OK' || echo 'dns-off-main-no-detour:FAIL' + +# Byte-parity: the main DNS server object with empty tag must equal the object +# built without passing a detour arg at all. +cfg_legacy="$base_config" +cfg_legacy=$(sing_box_cf_add_dns_server "$cfg_legacy" "udp" "$MAIN" "1.1.1.1" "") +legacy_obj=$(echo "$cfg_legacy" | jq -cS --arg t "$MAIN" '.dns.servers[] | select(.tag==$t)') +off_obj=$(echo "$cfg_off" | jq -cS --arg t "$MAIN" '.dns.servers[] | select(.tag==$t)') +if [ "$legacy_obj" = "$off_obj" ]; then + echo 'dns-off-byte-parity:OK' +else + echo 'dns-off-byte-parity:FAIL' +fi + +# ── Both configs must pass sing-box check (whole-chain validation). ────────── +if command -v sing-box > /dev/null 2>&1; then + echo "$cfg_on" > /tmp/dnsdetour-on.json + echo "$cfg_off" > /tmp/dnsdetour-off.json + sing-box -c /tmp/dnsdetour-on.json check >/dev/null 2>&1 \ + && echo 'dns-on-singbox-check:OK' || echo 'dns-on-singbox-check:FAIL' + sing-box -c /tmp/dnsdetour-off.json check >/dev/null 2>&1 \ + && echo 'dns-off-singbox-check:OK' || echo 'dns-off-singbox-check:FAIL' + rm -f /tmp/dnsdetour-on.json /tmp/dnsdetour-off.json +else + echo 'dns-on-singbox-check:SKIP' + echo 'dns-off-singbox-check:SKIP' +fi + +# ── Fail-safe cascade: exercise _get_dns_detour_tag VERBATIM from the bin. ─── +# Stub UCI + the reused helpers so the cascade is fully controllable. The stubs +# read from shell variables set per-case below. +eval "$(awk '/^_get_dns_detour_tag\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")" + +# UCI stubs (mimic LuCI config_get / config_get_bool: assign-and-return-0). +config_get_bool() { eval "$1=\"\${UCI_DNS_VIA_OUTBOUND:-0}\""; return 0; } +config_get() { + case "$3" in + dns_outbound_section) eval "$1=\"\$UCI_DNS_SECTION\"" ;; + connection_type) eval "$1=\"\$(_stub_conn_type \"$2\")\"" ;; + *) eval "$1=\"\"" ;; + esac + return 0 +} +_stub_conn_type() { + case "$1" in + block-sec) echo "block" ;; + excl-sec) echo "exclusion" ;; + "") echo "" ;; + *) echo "proxy" ;; + esac +} +# section_has_configured_outbound: true unless name contains 'noout'. +section_has_configured_outbound() { + case "$1" in + *noout*|"") return 1 ;; + esac + return 0 +} +get_first_outbound_section() { echo "$STUB_FIRST_SECTION"; } +get_outbound_tag_by_section() { echo "$1-out"; } +subscription_outbound_is_unavailable() { + case " $STUB_UNAVAILABLE " in *" $1 "*) return 0 ;; esac + return 1 +} + +# CASE off: feature disabled -> empty. +UCI_DNS_VIA_OUTBOUND=0; UCI_DNS_SECTION="main"; STUB_FIRST_SECTION="main"; STUB_UNAVAILABLE="" +r=$(_get_dns_detour_tag) +[ -z "$r" ] && echo 'cascade-off-empty:OK' || echo 'cascade-off-empty:FAIL' + +# CASE explicit-valid: enabled + valid explicit section -> its tag. +UCI_DNS_VIA_OUTBOUND=1; UCI_DNS_SECTION="vpn1"; STUB_FIRST_SECTION="main"; STUB_UNAVAILABLE="" +r=$(_get_dns_detour_tag) +[ "$r" = "vpn1-out" ] && echo 'cascade-explicit-valid:OK' || echo 'cascade-explicit-valid:FAIL' + +# CASE invalid->first: explicit section has no configured outbound -> first. +UCI_DNS_VIA_OUTBOUND=1; UCI_DNS_SECTION="noout-sec"; STUB_FIRST_SECTION="main"; STUB_UNAVAILABLE="" +r=$(_get_dns_detour_tag) +[ "$r" = "main-out" ] && echo 'cascade-invalid-to-first:OK' || echo 'cascade-invalid-to-first:FAIL' + +# CASE empty-selector->first: no explicit section -> first. +UCI_DNS_VIA_OUTBOUND=1; UCI_DNS_SECTION=""; STUB_FIRST_SECTION="main"; STUB_UNAVAILABLE="" +r=$(_get_dns_detour_tag) +[ "$r" = "main-out" ] && echo 'cascade-empty-to-first:OK' || echo 'cascade-empty-to-first:FAIL' + +# CASE no-outbound->direct: enabled but no outbound section at all -> empty. +UCI_DNS_VIA_OUTBOUND=1; UCI_DNS_SECTION=""; STUB_FIRST_SECTION=""; STUB_UNAVAILABLE="" +r=$(_get_dns_detour_tag) +[ -z "$r" ] && echo 'cascade-no-outbound-direct:OK' || echo 'cascade-no-outbound-direct:FAIL' + +# CASE block->direct: explicit block section -> empty. +UCI_DNS_VIA_OUTBOUND=1; UCI_DNS_SECTION="block-sec"; STUB_FIRST_SECTION="main"; STUB_UNAVAILABLE="" +r=$(_get_dns_detour_tag) +[ -z "$r" ] && echo 'cascade-block-direct:OK' || echo 'cascade-block-direct:FAIL' + +# CASE exclusion->direct: explicit exclusion section -> empty. +UCI_DNS_VIA_OUTBOUND=1; UCI_DNS_SECTION="excl-sec"; STUB_FIRST_SECTION="main"; STUB_UNAVAILABLE="" +r=$(_get_dns_detour_tag) +[ -z "$r" ] && echo 'cascade-exclusion-direct:OK' || echo 'cascade-exclusion-direct:FAIL' + +# CASE subscription-unavailable->direct: candidate present but outbound not built. +UCI_DNS_VIA_OUTBOUND=1; UCI_DNS_SECTION="sub1"; STUB_FIRST_SECTION="main"; STUB_UNAVAILABLE="sub1" +r=$(_get_dns_detour_tag) +[ -z "$r" ] && echo 'cascade-subscription-unavailable-direct:OK' || echo 'cascade-subscription-unavailable-direct:FAIL' + +echo 'DONE' +DDEOF + sed -i "s|FACADE_LIB_PATH|$facade_lib|; s|NETSHIFT_LIB|$NETSHIFT_LIB_DIR|g; s|BIN_PATH|$bin|g" "$drv" + + ash "$drv" 2>/dev/null | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + *:SKIP) skip "$line" ;; + DONE) ;; + *) ;; + esac + done + rm -f "$drv" +} + # ───────────────────────────────────────────────────────────────── # Main # ───────────────────────────────────────────────────────────────── @@ -2229,6 +2411,7 @@ main() { test_rejected_hash test_jobstate test_selfheal + test_dns_via_outbound ;; deps) test_deps ;; syntax) test_syntax ;; @@ -2240,12 +2423,13 @@ main() { rejected) test_rejected_hash ;; jobstate) test_jobstate ;; selfheal) test_selfheal ;; + dnsdetour) test_dns_via_outbound ;; jq) test_jq_helpers ;; cm) test_config_manager ;; sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription rejected jobstate selfheal" + echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription rejected jobstate selfheal dnsdetour" exit 1 ;; esac From 03806d7b108fc6eda55435744c84664685b636e6 Mon Sep 17 00:00:00 2001 From: rustnomicon <aquadefin@gmail.com> Date: Sat, 6 Jun 2026 19:33:48 +0800 Subject: [PATCH 53/75] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20ipv6=20=D0=B8=20doh-=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- fe-app-netshift/locales/calls.json | 798 +++++++++--------- fe-app-netshift/locales/netshift.pot | 773 +++++++++-------- fe-app-netshift/locales/netshift.ru.po | 81 +- fe-app-netshift/src/constants.ts | 5 + .../src/validators/tests/validateDns.test.js | 12 +- .../src/validators/tests/validateIp.test.js | 53 +- .../validators/tests/validateSubnet.test.js | 8 + fe-app-netshift/src/validators/validateDns.ts | 24 +- fe-app-netshift/src/validators/validateIp.ts | 27 + .../src/validators/validateSubnet.ts | 68 +- .../resources/view/netshift/main.js | 109 ++- .../resources/view/netshift/section.js | 81 +- .../resources/view/netshift/settings.js | 50 +- luci-app-netshift/po/ru/netshift.po | 81 +- luci-app-netshift/po/templates/netshift.pot | 773 +++++++++-------- netshift/files/etc/config/netshift | 3 + netshift/files/usr/bin/netshift | 485 +++++++++-- netshift/files/usr/lib/constants.sh | 24 + netshift/files/usr/lib/helpers.sh | 31 +- netshift/files/usr/lib/nft.sh | 9 +- .../files/usr/lib/sing_box_config_manager.sh | 60 +- tests/entrypoint.sh | 120 ++- 23 files changed, 2314 insertions(+), 1364 deletions(-) diff --git a/.gitignore b/.gitignore index f68151c7..ce520007 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ yarn-debug.log* yarn-error.log* fe-app-netshift/.yarn/ fe-app-netshift/.yarnrc.yml -fe-app-netshift/.pnp.* \ No newline at end of file +fe-app-netshift/.pnp.* +agent/ diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index 891f86af..5f151960 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -3,2127 +3,2165 @@ "call": "✔ Enabled", "key": "✔ Enabled", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:345" + "src/netshift/tabs/dashboard/initController.ts:345" ] }, { "call": "✔ Running", "key": "✔ Running", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:356" + "src/netshift/tabs/dashboard/initController.ts:356" ] }, { "call": "✘ Disabled", "key": "✘ Disabled", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:346" + "src/netshift/tabs/dashboard/initController.ts:346" ] }, { "call": "✘ Stopped", "key": "✘ Stopped", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:357" - ] - }, - { - "call": "Группировать по странам", - "key": "Группировать по странам", - "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:127" - ] - }, - { - "call": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", - "key": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", - "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:128" + "src/netshift/tabs/dashboard/initController.ts:357" ] }, { "call": "Active Connections", "key": "Active Connections", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:307" + "src/netshift/tabs/dashboard/initController.ts:307" ] }, { "call": "Additional marking rules found", "key": "Additional marking rules found", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:106" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:106" ] }, { "call": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "key": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:292" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290" ] }, { "call": "Applicable for SOCKS and Shadowsocks proxy", "key": "Applicable for SOCKS and Shadowsocks proxy", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:269" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:300" ] }, { "call": "At least one valid domain must be specified. Comments-only content is not allowed.", "key": "At least one valid domain must be specified. Comments-only content is not allowed.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:514" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:559" ] }, { "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:595" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:640" ] }, { "call": "Available actions", "key": "Available actions", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:47" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:47" + ] + }, + { + "call": "Block DoH Servers", + "key": "Block DoH Servers", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:462" ] }, { "call": "Bootsrap DNS", "key": "Bootsrap DNS", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:65" + "src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:65" ] }, { "call": "Bootstrap DNS server", "key": "Bootstrap DNS server", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:45" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45" ] }, { "call": "Browser is not using FakeIP", "key": "Browser is not using FakeIP", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:58" + "src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:58" ] }, { "call": "Browser is using FakeIP correctly", "key": "Browser is using FakeIP correctly", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:57" + "src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:57" ] }, { "call": "Cache File Path", "key": "Cache File Path", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:393" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:399" ] }, { "call": "Cache file path cannot be empty", "key": "Cache file path cannot be empty", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:407" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:413" ] }, { "call": "Cannot receive checks result", "key": "Cannot receive checks result", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:27", - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:28", - "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:27", - "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:25" + "src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:27", + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:28", + "src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:27", + "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:25" ] }, { "call": "Checking, please wait", "key": "Checking, please wait", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:15", - "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:15", - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:13", - "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:15", - "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:13" + "src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:15", + "src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:15", + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:13", + "src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:15", + "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:13" ] }, { "call": "checks", "key": "checks", "places": [ - "src\\netshift\\tabs\\diagnostic\\helpers\\getCheckTitle.ts:2" + "src/netshift/tabs/diagnostic/helpers/getCheckTitle.ts:2" ] }, { "call": "Checks failed", "key": "Checks failed", "places": [ - "src\\netshift\\tabs\\diagnostic\\helpers\\getMeta.ts:26" + "src/netshift/tabs/diagnostic/helpers/getMeta.ts:26" ] }, { "call": "Checks passed", "key": "Checks passed", "places": [ - "src\\netshift\\tabs\\diagnostic\\helpers\\getMeta.ts:13" + "src/netshift/tabs/diagnostic/helpers/getMeta.ts:13" ] }, { "call": "CIDR must be between 0 and 32", "key": "CIDR must be between 0 and 32", "places": [ - "src\\validators\\validateSubnet.ts:33" + "src/validators/validateSubnet.ts:25" ] }, { "call": "Close", "key": "Close", "places": [ - "src\\partials\\modal\\renderModal.ts:26" + "src/partials/modal/renderModal.ts:26" ] }, { "call": "Community Lists", "key": "Community Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:369" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:414" ] }, { "call": "Config File Path", "key": "Config File Path", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:380" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:386" ] }, { "call": "Configuration for NetShift service", "key": "Configuration for NetShift service", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:27" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:27" ] }, { "call": "Configuration Type", "key": "Configuration Type", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:23" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:23" ] }, { "call": "Connection Type", "key": "Connection Type", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:12" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:12" ] }, { "call": "Connection URL", "key": "Connection URL", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:26" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:26" ] }, { "call": "Copy", "key": "Copy", "places": [ - "src\\partials\\modal\\renderModal.ts:20" + "src/partials/modal/renderModal.ts:20" ] }, { "call": "Core switch failed", "key": "Core switch failed", "places": [ - "src\\netshift\\methods\\shell\\index.ts:157", - "src\\netshift\\methods\\shell\\pollSingBoxComponentAction.ts:65" + "src/netshift/methods/shell/index.ts:157", + "src/netshift/methods/shell/pollSingBoxComponentAction.ts:65" ] }, { "call": "Core switch timed out", "key": "Core switch timed out", "places": [ - "src\\netshift\\methods\\shell\\pollSingBoxComponentAction.ts:82" + "src/netshift/methods/shell/pollSingBoxComponentAction.ts:82" ] }, { "call": "Currently unavailable", "key": "Currently unavailable", "places": [ - "src\\netshift\\tabs\\dashboard\\partials\\renderWidget.ts:22" + "src/netshift/tabs/dashboard/partials/renderWidget.ts:22" ] }, { "call": "Dashboard", "key": "Dashboard", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:80" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:80" ] }, { "call": "Dashboard currently unavailable", "key": "Dashboard currently unavailable", "places": [ - "src\\netshift\\tabs\\dashboard\\partials\\renderSections.ts:19" + "src/netshift/tabs/dashboard/partials/renderSections.ts:19" ] }, { "call": "Delay in milliseconds before reloading NetShift after interface UP", "key": "Delay in milliseconds before reloading NetShift after interface UP", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:267" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:265" ] }, { "call": "Delay value cannot be empty", "key": "Delay value cannot be empty", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:274" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:272" ] }, { "call": "DHCP has DNS server", "key": "DHCP has DNS server", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:93" + "src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:93" ] }, { "call": "Diagnostics", "key": "Diagnostics", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:65" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:65" ] }, { "call": "Disable autostart", "key": "Disable autostart", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:83" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:83" ] }, { "call": "Disable QUIC", "key": "Disable QUIC", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:310" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:312" ] }, { "call": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "key": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:311" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:313" ] }, { "call": "Disabled", "key": "Disabled", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:460", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:540" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585" ] }, { "call": "DNS on router", "key": "DNS on router", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:88" + "src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:88" ] }, { "call": "DNS outbound section", "key": "DNS outbound section", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:79" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:79" ] }, { "call": "DNS over HTTPS (DoH)", "key": "DNS over HTTPS (DoH)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:337", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:15" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15" ] }, { "call": "DNS over TLS (DoT)", "key": "DNS over TLS (DoT)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:338", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:16" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16" ] }, { "call": "DNS Protocol Type", "key": "DNS Protocol Type", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:334", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:12" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:379", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12" ] }, { "call": "DNS Rewrite TTL", "key": "DNS Rewrite TTL", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:113" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:113" ] }, { "call": "DNS Server", "key": "DNS Server", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:347", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:24" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:392", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24" ] }, { "call": "DNS server address cannot be empty", "key": "DNS server address cannot be empty", "places": [ - "src\\validators\\validateDns.ts:7" + "src/validators/validateDns.ts:7" ] }, { "call": "Do not panic, everything can be fixed, just...", "key": "Do not panic, everything can be fixed, just...", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:26" + "src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26" ] }, { "call": "Domain Resolver", "key": "Domain Resolver", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:324" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369" ] }, { "call": "Dont Touch My DHCP!", "key": "Dont Touch My DHCP!", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:371" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:377" ] }, { "call": "Downlink", "key": "Downlink", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:241", - "src\\netshift\\tabs\\dashboard\\initController.ts:275" + "src/netshift/tabs/dashboard/initController.ts:241", + "src/netshift/tabs/dashboard/initController.ts:275" ] }, { "call": "Download", "key": "Download", "places": [ - "src\\partials\\modal\\renderModal.ts:15" + "src/partials/modal/renderModal.ts:15" ] }, { "call": "Download Lists via Proxy/VPN", "key": "Download Lists via Proxy/VPN", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:333" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:335" ] }, { "call": "Download Lists via specific proxy section", "key": "Download Lists via specific proxy section", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:342" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:344" ] }, { "call": "Downloading all lists via specific Proxy/VPN", "key": "Downloading all lists via specific Proxy/VPN", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:334", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:343" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:336", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:345" ] }, { "call": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "key": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:147" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155" ] }, { "call": "Dynamic List", "key": "Dynamic List", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:461", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:541" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586" ] }, { "call": "Enable autostart", "key": "Enable autostart", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:93" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:93" ] }, { "call": "Enable built-in DNS resolver for domains handled by this section", "key": "Enable built-in DNS resolver for domains handled by this section", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:325" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370" ] }, { "call": "Enable DNS resolve to get real IP when routing", "key": "Enable DNS resolve to get real IP when routing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:764" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:809" + ] + }, + { + "call": "Enable IPv6 Support", + "key": "Enable IPv6 Support", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:476" ] }, { "call": "Enable Mixed Proxy", "key": "Enable Mixed Proxy", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:735" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:780" ] }, { "call": "Enable Output Network Interface", "key": "Enable Output Network Interface", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:171" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:171" ] }, { "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:736" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:781" ] }, { "call": "Enable YACD", "key": "Enable YACD", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:282" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:280" ] }, { "call": "Enable YACD WAN Access", "key": "Enable YACD WAN Access", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:291" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:289" ] }, { "call": "Enter complete outbound configuration in JSON format", "key": "Enter complete outbound configuration in JSON format", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:67" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:69" ] }, { "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:496" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:541" ] }, { "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:470" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515" ] }, { "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:550" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:595" ] }, { "call": "Enter the subscription URL to fetch proxy configurations from your provider", "key": "Enter the subscription URL to fetch proxy configurations from your provider", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:90" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92" ] }, { "call": "Every 1 minute", "key": "Every 1 minute", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:205" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219" ] }, { "call": "Every 12 hours", "key": "Every 12 hours", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:119" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123" ] }, { "call": "Every 3 hours", "key": "Every 3 hours", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:117" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121" ] }, { "call": "Every 3 minutes", "key": "Every 3 minutes", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:206" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220" ] }, { "call": "Every 30 minutes", "key": "Every 30 minutes", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:115" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119" ] }, { "call": "Every 30 seconds", "key": "Every 30 seconds", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:204" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:218" ] }, { "call": "Every 5 minutes", "key": "Every 5 minutes", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:207" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:221" ] }, { "call": "Every 6 hours", "key": "Every 6 hours", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:118" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122" ] }, { "call": "Every day", "key": "Every day", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:120" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:124" ] }, { "call": "Every hour", "key": "Every hour", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:116" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:120" ] }, { "call": "Exclude NTP", "key": "Exclude NTP", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:447" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:451" ] }, { "call": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "key": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:448" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:452" ] }, { "call": "Exclude servers by keyword", "key": "Exclude servers by keyword", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:146" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154" ] }, { "call": "Failed to copy!", "key": "Failed to copy!", "places": [ - "src\\helpers\\copyToClipboard.ts:12" + "src/helpers/copyToClipboard.ts:12" ] }, { "call": "Failed to execute!", "key": "Failed to execute!", "places": [ - "src\\netshift\\tabs\\diagnostic\\initController.ts:229", - "src\\netshift\\tabs\\diagnostic\\initController.ts:233", - "src\\netshift\\tabs\\diagnostic\\initController.ts:263", - "src\\netshift\\tabs\\diagnostic\\initController.ts:267", - "src\\netshift\\tabs\\diagnostic\\initController.ts:304", - "src\\netshift\\tabs\\diagnostic\\initController.ts:308", - "src\\netshift\\tabs\\diagnostic\\initController.ts:347", - "src\\netshift\\tabs\\diagnostic\\initController.ts:351" + "src/netshift/tabs/diagnostic/initController.ts:229", + "src/netshift/tabs/diagnostic/initController.ts:233", + "src/netshift/tabs/diagnostic/initController.ts:263", + "src/netshift/tabs/diagnostic/initController.ts:267", + "src/netshift/tabs/diagnostic/initController.ts:304", + "src/netshift/tabs/diagnostic/initController.ts:308", + "src/netshift/tabs/diagnostic/initController.ts:347", + "src/netshift/tabs/diagnostic/initController.ts:351" ] }, { "call": "Fastest", "key": "Fastest", "places": [ - "src\\netshift\\methods\\custom\\getDashboardSections.ts:150", - "src\\netshift\\methods\\custom\\getDashboardSections.ts:181", - "src\\netshift\\methods\\custom\\getDashboardSections.ts:218", - "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:58" + "src/netshift/methods/custom/getDashboardSections.ts:150", + "src/netshift/methods/custom/getDashboardSections.ts:181", + "src/netshift/methods/custom/getDashboardSections.ts:218", + "src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:58" ] }, { "call": "Fully Routed IPs", "key": "Fully Routed IPs", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:708" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:753" ] }, { "call": "Get global check", "key": "Get global check", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:102" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:102" ] }, { "call": "Global check", "key": "Global check", "places": [ - "src\\netshift\\tabs\\diagnostic\\initController.ts:224" + "src/netshift/tabs/diagnostic/initController.ts:224" + ] + }, + { + "call": "Global Proxy", + "key": "Global Proxy", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309" ] }, { "call": "How often to automatically update the subscription", "key": "How often to automatically update the subscription", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:113" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117" ] }, { "call": "HTTP error", "key": "HTTP error", "places": [ - "src\\netshift\\api.ts:27" + "src/netshift/api.ts:27" ] }, { "call": "Include servers by keyword", "key": "Include servers by keyword", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:137" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:143" ] }, { "call": "Install extended", "key": "Install extended", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:129" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129" ] }, { "call": "Install stable", "key": "Install stable", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:129" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129" ] }, { "call": "Interface Monitoring", "key": "Interface Monitoring", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:234" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:232" ] }, { "call": "Interface Monitoring Delay", "key": "Interface Monitoring Delay", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:266" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:264" ] }, { "call": "Interface monitoring for Bad WAN", "key": "Interface monitoring for Bad WAN", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:235" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:233" ] }, { - "call": "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH", - "key": "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH", + "call": "Invalid DNS server format. Examples: 8.8.8.8, [::1], dns.example.com, or dns.example.com/dns-query for DoH", + "key": "Invalid DNS server format. Examples: 8.8.8.8, [::1], dns.example.com, or dns.example.com/dns-query for DoH", "places": [ - "src\\validators\\validateDns.ts:23" + "src/validators/validateDns.ts:39" ] }, { "call": "Invalid domain address", "key": "Invalid domain address", "places": [ - "src\\validators\\validateDomain.ts:18", - "src\\validators\\validateDomain.ts:27" + "src/validators/validateDomain.ts:18", + "src/validators/validateDomain.ts:27" ] }, { - "call": "Invalid format. Use X.X.X.X or X.X.X.X/Y", - "key": "Invalid format. Use X.X.X.X or X.X.X.X/Y", + "call": "Invalid format. Use X.X.X.X/Y or IPv6/Y", + "key": "Invalid format. Use X.X.X.X/Y or IPv6/Y", "places": [ - "src\\validators\\validateSubnet.ts:11" + "src/validators/validateSubnet.ts:57" ] }, { "call": "Invalid HY2 URL: insecure must be 0 or 1", "key": "Invalid HY2 URL: insecure must be 0 or 1", "places": [ - "src\\validators\\validateHysteriaUrl.ts:90" + "src/validators/validateHysteriaUrl.ts:90" ] }, { "call": "Invalid HY2 URL: invalid port number", "key": "Invalid HY2 URL: invalid port number", "places": [ - "src\\validators\\validateHysteriaUrl.ts:77" + "src/validators/validateHysteriaUrl.ts:77" ] }, { "call": "Invalid HY2 URL: missing credentials/server", "key": "Invalid HY2 URL: missing credentials/server", "places": [ - "src\\validators\\validateHysteriaUrl.ts:30" + "src/validators/validateHysteriaUrl.ts:30" ] }, { "call": "Invalid HY2 URL: missing host", "key": "Invalid HY2 URL: missing host", "places": [ - "src\\validators\\validateHysteriaUrl.ts:47" + "src/validators/validateHysteriaUrl.ts:47" ] }, { "call": "Invalid HY2 URL: missing host & port", "key": "Invalid HY2 URL: missing host & port", "places": [ - "src\\validators\\validateHysteriaUrl.ts:41" + "src/validators/validateHysteriaUrl.ts:41" ] }, { "call": "Invalid HY2 URL: missing password", "key": "Invalid HY2 URL: missing password", "places": [ - "src\\validators\\validateHysteriaUrl.ts:36" + "src/validators/validateHysteriaUrl.ts:36" ] }, { "call": "Invalid HY2 URL: missing port", "key": "Invalid HY2 URL: missing port", "places": [ - "src\\validators\\validateHysteriaUrl.ts:50" + "src/validators/validateHysteriaUrl.ts:50" ] }, { "call": "Invalid HY2 URL: must not contain spaces", "key": "Invalid HY2 URL: must not contain spaces", "places": [ - "src\\validators\\validateHysteriaUrl.ts:18" + "src/validators/validateHysteriaUrl.ts:18" ] }, { "call": "Invalid HY2 URL: must start with hysteria2:// or hy2://", "key": "Invalid HY2 URL: must start with hysteria2:// or hy2://", "places": [ - "src\\validators\\validateHysteriaUrl.ts:12" + "src/validators/validateHysteriaUrl.ts:12" ] }, { "call": "Invalid HY2 URL: obfs-password required when obfs is set", "key": "Invalid HY2 URL: obfs-password required when obfs is set", "places": [ - "src\\validators\\validateHysteriaUrl.ts:108" + "src/validators/validateHysteriaUrl.ts:108" ] }, { "call": "Invalid HY2 URL: parsing failed", "key": "Invalid HY2 URL: parsing failed", "places": [ - "src\\validators\\validateHysteriaUrl.ts:122" + "src/validators/validateHysteriaUrl.ts:122" ] }, { "call": "Invalid HY2 URL: sni cannot be empty", "key": "Invalid HY2 URL: sni cannot be empty", "places": [ - "src\\validators\\validateHysteriaUrl.ts:116" + "src/validators/validateHysteriaUrl.ts:116" ] }, { "call": "Invalid HY2 URL: unsupported obfs type", "key": "Invalid HY2 URL: unsupported obfs type", "places": [ - "src\\validators\\validateHysteriaUrl.ts:98" + "src/validators/validateHysteriaUrl.ts:98" ] }, { "call": "Invalid IP address", "key": "Invalid IP address", "places": [ - "src\\validators\\validateIp.ts:11" + "src/validators/validateIp.ts:11" + ] + }, + { + "call": "Invalid IPv6 address", + "key": "Invalid IPv6 address", + "places": [ + "src/validators/validateIp.ts:28" ] }, { "call": "Invalid JSON format", "key": "Invalid JSON format", "places": [ - "src\\validators\\validateOutboundJson.ts:9" + "src/validators/validateOutboundJson.ts:9" ] }, { "call": "Invalid path format. Path must start with \"/\" and contain valid characters", "key": "Invalid path format. Path must start with \"/\" and contain valid characters", "places": [ - "src\\validators\\validatePath.ts:22" + "src/validators/validatePath.ts:22" ] }, { "call": "Invalid port number. Must be between 1 and 65535", "key": "Invalid port number. Must be between 1 and 65535", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:85" + "src/validators/validateShadowsocksUrl.ts:85" ] }, { "call": "Invalid Shadowsocks URL: decoded credentials must contain method:password", "key": "Invalid Shadowsocks URL: decoded credentials must contain method:password", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:37" + "src/validators/validateShadowsocksUrl.ts:37" ] }, { "call": "Invalid Shadowsocks URL: missing credentials", "key": "Invalid Shadowsocks URL: missing credentials", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:27" + "src/validators/validateShadowsocksUrl.ts:27" ] }, { "call": "Invalid Shadowsocks URL: missing method and password separator \":\"", "key": "Invalid Shadowsocks URL: missing method and password separator \":\"", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:46" + "src/validators/validateShadowsocksUrl.ts:46" ] }, { "call": "Invalid Shadowsocks URL: missing port", "key": "Invalid Shadowsocks URL: missing port", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:76" + "src/validators/validateShadowsocksUrl.ts:76" ] }, { "call": "Invalid Shadowsocks URL: missing server", "key": "Invalid Shadowsocks URL: missing server", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:67" + "src/validators/validateShadowsocksUrl.ts:67" ] }, { "call": "Invalid Shadowsocks URL: missing server address", "key": "Invalid Shadowsocks URL: missing server address", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:58" + "src/validators/validateShadowsocksUrl.ts:58" ] }, { "call": "Invalid Shadowsocks URL: must not contain spaces", "key": "Invalid Shadowsocks URL: must not contain spaces", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:16" + "src/validators/validateShadowsocksUrl.ts:16" ] }, { "call": "Invalid Shadowsocks URL: must start with ss://", "key": "Invalid Shadowsocks URL: must start with ss://", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:8" + "src/validators/validateShadowsocksUrl.ts:8" ] }, { "call": "Invalid Shadowsocks URL: parsing failed", "key": "Invalid Shadowsocks URL: parsing failed", "places": [ - "src\\validators\\validateShadowsocksUrl.ts:91" + "src/validators/validateShadowsocksUrl.ts:91" ] }, { "call": "Invalid SOCKS URL: invalid host format", "key": "Invalid SOCKS URL: invalid host format", "places": [ - "src\\validators\\validateSocksUrl.ts:73" + "src/validators/validateSocksUrl.ts:73" ] }, { "call": "Invalid SOCKS URL: invalid port number", "key": "Invalid SOCKS URL: invalid port number", "places": [ - "src\\validators\\validateSocksUrl.ts:63" + "src/validators/validateSocksUrl.ts:63" ] }, { "call": "Invalid SOCKS URL: missing host and port", "key": "Invalid SOCKS URL: missing host and port", "places": [ - "src\\validators\\validateSocksUrl.ts:42" + "src/validators/validateSocksUrl.ts:42" ] }, { "call": "Invalid SOCKS URL: missing hostname or IP", "key": "Invalid SOCKS URL: missing hostname or IP", "places": [ - "src\\validators\\validateSocksUrl.ts:51" + "src/validators/validateSocksUrl.ts:51" ] }, { "call": "Invalid SOCKS URL: missing port", "key": "Invalid SOCKS URL: missing port", "places": [ - "src\\validators\\validateSocksUrl.ts:56" + "src/validators/validateSocksUrl.ts:56" ] }, { "call": "Invalid SOCKS URL: missing username", "key": "Invalid SOCKS URL: missing username", "places": [ - "src\\validators\\validateSocksUrl.ts:34" + "src/validators/validateSocksUrl.ts:34" ] }, { "call": "Invalid SOCKS URL: must not contain spaces", "key": "Invalid SOCKS URL: must not contain spaces", "places": [ - "src\\validators\\validateSocksUrl.ts:19" + "src/validators/validateSocksUrl.ts:19" ] }, { "call": "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://", "key": "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://", "places": [ - "src\\validators\\validateSocksUrl.ts:10" + "src/validators/validateSocksUrl.ts:10" ] }, { "call": "Invalid SOCKS URL: parsing failed", "key": "Invalid SOCKS URL: parsing failed", "places": [ - "src\\validators\\validateSocksUrl.ts:77" + "src/validators/validateSocksUrl.ts:77" ] }, { "call": "Invalid Trojan URL: must not contain spaces", "key": "Invalid Trojan URL: must not contain spaces", "places": [ - "src\\validators\\validateTrojanUrl.ts:15" + "src/validators/validateTrojanUrl.ts:15" ] }, { "call": "Invalid Trojan URL: must start with trojan://", "key": "Invalid Trojan URL: must start with trojan://", "places": [ - "src\\validators\\validateTrojanUrl.ts:8" + "src/validators/validateTrojanUrl.ts:8" ] }, { "call": "Invalid Trojan URL: parsing failed", "key": "Invalid Trojan URL: parsing failed", "places": [ - "src\\validators\\validateTrojanUrl.ts:56" + "src/validators/validateTrojanUrl.ts:56" ] }, { "call": "Invalid URL format", "key": "Invalid URL format", "places": [ - "src\\validators\\validateUrl.ts:8", - "src\\validators\\validateUrl.ts:31" + "src/validators/validateUrl.ts:8", + "src/validators/validateUrl.ts:31" ] }, { "call": "Invalid VLESS URL: parsing failed", "key": "Invalid VLESS URL: parsing failed", "places": [ - "src\\validators\\validateVlessUrl.ts:110" + "src/validators/validateVlessUrl.ts:110" ] }, { "call": "Invalid VMess URL: invalid port", "key": "Invalid VMess URL: invalid port", "places": [ - "src\\validators\\validateVmessUrl.ts:82" + "src/validators/validateVmessUrl.ts:82" ] }, { "call": "Invalid VMess URL: malformed base64", "key": "Invalid VMess URL: malformed base64", "places": [ - "src\\validators\\validateVmessUrl.ts:40" + "src/validators/validateVmessUrl.ts:40" ] }, { "call": "Invalid VMess URL: malformed JSON", "key": "Invalid VMess URL: malformed JSON", "places": [ - "src\\validators\\validateVmessUrl.ts:50", - "src\\validators\\validateVmessUrl.ts:57" + "src/validators/validateVmessUrl.ts:50", + "src/validators/validateVmessUrl.ts:57" ] }, { "call": "Invalid VMess URL: missing address", "key": "Invalid VMess URL: missing address", "places": [ - "src\\validators\\validateVmessUrl.ts:66" + "src/validators/validateVmessUrl.ts:66" ] }, { "call": "Invalid VMess URL: missing id", "key": "Invalid VMess URL: missing id", "places": [ - "src\\validators\\validateVmessUrl.ts:73" + "src/validators/validateVmessUrl.ts:73" ] }, { "call": "Invalid VMess URL: must not contain spaces", "key": "Invalid VMess URL: must not contain spaces", "places": [ - "src\\validators\\validateVmessUrl.ts:25" + "src/validators/validateVmessUrl.ts:25" ] }, { "call": "Invalid VMess URL: must start with vmess://", "key": "Invalid VMess URL: must start with vmess://", "places": [ - "src\\validators\\validateVmessUrl.ts:7" + "src/validators/validateVmessUrl.ts:7" ] }, { "call": "IP address 0.0.0.0 is not allowed", "key": "IP address 0.0.0.0 is not allowed", "places": [ - "src\\validators\\validateSubnet.ts:18" + "src/validators/validateSubnet.ts:11" + ] + }, + { + "call": "IPv6 CIDR must be between 0 and 128", + "key": "IPv6 CIDR must be between 0 and 128", + "places": [ + "src/validators/validateSubnet.ts:47" ] }, { "call": "Issues detected", "key": "Issues detected", "places": [ - "src\\netshift\\tabs\\diagnostic\\helpers\\getMeta.ts:20" + "src/netshift/tabs/diagnostic/helpers/getMeta.ts:20" ] }, { "call": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "key": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:138" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:144" ] }, { "call": "Latest", "key": "Latest", "places": [ - "src\\netshift\\tabs\\diagnostic\\helpers\\getNetshiftVersionRow.ts:48" + "src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:48" ] }, { "call": "List Update Frequency", "key": "List Update Frequency", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:321" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:323" ] }, { "call": "Local Domain Lists", "key": "Local Domain Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:616" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661" ] }, { "call": "Local Subnet Lists", "key": "Local Subnet Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:639" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:684" ] }, { "call": "Log Level", "key": "Log Level", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:429" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:435" ] }, { "call": "Main DNS", "key": "Main DNS", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:72" + "src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:72" ] }, { "call": "Main DNS via outbound", "key": "Main DNS via outbound", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runDnsCheck.ts:81" + "src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:81" ] }, { "call": "Memory Usage", "key": "Memory Usage", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:311" + "src/netshift/tabs/dashboard/initController.ts:311" ] }, { "call": "Mixed Proxy Port", "key": "Mixed Proxy Port", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:748" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793" ] }, { "call": "Monitored Interfaces", "key": "Monitored Interfaces", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:243" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:241" ] }, { "call": "Must be a number in the range of 50 - 1000", "key": "Must be a number in the range of 50 - 1000", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:233" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:255" ] }, { "call": "NetShift", "key": "NetShift", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:343" + "src/netshift/tabs/dashboard/initController.ts:343" ] }, { "call": "NetShift Settings", "key": "NetShift Settings", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:26" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:26" ] }, { "call": "NetShift will not modify your DHCP configuration", "key": "NetShift will not modify your DHCP configuration", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:372" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:378" ] }, { "call": "Network Interface", "key": "Network Interface", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:278" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323" ] }, { "call": "No other marking rules found", "key": "No other marking rules found", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:105" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:105" ] }, { "call": "Not implement yet", "key": "Not implement yet", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderCheckSection.ts:189" + "src/netshift/tabs/diagnostic/partials/renderCheckSection.ts:189" ] }, { "call": "Not responding", "key": "Not responding", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:74", - "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:80", - "src\\netshift\\tabs\\diagnostic\\checks\\runSectionsCheck.ts:99" + "src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:74", + "src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:80", + "src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:99" ] }, { "call": "Not running", "key": "Not running", "places": [ - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:59", - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:67", - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:75", - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:83", - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:91" + "src/netshift/tabs/diagnostic/diagnostic.store.ts:59", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:67", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:75", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:83", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:91" ] }, { "call": "Operation timed out", "key": "Operation timed out", "places": [ - "src\\helpers\\withTimeout.ts:7" + "src/helpers/withTimeout.ts:7" ] }, { "call": "Outbound Config", "key": "Outbound Config", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:30" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:30" ] }, { "call": "Outbound Configuration", "key": "Outbound Configuration", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:66" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:68" ] }, { "call": "Outdated", "key": "Outdated", "places": [ - "src\\netshift\\tabs\\diagnostic\\helpers\\getNetshiftVersionRow.ts:38" + "src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:38" ] }, { "call": "Output Network Interface", "key": "Output Network Interface", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:180" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:180" ] }, { "call": "Path cannot be empty", "key": "Path cannot be empty", "places": [ - "src\\validators\\validatePath.ts:7" + "src/validators/validatePath.ts:7" ] }, { "call": "Path must be absolute (start with /)", "key": "Path must be absolute (start with /)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:411" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:417" ] }, { "call": "Path must contain at least one directory (like /tmp/cache.db)", "key": "Path must contain at least one directory (like /tmp/cache.db)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:420" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:426" ] }, { "call": "Path must end with cache.db", "key": "Path must end with cache.db", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:415" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:421" ] }, { "call": "Pending", "key": "Pending", "places": [ - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:107", - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:115", - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:123", - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:131", - "src\\netshift\\tabs\\diagnostic\\diagnostic.store.ts:139" + "src/netshift/tabs/diagnostic/diagnostic.store.ts:107", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:115", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:123", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:131", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:139" ] }, { "call": "Proxy Configuration URL", "key": "Proxy Configuration URL", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:37" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:37" ] }, { "call": "Proxy traffic is not routed via FakeIP", "key": "Proxy traffic is not routed via FakeIP", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:66" + "src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:66" ] }, { "call": "Proxy traffic is routed via FakeIP", "key": "Proxy traffic is routed via FakeIP", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:65" + "src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:65" ] }, { "call": "Regional options cannot be used together", "key": "Regional options cannot be used together", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:403" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:448" ] }, { "call": "Remote Domain Lists", "key": "Remote Domain Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:662" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:707" ] }, { "call": "Remote Subnet Lists", "key": "Remote Subnet Lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:685" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:730" ] }, { "call": "Resolve real IP for routing", "key": "Resolve real IP for routing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:763" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808" ] }, { "call": "Restart NetShift", "key": "Restart NetShift", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:53" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:53" ] }, { "call": "Route main DNS through proxy/VPN", "key": "Route main DNS through proxy/VPN", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:68" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:68" ] }, { "call": "Router DNS is not routed through sing-box", "key": "Router DNS is not routed through sing-box", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:51" + "src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:51" ] }, { "call": "Router DNS is routed through sing-box", "key": "Router DNS is routed through sing-box", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runFakeIPCheck.ts:50" + "src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:50" ] }, { "call": "Routing Excluded IPs", "key": "Routing Excluded IPs", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:458" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:488" ] }, { "call": "Rules mangle counters", "key": "Rules mangle counters", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:79" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:79" ] }, { "call": "Rules mangle exist", "key": "Rules mangle exist", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:74" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:74" ] }, { "call": "Rules mangle output counters", "key": "Rules mangle output counters", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:89" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:89" ] }, { "call": "Rules mangle output exist", "key": "Rules mangle output exist", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:84" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:84" ] }, { "call": "Rules proxy counters", "key": "Rules proxy counters", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:99" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99" ] }, { "call": "Rules proxy exist", "key": "Rules proxy exist", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:94" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:94" ] }, { "call": "Run Diagnostic", "key": "Run Diagnostic", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderRunAction.ts:15" + "src/netshift/tabs/diagnostic/partials/renderRunAction.ts:15" ] }, { "call": "Russia inside restrictions", "key": "Russia inside restrictions", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:422" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467" ] }, { "call": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "key": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:302" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:302" ] }, { "call": "Sections", "key": "Sections", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:36" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:36" ] }, { "call": "Select a predefined list for routing", "key": "Select a predefined list for routing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:370" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:415" ] }, { "call": "Select between VPN and Proxy connection methods for traffic routing", "key": "Select between VPN and Proxy connection methods for traffic routing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:13" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:13" ] }, { "call": "Select DNS protocol to use", "key": "Select DNS protocol to use", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:13" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:13" ] }, { "call": "Select how often the domain or subnet lists are updated automatically", "key": "Select how often the domain or subnet lists are updated automatically", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:322" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:324" ] }, { "call": "Select how to configure the proxy", "key": "Select how to configure the proxy", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:24" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:24" ] }, { "call": "Select network interface for VPN connection", "key": "Select network interface for VPN connection", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:279" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324" ] }, { "call": "Select or enter DNS server address", "key": "Select or enter DNS server address", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:348", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:25" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:393", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25" ] }, { "call": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "key": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:394" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:400" ] }, { "call": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "key": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:381" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:387" ] }, { "call": "Select the DNS protocol type for the domain resolver", "key": "Select the DNS protocol type for the domain resolver", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:335" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:380" ] }, { "call": "Select the list type for adding custom domains", "key": "Select the list type for adding custom domains", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:458" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:503" ] }, { "call": "Select the list type for adding custom subnets", "key": "Select the list type for adding custom subnets", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:538" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:583" ] }, { "call": "Select the log level for sing-box", "key": "Select the log level for sing-box", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:430" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:436" ] }, { "call": "Select the network interface from which the traffic will originate", "key": "Select the network interface from which the traffic will originate", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:135" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:135" ] }, { "call": "Select the network interface to which the traffic will originate", "key": "Select the network interface to which the traffic will originate", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:181" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:181" ] }, { "call": "Select the WAN interfaces to be monitored", "key": "Select the WAN interfaces to be monitored", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:244" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:242" ] }, { "call": "Selector", "key": "Selector", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:27" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:27" ] }, { "call": "Selector Proxy Links", "key": "Selector Proxy Links", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:155" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165" ] }, { "call": "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct.", "key": "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:69" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:69" ] }, { "call": "Services info", "key": "Services info", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:340" + "src/netshift/tabs/dashboard/initController.ts:340" ] }, { "call": "Settings", "key": "Settings", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\netshift.js:49" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:49" ] }, { "call": "Show sing-box config", "key": "Show sing-box config", "places": [ - "src\\netshift\\tabs\\diagnostic\\initController.ts:292", - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:120" + "src/netshift/tabs/diagnostic/initController.ts:292", + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:120" ] }, { "call": "Sing-box", "key": "Sing-box", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:354" + "src/netshift/tabs/dashboard/initController.ts:354" ] }, { "call": "Sing-box autostart disabled", "key": "Sing-box autostart disabled", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:77" + "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:77" ] }, { "call": "Sing-box core changed, version:", "key": "Sing-box core changed, version:", "places": [ - "src\\netshift\\tabs\\diagnostic\\initController.ts:342" + "src/netshift/tabs/diagnostic/initController.ts:342" ] }, { "call": "Sing-box installed", "key": "Sing-box installed", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:62" + "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:62" ] }, { "call": "Sing-box listening ports", "key": "Sing-box listening ports", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:87" + "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:87" ] }, { "call": "Sing-box process running", "key": "Sing-box process running", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:82" + "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:82" ] }, { "call": "Sing-box service exist", "key": "Sing-box service exist", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:72" + "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:72" ] }, { "call": "Sing-box version is compatible (newer than 1.12.4)", "key": "Sing-box version is compatible (newer than 1.12.4)", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runSingBoxCheck.ts:67" + "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:67" ] }, { "call": "Source Network Interface", "key": "Source Network Interface", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:134" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:134" ] }, { "call": "Specify a local IP address to be excluded from routing", "key": "Specify a local IP address to be excluded from routing", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:459" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:489" ] }, { "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:709" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:754" ] }, { "call": "Specify remote URLs to download and use domain lists", "key": "Specify remote URLs to download and use domain lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:663" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:708" ] }, { "call": "Specify remote URLs to download and use subnet lists", "key": "Specify remote URLs to download and use subnet lists", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:686" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:731" ] }, { "call": "Specify the path to the list file located on the router filesystem", "key": "Specify the path to the list file located on the router filesystem", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:617", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:640" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:685" ] }, { "call": "Start NetShift", "key": "Start NetShift", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:73" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:73" ] }, { "call": "Stop NetShift", "key": "Stop NetShift", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:63" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:63" ] }, { "call": "Subscription", "key": "Subscription", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:29" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:29" ] }, { "call": "Subscription Update Interval", "key": "Subscription Update Interval", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:112" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116" ] }, { "call": "Subscription URL", "key": "Subscription URL", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:89" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:91" ] }, { "call": "Successfully copied!", "key": "Successfully copied!", "places": [ - "src\\helpers\\copyToClipboard.ts:10" + "src/helpers/copyToClipboard.ts:10" ] }, { "call": "Switching sing-box core, this may take a few minutes…", "key": "Switching sing-box core, this may take a few minutes…", "places": [ - "src\\netshift\\tabs\\diagnostic\\initController.ts:331" + "src/netshift/tabs/diagnostic/initController.ts:331" ] }, { "call": "System info", "key": "System info", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:304" + "src/netshift/tabs/dashboard/initController.ts:304" ] }, { "call": "System information", "key": "System information", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderSystemInfo.ts:21" + "src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts:21" ] }, { "call": "Table exist", "key": "Table exist", "places": [ - "src\\netshift\\tabs\\diagnostic\\checks\\runNftCheck.ts:69" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:69" ] }, { "call": "Test latency", "key": "Test latency", "places": [ - "src\\netshift\\tabs\\dashboard\\partials\\renderSections.ts:108" + "src/netshift/tabs/dashboard/partials/renderSections.ts:108" ] }, { "call": "Text List", "key": "Text List", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:462", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:542" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:507", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587" ] }, { "call": "The DNS server used to look up the IP address of an upstream DNS server", "key": "The DNS server used to look up the IP address of an upstream DNS server", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:46" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:46" ] }, { "call": "The interval between connectivity tests", "key": "The interval between connectivity tests", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:202" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216" ] }, { "call": "The maximum difference in response times (ms) allowed when comparing servers", "key": "The maximum difference in response times (ms) allowed when comparing servers", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:216" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230" ] }, { "call": "The URL used to test server connectivity", "key": "The URL used to test server connectivity", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:240" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:262" ] }, { "call": "Time in seconds for DNS record caching (default: 60)", "key": "Time in seconds for DNS record caching (default: 60)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:114" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:114" ] }, { "call": "Traffic", "key": "Traffic", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:238" + "src/netshift/tabs/dashboard/initController.ts:238" ] }, { "call": "Traffic Total", "key": "Traffic Total", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:268" + "src/netshift/tabs/dashboard/initController.ts:268" ] }, { "call": "Troubleshooting", "key": "Troubleshooting", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:25" + "src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:25" ] }, { "call": "TTL must be a positive number", "key": "TTL must be a positive number", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:125" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:125" ] }, { "call": "TTL value cannot be empty", "key": "TTL value cannot be empty", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:120" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:120" ] }, { "call": "UDP (Unprotected DNS)", "key": "UDP (Unprotected DNS)", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:339", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:17" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17" ] }, { "call": "UDP over TCP", "key": "UDP over TCP", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:268" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299" ] }, { "call": "unknown", "key": "unknown", "places": [ - "src\\netshift\\tabs\\diagnostic\\initController.ts:39", - "src\\netshift\\tabs\\diagnostic\\initController.ts:40", - "src\\netshift\\tabs\\diagnostic\\initController.ts:41", - "src\\netshift\\tabs\\diagnostic\\initController.ts:42", - "src\\netshift\\tabs\\diagnostic\\initController.ts:43", - "src\\netshift\\tabs\\diagnostic\\initController.ts:44", - "src\\netshift\\tabs\\diagnostic\\helpers\\getNetshiftVersionRow.ts:7" + "src/netshift/tabs/diagnostic/initController.ts:39", + "src/netshift/tabs/diagnostic/initController.ts:40", + "src/netshift/tabs/diagnostic/initController.ts:41", + "src/netshift/tabs/diagnostic/initController.ts:42", + "src/netshift/tabs/diagnostic/initController.ts:43", + "src/netshift/tabs/diagnostic/initController.ts:44", + "src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7" ] }, { "call": "Unknown error", "key": "Unknown error", "places": [ - "src\\netshift\\api.ts:40" + "src/netshift/api.ts:40" ] }, { "call": "Uplink", "key": "Uplink", "places": [ - "src\\netshift\\tabs\\dashboard\\initController.ts:240", - "src\\netshift\\tabs\\dashboard\\initController.ts:271" + "src/netshift/tabs/dashboard/initController.ts:240", + "src/netshift/tabs/dashboard/initController.ts:271" ] }, { "call": "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", "key": "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", "places": [ - "src\\validators\\validateProxyUrl.ts:42" + "src/validators/validateProxyUrl.ts:42" ] }, { "call": "URL must use one of the following protocols:", "key": "URL must use one of the following protocols:", "places": [ - "src\\validators\\validateUrl.ts:17" + "src/validators/validateUrl.ts:17" ] }, { "call": "URLTest", "key": "URLTest", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:28" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:28" ] }, { "call": "URLTest Check Interval", "key": "URLTest Check Interval", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:201" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:215" ] }, { "call": "URLTest Proxy Links", "key": "URLTest Proxy Links", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:178" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:190" ] }, { "call": "URLTest Testing URL", "key": "URLTest Testing URL", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:239" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:261" ] }, { "call": "URLTest Tolerance", "key": "URLTest Tolerance", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:215" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229" ] }, { "call": "User Domain List Type", "key": "User Domain List Type", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:457" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:502" ] }, { "call": "User Domains", "key": "User Domains", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:469" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514" ] }, { "call": "User Domains List", "key": "User Domains List", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:495" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540" ] }, { "call": "User Subnet List Type", "key": "User Subnet List Type", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:537" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:582" ] }, { "call": "User Subnets", "key": "User Subnets", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:549" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594" ] }, { "call": "User Subnets List", "key": "User Subnets List", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:575" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:620" ] }, { "call": "Valid", "key": "Valid", "places": [ - "src\\validators\\validateDns.ts:14", - "src\\validators\\validateDns.ts:18", - "src\\validators\\validateDomain.ts:13", - "src\\validators\\validateDomain.ts:30", - "src\\validators\\validateHysteriaUrl.ts:120", - "src\\validators\\validateIp.ts:8", - "src\\validators\\validateOutboundJson.ts:7", - "src\\validators\\validatePath.ts:16", - "src\\validators\\validateShadowsocksUrl.ts:95", - "src\\validators\\validateSocksUrl.ts:80", - "src\\validators\\validateSubnet.ts:38", - "src\\validators\\validateTrojanUrl.ts:59", - "src\\validators\\validateUrl.ts:28", - "src\\validators\\validateVlessUrl.ts:108", - "src\\validators\\validateVmessUrl.ts:86" + "src/validators/validateDns.ts:26", + "src/validators/validateDns.ts:30", + "src/validators/validateDns.ts:34", + "src/validators/validateDomain.ts:13", + "src/validators/validateDomain.ts:30", + "src/validators/validateHysteriaUrl.ts:120", + "src/validators/validateIp.ts:8", + "src/validators/validateIp.ts:24", + "src/validators/validateOutboundJson.ts:7", + "src/validators/validatePath.ts:16", + "src/validators/validateShadowsocksUrl.ts:95", + "src/validators/validateSocksUrl.ts:80", + "src/validators/validateSubnet.ts:30", + "src/validators/validateSubnet.ts:52", + "src/validators/validateTrojanUrl.ts:59", + "src/validators/validateUrl.ts:28", + "src/validators/validateVlessUrl.ts:108", + "src/validators/validateVmessUrl.ts:86" ] }, { "call": "Validation errors:", "key": "Validation errors:", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:528", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:607" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:652" ] }, { "call": "View logs", "key": "View logs", "places": [ - "src\\netshift\\tabs\\diagnostic\\initController.ts:258", - "src\\netshift\\tabs\\diagnostic\\partials\\renderAvailableActions.ts:111" + "src/netshift/tabs/diagnostic/initController.ts:258", + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:111" ] }, { "call": "Visit Wiki", "key": "Visit Wiki", "places": [ - "src\\netshift\\tabs\\diagnostic\\partials\\renderWikiDisclaimer.ts:31" + "src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:31" ] }, { "call": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:38", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:156", - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:179" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:38", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:191" ] }, { "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:405" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:450" ] }, { "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\section.js:424" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:469" ] }, { "call": "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound.", "key": "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound.", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:80" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80" ] }, { "call": "YACD Secret Key", "key": "YACD Secret Key", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:301" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:301" ] }, { "call": "You can select Output Network Interface, by default autodetect", "key": "You can select Output Network Interface, by default autodetect", "places": [ - "..\\luci-app-netshift\\htdocs\\luci-static\\resources\\view\\netshift\\settings.js:172" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:172" + ] + }, + { + "call": "Группировать по странам", + "key": "Группировать по странам", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131" + ] + }, + { + "call": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", + "key": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:132" ] } -] \ No newline at end of file +] diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index 37c06163..c70bb9d5 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -1,1262 +1,1285 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# yandexru45 <sukadark228@gmail.com>, 2026. +# spgsroot, yandexru45, 2026. #, fuzzy msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 18:43+0300\n" -"PO-Revision-Date: 2026-06-05 18:43+0300\n" -"Last-Translator: yandexru45 <sukadark228@gmail.com>\n" +"POT-Creation-Date: 2026-06-06 07:00+0800\n" +"PO-Revision-Date: 2026-06-06 07:00+0800\n" +"Last-Translator: spgsroot, yandexru45\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src\netshift\tabs\dashboard\initController.ts:345 +#: src/netshift/tabs/dashboard/initController.ts:345 msgid "✔ Enabled" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:356 +#: src/netshift/tabs/dashboard/initController.ts:356 msgid "✔ Running" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:346 +#: src/netshift/tabs/dashboard/initController.ts:346 msgid "✘ Disabled" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:357 +#: src/netshift/tabs/dashboard/initController.ts:357 msgid "✘ Stopped" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:127 -msgid "Группировать по странам" -msgstr "" - -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:128 -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" -msgstr "" - -#: src\netshift\tabs\dashboard\initController.ts:307 +#: src/netshift/tabs/dashboard/initController.ts:307 msgid "Active Connections" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:106 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:106 msgid "Additional marking rules found" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:292 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:269 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:300 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:514 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:559 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:595 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:640 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:47 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:47 msgid "Available actions" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:65 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:462 +msgid "Block DoH Servers" +msgstr "" + +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:65 msgid "Bootsrap DNS" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:45 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 msgid "Bootstrap DNS server" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:58 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:58 msgid "Browser is not using FakeIP" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:57 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:57 msgid "Browser is using FakeIP correctly" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:393 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:399 msgid "Cache File Path" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:407 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:413 msgid "Cache file path cannot be empty" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:27 -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:28 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:27 -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:25 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:27 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:28 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:27 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:25 msgid "Cannot receive checks result" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:15 -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:15 -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:13 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:15 -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:13 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:15 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:15 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:13 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:15 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:13 msgid "Checking, please wait" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getCheckTitle.ts:2 +#: src/netshift/tabs/diagnostic/helpers/getCheckTitle.ts:2 msgid "checks" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:26 +#: src/netshift/tabs/diagnostic/helpers/getMeta.ts:26 msgid "Checks failed" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:13 +#: src/netshift/tabs/diagnostic/helpers/getMeta.ts:13 msgid "Checks passed" msgstr "" -#: src\validators\validateSubnet.ts:33 +#: src/validators/validateSubnet.ts:25 msgid "CIDR must be between 0 and 32" msgstr "" -#: src\partials\modal\renderModal.ts:26 +#: src/partials/modal/renderModal.ts:26 msgid "Close" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:369 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:414 msgid "Community Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:380 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:386 msgid "Config File Path" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:27 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:27 msgid "Configuration for NetShift service" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:23 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:23 msgid "Configuration Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:12 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:12 msgid "Connection Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:26 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:26 msgid "Connection URL" msgstr "" -#: src\partials\modal\renderModal.ts:20 +#: src/partials/modal/renderModal.ts:20 msgid "Copy" msgstr "" -#: src\netshift\methods\shell\index.ts:157 -#: src\netshift\methods\shell\pollSingBoxComponentAction.ts:65 +#: src/netshift/methods/shell/index.ts:157 +#: src/netshift/methods/shell/pollSingBoxComponentAction.ts:65 msgid "Core switch failed" msgstr "" -#: src\netshift\methods\shell\pollSingBoxComponentAction.ts:82 +#: src/netshift/methods/shell/pollSingBoxComponentAction.ts:82 msgid "Core switch timed out" msgstr "" -#: src\netshift\tabs\dashboard\partials\renderWidget.ts:22 +#: src/netshift/tabs/dashboard/partials/renderWidget.ts:22 msgid "Currently unavailable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:80 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:80 msgid "Dashboard" msgstr "" -#: src\netshift\tabs\dashboard\partials\renderSections.ts:19 +#: src/netshift/tabs/dashboard/partials/renderSections.ts:19 msgid "Dashboard currently unavailable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:267 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:265 msgid "Delay in milliseconds before reloading NetShift after interface UP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:274 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:272 msgid "Delay value cannot be empty" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:93 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:93 msgid "DHCP has DNS server" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:65 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:65 msgid "Diagnostics" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:83 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:83 msgid "Disable autostart" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:310 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:312 msgid "Disable QUIC" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:311 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:313 msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:460 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:540 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585 msgid "Disabled" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:88 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:88 msgid "DNS on router" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:79 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:79 msgid "DNS outbound section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:337 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:15 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:338 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:16 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:334 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:12 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:379 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12 msgid "DNS Protocol Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:113 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:113 msgid "DNS Rewrite TTL" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:347 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:24 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:392 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24 msgid "DNS Server" msgstr "" -#: src\validators\validateDns.ts:7 +#: src/validators/validateDns.ts:7 msgid "DNS server address cannot be empty" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:26 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26 msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369 msgid "Domain Resolver" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:371 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:377 msgid "Dont Touch My DHCP!" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:241 -#: src\netshift\tabs\dashboard\initController.ts:275 +#: src/netshift/tabs/dashboard/initController.ts:241 +#: src/netshift/tabs/dashboard/initController.ts:275 msgid "Downlink" msgstr "" -#: src\partials\modal\renderModal.ts:15 +#: src/partials/modal/renderModal.ts:15 msgid "Download" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:333 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:335 msgid "Download Lists via Proxy/VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:342 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:344 msgid "Download Lists via specific proxy section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:334 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:343 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:336 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:345 msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:147 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:461 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:541 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586 msgid "Dynamic List" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:93 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:93 msgid "Enable autostart" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:325 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:764 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:809 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:735 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:476 +msgid "Enable IPv6 Support" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:780 msgid "Enable Mixed Proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:171 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:171 msgid "Enable Output Network Interface" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:736 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:781 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:282 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:280 msgid "Enable YACD" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:291 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:289 msgid "Enable YACD WAN Access" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:67 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:69 msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:496 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:541 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:470 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:550 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:595 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:90 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92 msgid "Enter the subscription URL to fetch proxy configurations from your provider" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:205 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219 msgid "Every 1 minute" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:119 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123 msgid "Every 12 hours" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:117 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121 msgid "Every 3 hours" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:206 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220 msgid "Every 3 minutes" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:115 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119 msgid "Every 30 minutes" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:204 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:218 msgid "Every 30 seconds" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:207 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:221 msgid "Every 5 minutes" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:118 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122 msgid "Every 6 hours" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:120 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:124 msgid "Every day" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:116 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:120 msgid "Every hour" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:447 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:451 msgid "Exclude NTP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:448 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:452 msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:146 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154 msgid "Exclude servers by keyword" msgstr "" -#: src\helpers\copyToClipboard.ts:12 +#: src/helpers/copyToClipboard.ts:12 msgid "Failed to copy!" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:229 -#: src\netshift\tabs\diagnostic\initController.ts:233 -#: src\netshift\tabs\diagnostic\initController.ts:263 -#: src\netshift\tabs\diagnostic\initController.ts:267 -#: src\netshift\tabs\diagnostic\initController.ts:304 -#: src\netshift\tabs\diagnostic\initController.ts:308 -#: src\netshift\tabs\diagnostic\initController.ts:347 -#: src\netshift\tabs\diagnostic\initController.ts:351 +#: src/netshift/tabs/diagnostic/initController.ts:229 +#: src/netshift/tabs/diagnostic/initController.ts:233 +#: src/netshift/tabs/diagnostic/initController.ts:263 +#: src/netshift/tabs/diagnostic/initController.ts:267 +#: src/netshift/tabs/diagnostic/initController.ts:304 +#: src/netshift/tabs/diagnostic/initController.ts:308 +#: src/netshift/tabs/diagnostic/initController.ts:347 +#: src/netshift/tabs/diagnostic/initController.ts:351 msgid "Failed to execute!" msgstr "" -#: src\netshift\methods\custom\getDashboardSections.ts:150 -#: src\netshift\methods\custom\getDashboardSections.ts:181 -#: src\netshift\methods\custom\getDashboardSections.ts:218 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:58 +#: src/netshift/methods/custom/getDashboardSections.ts:150 +#: src/netshift/methods/custom/getDashboardSections.ts:181 +#: src/netshift/methods/custom/getDashboardSections.ts:218 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:58 msgid "Fastest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:708 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:753 msgid "Fully Routed IPs" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:102 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:102 msgid "Get global check" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:224 +#: src/netshift/tabs/diagnostic/initController.ts:224 msgid "Global check" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:113 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309 +msgid "Global Proxy" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117 msgid "How often to automatically update the subscription" msgstr "" -#: src\netshift\api.ts:27 +#: src/netshift/api.ts:27 msgid "HTTP error" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:137 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:143 msgid "Include servers by keyword" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129 msgid "Install extended" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129 msgid "Install stable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:234 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:232 msgid "Interface Monitoring" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:266 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:264 msgid "Interface Monitoring Delay" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:235 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:233 msgid "Interface monitoring for Bad WAN" msgstr "" -#: src\validators\validateDns.ts:23 -msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" +#: src/validators/validateDns.ts:39 +msgid "Invalid DNS server format. Examples: 8.8.8.8, [::1], dns.example.com, or dns.example.com/dns-query for DoH" msgstr "" -#: src\validators\validateDomain.ts:18 -#: src\validators\validateDomain.ts:27 +#: src/validators/validateDomain.ts:18 +#: src/validators/validateDomain.ts:27 msgid "Invalid domain address" msgstr "" -#: src\validators\validateSubnet.ts:11 -msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" +#: src/validators/validateSubnet.ts:57 +msgid "Invalid format. Use X.X.X.X/Y or IPv6/Y" msgstr "" -#: src\validators\validateHysteriaUrl.ts:90 +#: src/validators/validateHysteriaUrl.ts:90 msgid "Invalid HY2 URL: insecure must be 0 or 1" msgstr "" -#: src\validators\validateHysteriaUrl.ts:77 +#: src/validators/validateHysteriaUrl.ts:77 msgid "Invalid HY2 URL: invalid port number" msgstr "" -#: src\validators\validateHysteriaUrl.ts:30 +#: src/validators/validateHysteriaUrl.ts:30 msgid "Invalid HY2 URL: missing credentials/server" msgstr "" -#: src\validators\validateHysteriaUrl.ts:47 +#: src/validators/validateHysteriaUrl.ts:47 msgid "Invalid HY2 URL: missing host" msgstr "" -#: src\validators\validateHysteriaUrl.ts:41 +#: src/validators/validateHysteriaUrl.ts:41 msgid "Invalid HY2 URL: missing host & port" msgstr "" -#: src\validators\validateHysteriaUrl.ts:36 +#: src/validators/validateHysteriaUrl.ts:36 msgid "Invalid HY2 URL: missing password" msgstr "" -#: src\validators\validateHysteriaUrl.ts:50 +#: src/validators/validateHysteriaUrl.ts:50 msgid "Invalid HY2 URL: missing port" msgstr "" -#: src\validators\validateHysteriaUrl.ts:18 +#: src/validators/validateHysteriaUrl.ts:18 msgid "Invalid HY2 URL: must not contain spaces" msgstr "" -#: src\validators\validateHysteriaUrl.ts:12 +#: src/validators/validateHysteriaUrl.ts:12 msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" msgstr "" -#: src\validators\validateHysteriaUrl.ts:108 +#: src/validators/validateHysteriaUrl.ts:108 msgid "Invalid HY2 URL: obfs-password required when obfs is set" msgstr "" -#: src\validators\validateHysteriaUrl.ts:122 +#: src/validators/validateHysteriaUrl.ts:122 msgid "Invalid HY2 URL: parsing failed" msgstr "" -#: src\validators\validateHysteriaUrl.ts:116 +#: src/validators/validateHysteriaUrl.ts:116 msgid "Invalid HY2 URL: sni cannot be empty" msgstr "" -#: src\validators\validateHysteriaUrl.ts:98 +#: src/validators/validateHysteriaUrl.ts:98 msgid "Invalid HY2 URL: unsupported obfs type" msgstr "" -#: src\validators\validateIp.ts:11 +#: src/validators/validateIp.ts:11 msgid "Invalid IP address" msgstr "" -#: src\validators\validateOutboundJson.ts:9 +#: src/validators/validateIp.ts:28 +msgid "Invalid IPv6 address" +msgstr "" + +#: src/validators/validateOutboundJson.ts:9 msgid "Invalid JSON format" msgstr "" -#: src\validators\validatePath.ts:22 +#: src/validators/validatePath.ts:22 msgid "Invalid path format. Path must start with \"/\" and contain valid characters" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:85 +#: src/validators/validateShadowsocksUrl.ts:85 msgid "Invalid port number. Must be between 1 and 65535" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:37 +#: src/validators/validateShadowsocksUrl.ts:37 msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:27 +#: src/validators/validateShadowsocksUrl.ts:27 msgid "Invalid Shadowsocks URL: missing credentials" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:46 +#: src/validators/validateShadowsocksUrl.ts:46 msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:76 +#: src/validators/validateShadowsocksUrl.ts:76 msgid "Invalid Shadowsocks URL: missing port" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:67 +#: src/validators/validateShadowsocksUrl.ts:67 msgid "Invalid Shadowsocks URL: missing server" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:58 +#: src/validators/validateShadowsocksUrl.ts:58 msgid "Invalid Shadowsocks URL: missing server address" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:16 +#: src/validators/validateShadowsocksUrl.ts:16 msgid "Invalid Shadowsocks URL: must not contain spaces" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:8 +#: src/validators/validateShadowsocksUrl.ts:8 msgid "Invalid Shadowsocks URL: must start with ss://" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:91 +#: src/validators/validateShadowsocksUrl.ts:91 msgid "Invalid Shadowsocks URL: parsing failed" msgstr "" -#: src\validators\validateSocksUrl.ts:73 +#: src/validators/validateSocksUrl.ts:73 msgid "Invalid SOCKS URL: invalid host format" msgstr "" -#: src\validators\validateSocksUrl.ts:63 +#: src/validators/validateSocksUrl.ts:63 msgid "Invalid SOCKS URL: invalid port number" msgstr "" -#: src\validators\validateSocksUrl.ts:42 +#: src/validators/validateSocksUrl.ts:42 msgid "Invalid SOCKS URL: missing host and port" msgstr "" -#: src\validators\validateSocksUrl.ts:51 +#: src/validators/validateSocksUrl.ts:51 msgid "Invalid SOCKS URL: missing hostname or IP" msgstr "" -#: src\validators\validateSocksUrl.ts:56 +#: src/validators/validateSocksUrl.ts:56 msgid "Invalid SOCKS URL: missing port" msgstr "" -#: src\validators\validateSocksUrl.ts:34 +#: src/validators/validateSocksUrl.ts:34 msgid "Invalid SOCKS URL: missing username" msgstr "" -#: src\validators\validateSocksUrl.ts:19 +#: src/validators/validateSocksUrl.ts:19 msgid "Invalid SOCKS URL: must not contain spaces" msgstr "" -#: src\validators\validateSocksUrl.ts:10 +#: src/validators/validateSocksUrl.ts:10 msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" msgstr "" -#: src\validators\validateSocksUrl.ts:77 +#: src/validators/validateSocksUrl.ts:77 msgid "Invalid SOCKS URL: parsing failed" msgstr "" -#: src\validators\validateTrojanUrl.ts:15 +#: src/validators/validateTrojanUrl.ts:15 msgid "Invalid Trojan URL: must not contain spaces" msgstr "" -#: src\validators\validateTrojanUrl.ts:8 +#: src/validators/validateTrojanUrl.ts:8 msgid "Invalid Trojan URL: must start with trojan://" msgstr "" -#: src\validators\validateTrojanUrl.ts:56 +#: src/validators/validateTrojanUrl.ts:56 msgid "Invalid Trojan URL: parsing failed" msgstr "" -#: src\validators\validateUrl.ts:8 -#: src\validators\validateUrl.ts:31 +#: src/validators/validateUrl.ts:8 +#: src/validators/validateUrl.ts:31 msgid "Invalid URL format" msgstr "" -#: src\validators\validateVlessUrl.ts:110 +#: src/validators/validateVlessUrl.ts:110 msgid "Invalid VLESS URL: parsing failed" msgstr "" -#: src\validators\validateVmessUrl.ts:82 +#: src/validators/validateVmessUrl.ts:82 msgid "Invalid VMess URL: invalid port" msgstr "" -#: src\validators\validateVmessUrl.ts:40 +#: src/validators/validateVmessUrl.ts:40 msgid "Invalid VMess URL: malformed base64" msgstr "" -#: src\validators\validateVmessUrl.ts:50 -#: src\validators\validateVmessUrl.ts:57 +#: src/validators/validateVmessUrl.ts:50 +#: src/validators/validateVmessUrl.ts:57 msgid "Invalid VMess URL: malformed JSON" msgstr "" -#: src\validators\validateVmessUrl.ts:66 +#: src/validators/validateVmessUrl.ts:66 msgid "Invalid VMess URL: missing address" msgstr "" -#: src\validators\validateVmessUrl.ts:73 +#: src/validators/validateVmessUrl.ts:73 msgid "Invalid VMess URL: missing id" msgstr "" -#: src\validators\validateVmessUrl.ts:25 +#: src/validators/validateVmessUrl.ts:25 msgid "Invalid VMess URL: must not contain spaces" msgstr "" -#: src\validators\validateVmessUrl.ts:7 +#: src/validators/validateVmessUrl.ts:7 msgid "Invalid VMess URL: must start with vmess://" msgstr "" -#: src\validators\validateSubnet.ts:18 +#: src/validators/validateSubnet.ts:11 msgid "IP address 0.0.0.0 is not allowed" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:20 +#: src/validators/validateSubnet.ts:47 +msgid "IPv6 CIDR must be between 0 and 128" +msgstr "" + +#: src/netshift/tabs/diagnostic/helpers/getMeta.ts:20 msgid "Issues detected" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:138 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:144 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:48 +#: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:48 msgid "Latest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:321 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:323 msgid "List Update Frequency" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:616 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661 msgid "Local Domain Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:639 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:684 msgid "Local Subnet Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:429 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:435 msgid "Log Level" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:72 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:72 msgid "Main DNS" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:81 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:81 msgid "Main DNS via outbound" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:311 +#: src/netshift/tabs/dashboard/initController.ts:311 msgid "Memory Usage" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:748 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793 msgid "Mixed Proxy Port" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:243 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:241 msgid "Monitored Interfaces" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:233 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:255 msgid "Must be a number in the range of 50 - 1000" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:343 +#: src/netshift/tabs/dashboard/initController.ts:343 msgid "NetShift" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:26 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:26 msgid "NetShift Settings" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:372 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:378 msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:278 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323 msgid "Network Interface" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:105 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:105 msgid "No other marking rules found" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderCheckSection.ts:189 +#: src/netshift/tabs/diagnostic/partials/renderCheckSection.ts:189 msgid "Not implement yet" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:74 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:80 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:99 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:74 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:80 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:99 msgid "Not responding" msgstr "" -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:59 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:67 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:75 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:83 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:91 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:59 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:67 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:75 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:83 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:91 msgid "Not running" msgstr "" -#: src\helpers\withTimeout.ts:7 +#: src/helpers/withTimeout.ts:7 msgid "Operation timed out" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:30 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:30 msgid "Outbound Config" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:66 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:68 msgid "Outbound Configuration" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:38 +#: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:38 msgid "Outdated" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:180 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:180 msgid "Output Network Interface" msgstr "" -#: src\validators\validatePath.ts:7 +#: src/validators/validatePath.ts:7 msgid "Path cannot be empty" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:411 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:417 msgid "Path must be absolute (start with /)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:420 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:426 msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:415 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:421 msgid "Path must end with cache.db" msgstr "" -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:107 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:115 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:123 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:131 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:139 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:107 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:115 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:123 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:131 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:139 msgid "Pending" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:37 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:37 msgid "Proxy Configuration URL" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:66 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:66 msgid "Proxy traffic is not routed via FakeIP" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:65 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:65 msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:403 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:448 msgid "Regional options cannot be used together" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:662 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:707 msgid "Remote Domain Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:685 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:730 msgid "Remote Subnet Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:763 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808 msgid "Resolve real IP for routing" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:53 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:53 msgid "Restart NetShift" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:68 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:68 msgid "Route main DNS through proxy/VPN" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:51 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:51 msgid "Router DNS is not routed through sing-box" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:50 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:50 msgid "Router DNS is routed through sing-box" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:458 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:488 msgid "Routing Excluded IPs" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:79 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:79 msgid "Rules mangle counters" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:74 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:74 msgid "Rules mangle exist" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:89 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:89 msgid "Rules mangle output counters" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:84 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:84 msgid "Rules mangle output exist" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:99 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 msgid "Rules proxy counters" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:94 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:94 msgid "Rules proxy exist" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderRunAction.ts:15 +#: src/netshift/tabs/diagnostic/partials/renderRunAction.ts:15 msgid "Run Diagnostic" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:422 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467 msgid "Russia inside restrictions" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:302 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:302 msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:36 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:36 msgid "Sections" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:370 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:415 msgid "Select a predefined list for routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:13 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:13 msgid "Select between VPN and Proxy connection methods for traffic routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:13 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:13 msgid "Select DNS protocol to use" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:322 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:324 msgid "Select how often the domain or subnet lists are updated automatically" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:24 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:24 msgid "Select how to configure the proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:279 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 msgid "Select network interface for VPN connection" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:348 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:25 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:393 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25 msgid "Select or enter DNS server address" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:394 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:400 msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:381 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:387 msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:335 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:380 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:458 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:503 msgid "Select the list type for adding custom domains" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:538 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:583 msgid "Select the list type for adding custom subnets" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:430 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:436 msgid "Select the log level for sing-box" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:135 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:135 msgid "Select the network interface from which the traffic will originate" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:181 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:181 msgid "Select the network interface to which the traffic will originate" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:244 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:242 msgid "Select the WAN interfaces to be monitored" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:27 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:27 msgid "Selector" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:155 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165 msgid "Selector Proxy Links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:69 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:69 msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:340 +#: src/netshift/tabs/dashboard/initController.ts:340 msgid "Services info" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:49 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:49 msgid "Settings" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:292 -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:120 +#: src/netshift/tabs/diagnostic/initController.ts:292 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:120 msgid "Show sing-box config" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:354 +#: src/netshift/tabs/dashboard/initController.ts:354 msgid "Sing-box" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:77 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:77 msgid "Sing-box autostart disabled" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:342 +#: src/netshift/tabs/diagnostic/initController.ts:342 msgid "Sing-box core changed, version:" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:62 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:62 msgid "Sing-box installed" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:87 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:87 msgid "Sing-box listening ports" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:82 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:82 msgid "Sing-box process running" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:72 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:72 msgid "Sing-box service exist" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:67 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:67 msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:134 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:134 msgid "Source Network Interface" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:459 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:489 msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:709 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:754 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:663 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:708 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:686 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:731 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:617 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:640 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:685 msgid "Specify the path to the list file located on the router filesystem" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:73 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:73 msgid "Start NetShift" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:63 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:63 msgid "Stop NetShift" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:29 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:29 msgid "Subscription" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:112 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116 msgid "Subscription Update Interval" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:89 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:91 msgid "Subscription URL" msgstr "" -#: src\helpers\copyToClipboard.ts:10 +#: src/helpers/copyToClipboard.ts:10 msgid "Successfully copied!" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:331 +#: src/netshift/tabs/diagnostic/initController.ts:331 msgid "Switching sing-box core, this may take a few minutes…" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:304 +#: src/netshift/tabs/dashboard/initController.ts:304 msgid "System info" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderSystemInfo.ts:21 +#: src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts:21 msgid "System information" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:69 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:69 msgid "Table exist" msgstr "" -#: src\netshift\tabs\dashboard\partials\renderSections.ts:108 +#: src/netshift/tabs/dashboard/partials/renderSections.ts:108 msgid "Test latency" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:462 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:542 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:507 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 msgid "Text List" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:46 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:46 msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:202 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216 msgid "The interval between connectivity tests" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:216 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:240 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:262 msgid "The URL used to test server connectivity" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:114 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:114 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:238 +#: src/netshift/tabs/dashboard/initController.ts:238 msgid "Traffic" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:268 +#: src/netshift/tabs/dashboard/initController.ts:268 msgid "Traffic Total" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:25 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:25 msgid "Troubleshooting" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:125 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:125 msgid "TTL must be a positive number" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:120 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:120 msgid "TTL value cannot be empty" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:339 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:17 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:268 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299 msgid "UDP over TCP" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:39 -#: src\netshift\tabs\diagnostic\initController.ts:40 -#: src\netshift\tabs\diagnostic\initController.ts:41 -#: src\netshift\tabs\diagnostic\initController.ts:42 -#: src\netshift\tabs\diagnostic\initController.ts:43 -#: src\netshift\tabs\diagnostic\initController.ts:44 -#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:7 +#: src/netshift/tabs/diagnostic/initController.ts:39 +#: src/netshift/tabs/diagnostic/initController.ts:40 +#: src/netshift/tabs/diagnostic/initController.ts:41 +#: src/netshift/tabs/diagnostic/initController.ts:42 +#: src/netshift/tabs/diagnostic/initController.ts:43 +#: src/netshift/tabs/diagnostic/initController.ts:44 +#: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7 msgid "unknown" msgstr "" -#: src\netshift\api.ts:40 +#: src/netshift/api.ts:40 msgid "Unknown error" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:240 -#: src\netshift\tabs\dashboard\initController.ts:271 +#: src/netshift/tabs/dashboard/initController.ts:240 +#: src/netshift/tabs/dashboard/initController.ts:271 msgid "Uplink" msgstr "" -#: src\validators\validateProxyUrl.ts:42 +#: src/validators/validateProxyUrl.ts:42 msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" -#: src\validators\validateUrl.ts:17 +#: src/validators/validateUrl.ts:17 msgid "URL must use one of the following protocols:" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:28 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:28 msgid "URLTest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:201 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:215 msgid "URLTest Check Interval" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:178 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:190 msgid "URLTest Proxy Links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:239 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:261 msgid "URLTest Testing URL" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:215 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229 msgid "URLTest Tolerance" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:457 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:502 msgid "User Domain List Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:469 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514 msgid "User Domains" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:495 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540 msgid "User Domains List" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:537 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:582 msgid "User Subnet List Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:549 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594 msgid "User Subnets" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:575 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:620 msgid "User Subnets List" msgstr "" -#: src\validators\validateDns.ts:14 -#: src\validators\validateDns.ts:18 -#: src\validators\validateDomain.ts:13 -#: src\validators\validateDomain.ts:30 -#: src\validators\validateHysteriaUrl.ts:120 -#: src\validators\validateIp.ts:8 -#: src\validators\validateOutboundJson.ts:7 -#: src\validators\validatePath.ts:16 -#: src\validators\validateShadowsocksUrl.ts:95 -#: src\validators\validateSocksUrl.ts:80 -#: src\validators\validateSubnet.ts:38 -#: src\validators\validateTrojanUrl.ts:59 -#: src\validators\validateUrl.ts:28 -#: src\validators\validateVlessUrl.ts:108 -#: src\validators\validateVmessUrl.ts:86 +#: src/validators/validateDns.ts:26 +#: src/validators/validateDns.ts:30 +#: src/validators/validateDns.ts:34 +#: src/validators/validateDomain.ts:13 +#: src/validators/validateDomain.ts:30 +#: src/validators/validateHysteriaUrl.ts:120 +#: src/validators/validateIp.ts:8 +#: src/validators/validateIp.ts:24 +#: src/validators/validateOutboundJson.ts:7 +#: src/validators/validatePath.ts:16 +#: src/validators/validateShadowsocksUrl.ts:95 +#: src/validators/validateSocksUrl.ts:80 +#: src/validators/validateSubnet.ts:30 +#: src/validators/validateSubnet.ts:52 +#: src/validators/validateTrojanUrl.ts:59 +#: src/validators/validateUrl.ts:28 +#: src/validators/validateVlessUrl.ts:108 +#: src/validators/validateVmessUrl.ts:86 msgid "Valid" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:528 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:607 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:652 msgid "Validation errors:" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:258 -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:111 +#: src/netshift/tabs/diagnostic/initController.ts:258 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:111 msgid "View logs" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:31 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:31 msgid "Visit Wiki" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:38 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:156 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:179 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:38 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:191 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:405 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:450 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:424 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:469 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:80 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80 msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:301 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:301 msgid "YACD Secret Key" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:172 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:172 msgid "You can select Output Network Interface, by default autodetect" msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131 +msgid "Группировать по странам" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:132 +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index e43b8111..68d0c4eb 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -1,15 +1,15 @@ # RU translations for NETSHIFT package. # Copyright (C) 2026 THE NETSHIFT'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# yandexru45, 2026. +# spgsroot, yandexru45, 2026. # msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 21:43+0300\n" -"PO-Revision-Date: 2026-06-05 21:43+0300\n" -"Last-Translator: yandexru45\n" +"POT-Creation-Date: 2026-06-06 15:00+0800\n" +"PO-Revision-Date: 2026-06-06 15:00+0800\n" +"Last-Translator: spgsroot, yandexru45\n" "Language-Team: none\n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -29,12 +29,6 @@ msgstr "✘ Отключено" msgid "✘ Stopped" msgstr "✘ Остановлен" -msgid "Группировать по странам" -msgstr "" - -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" -msgstr "" - msgid "Active Connections" msgstr "Активные соединения" @@ -56,6 +50,9 @@ msgstr "Необходимо указать хотя бы одну действ msgid "Available actions" msgstr "Доступные действия" +msgid "Block DoH Servers" +msgstr "Блокировать DoH-серверы" + msgid "Bootsrap DNS" msgstr "Bootstrap DNS" @@ -102,7 +99,7 @@ msgid "Config File Path" msgstr "Путь к файлу конфигурации" msgid "Configuration for NetShift service" -msgstr "" +msgstr "Конфигурация службы NetShift" msgid "Configuration Type" msgstr "Тип конфигурации" @@ -132,7 +129,7 @@ msgid "Dashboard currently unavailable" msgstr "Дашборд сейчас недоступен" msgid "Delay in milliseconds before reloading NetShift after interface UP" -msgstr "" +msgstr "Задержка в миллисекундах перед перезагрузкой NetShift после поднятия интерфейса" msgid "Delay value cannot be empty" msgstr "Значение задержки не может быть пустым" @@ -218,6 +215,9 @@ msgstr "Включить встроенный DNS-резолвер для дом msgid "Enable DNS resolve to get real IP when routing" msgstr "Разрешать домены в реальные IP-адреса перед маршрутизацией в outbound" +msgid "Enable IPv6 Support" +msgstr "Включить поддержку IPv6" + msgid "Enable Mixed Proxy" msgstr "Включить смешанный прокси" @@ -246,22 +246,22 @@ msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addres msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса" msgid "Enter the subscription URL to fetch proxy configurations from your provider" -msgstr "" +msgstr "Введите URL подписки для получения конфигураций прокси от вашего провайдера" msgid "Every 1 minute" msgstr "Каждую минуту" msgid "Every 12 hours" -msgstr "" +msgstr "Каждые 12 часов" msgid "Every 3 hours" -msgstr "" +msgstr "Каждые 3 часа" msgid "Every 3 minutes" msgstr "Каждые 3 минуты" msgid "Every 30 minutes" -msgstr "" +msgstr "Каждые 30 минут" msgid "Every 30 seconds" msgstr "Каждые 30 секунд" @@ -270,13 +270,13 @@ msgid "Every 5 minutes" msgstr "Каждые 5 минут" msgid "Every 6 hours" -msgstr "" +msgstr "Каждые 6 часов" msgid "Every day" -msgstr "" +msgstr "Каждый день" msgid "Every hour" -msgstr "" +msgstr "Каждый час" msgid "Exclude NTP" msgstr "Исключить NTP" @@ -305,8 +305,11 @@ msgstr "Получить глобальную проверку" msgid "Global check" msgstr "Глобальная проверка" +msgid "Global Proxy" +msgstr "Глобальный прокси" + msgid "How often to automatically update the subscription" -msgstr "" +msgstr "Как часто автоматически обновлять подписку" msgid "HTTP error" msgstr "Ошибка HTTP" @@ -329,14 +332,14 @@ msgstr "Задержка при мониторинге интерфейсов" msgid "Interface monitoring for Bad WAN" msgstr "Мониторинг интерфейса для Bad WAN" -msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" -msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH" +msgid "Invalid DNS server format. Examples: 8.8.8.8, [::1], dns.example.com, or dns.example.com/dns-query for DoH" +msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, [::1], dns.example.com или dns.example.com/dns-query для DoH" msgid "Invalid domain address" msgstr "Неверный домен" -msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" -msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y" +msgid "Invalid format. Use X.X.X.X/Y or IPv6/Y" +msgstr "Неверный формат. Используйте X.X.X.X/Y или IPv6/Y" msgid "Invalid HY2 URL: insecure must be 0 or 1" msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1" @@ -380,6 +383,9 @@ msgstr "Неверный URL Hysteria2: неподдерживаемый тип msgid "Invalid IP address" msgstr "Неверный IP-адрес" +msgid "Invalid IPv6 address" +msgstr "Неверный IPv6-адрес" + msgid "Invalid JSON format" msgstr "Неверный формат JSON" @@ -482,6 +488,9 @@ msgstr "Неверный URL VMess: должен начинаться с vmess:/ msgid "IP address 0.0.0.0 is not allowed" msgstr "IP-адрес 0.0.0.0 не допускается" +msgid "IPv6 CIDR must be between 0 and 128" +msgstr "IPv6 CIDR должен быть от 0 до 128" + msgid "Issues detected" msgstr "Обнаружены проблемы" @@ -522,13 +531,13 @@ msgid "Must be a number in the range of 50 - 1000" msgstr "Должно быть числом от 50 до 1000" msgid "NetShift" -msgstr "" +msgstr "NetShift" msgid "NetShift Settings" -msgstr "" +msgstr "Настройки NetShift" msgid "NetShift will not modify your DHCP configuration" -msgstr "" +msgstr "NetShift не будет изменять вашу конфигурацию DHCP" msgid "Network Interface" msgstr "Сетевой интерфейс" @@ -597,7 +606,7 @@ msgid "Resolve real IP for routing" msgstr "Разрешение реальных IP-адресов" msgid "Restart NetShift" -msgstr "" +msgstr "Перезапустить NetShift" msgid "Route main DNS through proxy/VPN" msgstr "Основной DNS через прокси/VPN" @@ -750,19 +759,19 @@ msgid "Specify the path to the list file located on the router filesystem" msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора." msgid "Start NetShift" -msgstr "" +msgstr "Запустить NetShift" msgid "Stop NetShift" -msgstr "" +msgstr "Остановить NetShift" msgid "Subscription" -msgstr "" +msgstr "Подписка" msgid "Subscription Update Interval" -msgstr "" +msgstr "Интервал обновления подписки" msgid "Subscription URL" -msgstr "" +msgstr "URL подписки" msgid "Successfully copied!" msgstr "Успешно скопировано!" @@ -898,3 +907,9 @@ msgstr "Секретный ключ YACD" msgid "You can select Output Network Interface, by default autodetect" msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически." + +msgid "Группировать по странам" +msgstr "Группировать по странам" + +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" diff --git a/fe-app-netshift/src/constants.ts b/fe-app-netshift/src/constants.ts index 60596254..fb889410 100644 --- a/fe-app-netshift/src/constants.ts +++ b/fe-app-netshift/src/constants.ts @@ -83,6 +83,9 @@ export const DNS_SERVER_OPTIONS = { 'unfiltered.adguard-dns.com': 'unfiltered.adguard-dns.com (AdGuard Unfiltered)', 'family.adguard-dns.com': 'family.adguard-dns.com (AdGuard Family)', + '2001:4860:4860::8888': '2001:4860:4860::8888 (Google IPv6)', + '2606:4700:4700::1111': '2606:4700:4700::1111 (Cloudflare IPv6)', + '2620:fe::fe': '2620:fe::fe (Quad9 IPv6)', }; export const BOOTSTRAP_DNS_SERVER_OPTIONS = { '77.88.8.8': '77.88.8.8 (Yandex DNS)', @@ -93,6 +96,8 @@ export const BOOTSTRAP_DNS_SERVER_OPTIONS = { '8.8.4.4': '8.8.4.4 (Google DNS)', '9.9.9.9': '9.9.9.9 (Quad9 DNS)', '9.9.9.11': '9.9.9.11 (Quad9 DNS)', + '2001:4860:4860::8888': '2001:4860:4860::8888 (Google DNS IPv6)', + '2606:4700:4700::1111': '2606:4700:4700::1111 (Cloudflare DNS IPv6)', }; export const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds diff --git a/fe-app-netshift/src/validators/tests/validateDns.test.js b/fe-app-netshift/src/validators/tests/validateDns.test.js index bd105b1c..c2bac53a 100644 --- a/fe-app-netshift/src/validators/tests/validateDns.test.js +++ b/fe-app-netshift/src/validators/tests/validateDns.test.js @@ -12,11 +12,21 @@ export const additionalValidDns = [ ['DoH IP with port 443', '1.1.1.1:443/dns-query'], ['DoH domain', 'cloudflare-dns.com/dns-query'], ['DoH domain with port 443', 'cloudflare-dns.com:443/dns-query'], + ['IPv6 address', '2001:db8::1'], + ['Bracketed IPv6', '[2001:db8::1]'], + ['Bracketed IPv6 with port', '[2001:db8::1]:853'], + ['IPv6 DoH path', '2001:db8::1/dns-query'], + ['Bracketed IPv6 DoH path with port', '[2001:db8::1]:443/dns-query'], +]; + +export const additionalInvalidDns = [ + ['IPv6 invalid hex', '2001:db8::zzzz'], + ['IPv6 group too long', '12345::1'], ]; const validDns = [...validIPs, ...validDomains, ...additionalValidDns]; -const invalidDns = [...invalidIPs, ...invalidDomains]; +const invalidDns = [...invalidIPs, ...invalidDomains, ...additionalInvalidDns]; describe('validateDns', () => { describe.each(validDns)('Valid dns: %s', (_desc, domain) => { diff --git a/fe-app-netshift/src/validators/tests/validateIp.test.js b/fe-app-netshift/src/validators/tests/validateIp.test.js index fe86c1ce..9fd63fe2 100644 --- a/fe-app-netshift/src/validators/tests/validateIp.test.js +++ b/fe-app-netshift/src/validators/tests/validateIp.test.js @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { validateIPV4 } from '../validateIp'; +import { validateIP, validateIPV4, validateIPV6 } from '../validateIp'; export const validIPs = [ ['Private LAN', '192.168.1.1'], @@ -21,6 +21,19 @@ export const invalidIPs = [ ['Trailing dot', '1.2.3.'], ]; +export const validIPv6 = [ + ['Loopback', '::1'], + ['Compressed', '2001:db8::1'], + ['Full form', '2001:0db8:85a3:0000:0000:8a2e:0370:7334'], + ['Bracketed', '[2001:db8::1]'], +]; + +export const invalidIPv6 = [ + ['Invalid hex', '2001:db8::zzzz'], + ['Group too long', '12345::1'], + ['Too many groups', '2001:db8:85a3:0:0:8a2e:370:7334:1234'], +]; + describe('validateIPV4', () => { describe.each(validIPs)('Valid IP: %s', (_desc, ip) => { it(`returns {valid:true} for "${ip}"`, () => { @@ -36,3 +49,41 @@ describe('validateIPV4', () => { }); }); }); + +describe('validateIPV6', () => { + describe.each(validIPv6)('Valid IPv6: %s', (_desc, ip) => { + it(`returns {valid:true} for "${ip}"`, () => { + const res = validateIPV6(ip); + expect(res.valid).toBe(true); + }); + }); + + describe.each([...invalidIPv6, ['IPv4 address', '192.168.1.1']])( + 'Invalid IPv6: %s', + (_desc, ip) => { + it(`returns {valid:false} for "${ip}"`, () => { + const res = validateIPV6(ip); + expect(res.valid).toBe(false); + }); + }, + ); +}); + +describe('validateIP', () => { + describe.each([...validIPs, ...validIPv6])('Valid IP: %s', (_desc, ip) => { + it(`returns {valid:true} for "${ip}"`, () => { + const res = validateIP(ip); + expect(res.valid).toBe(true); + }); + }); + + describe.each([...invalidIPs, ...invalidIPv6])( + 'Invalid IP: %s', + (_desc, ip) => { + it(`returns {valid:false} for "${ip}"`, () => { + const res = validateIP(ip); + expect(res.valid).toBe(false); + }); + }, + ); +}); diff --git a/fe-app-netshift/src/validators/tests/validateSubnet.test.js b/fe-app-netshift/src/validators/tests/validateSubnet.test.js index 13621b74..9415e9b7 100644 --- a/fe-app-netshift/src/validators/tests/validateSubnet.test.js +++ b/fe-app-netshift/src/validators/tests/validateSubnet.test.js @@ -8,6 +8,10 @@ export const validSubnets = [ ['CIDR /32', '172.16.0.1/32'], ['Loopback', '127.0.0.1'], ['Broadcast with mask', '255.255.255.255/32'], + ['IPv6 loopback', '::1'], + ['IPv6 with CIDR /0', '::/0'], + ['IPv6 with CIDR /32', '2001:db8::/32'], + ['IPv6 with CIDR /128', '2001:db8::1/128'], ]; export const invalidSubnets = [ @@ -22,6 +26,10 @@ export const invalidSubnets = [ ['Invalid CIDR (negative)', '192.168.1.1/-1'], ['CIDR not number', '192.168.1.1/abc'], ['Forbidden 0.0.0.0', '0.0.0.0'], + ['IPv6 invalid hex', '2001:db8::zzzz'], + ['IPv6 CIDR too high', '2001:db8::1/129'], + ['IPv6 CIDR negative', '2001:db8::1/-1'], + ['IPv6 CIDR not number', '2001:db8::1/abc'], ]; describe('validateSubnet', () => { diff --git a/fe-app-netshift/src/validators/validateDns.ts b/fe-app-netshift/src/validators/validateDns.ts index 1952a739..55e01110 100644 --- a/fe-app-netshift/src/validators/validateDns.ts +++ b/fe-app-netshift/src/validators/validateDns.ts @@ -1,5 +1,5 @@ import { validateDomain } from './validateDomain'; -import { validateIPV4 } from './validateIp'; +import { validateIPV4, validateIPV6 } from './validateIp'; import { ValidationResult } from './types'; export function validateDNS(value: string): ValidationResult { @@ -7,13 +7,29 @@ export function validateDNS(value: string): ValidationResult { return { valid: false, message: _('DNS server address cannot be empty') }; } - const cleanedValueWithoutPort = value.replace(/:(\d+)(?=\/|$)/, ''); - const cleanedIpWithoutPath = cleanedValueWithoutPort.split('/')[0]; + const valueBeforePath = value.split('/')[0]; + let cleanedValueWithoutPort = value; + let cleanedIpWithoutPath = valueBeforePath; + + if (valueBeforePath.startsWith('[')) { + const closingBracketIndex = valueBeforePath.indexOf(']'); + + if (closingBracketIndex > 0) { + cleanedIpWithoutPath = valueBeforePath.slice(1, closingBracketIndex); + } + } else if ((valueBeforePath.match(/:/g) || []).length < 2) { + cleanedValueWithoutPort = value.replace(/:(\d+)(?=\/|$)/, ''); + cleanedIpWithoutPath = cleanedValueWithoutPort.split('/')[0]; + } if (validateIPV4(cleanedIpWithoutPath).valid) { return { valid: true, message: _('Valid') }; } + if (validateIPV6(cleanedIpWithoutPath).valid) { + return { valid: true, message: _('Valid') }; + } + if (validateDomain(cleanedValueWithoutPort).valid) { return { valid: true, message: _('Valid') }; } @@ -21,7 +37,7 @@ export function validateDNS(value: string): ValidationResult { return { valid: false, message: _( - 'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH', + 'Invalid DNS server format. Examples: 8.8.8.8, [::1], dns.example.com, or dns.example.com/dns-query for DoH', ), }; } diff --git a/fe-app-netshift/src/validators/validateIp.ts b/fe-app-netshift/src/validators/validateIp.ts index 78c154d2..fab5b44b 100644 --- a/fe-app-netshift/src/validators/validateIp.ts +++ b/fe-app-netshift/src/validators/validateIp.ts @@ -10,3 +10,30 @@ export function validateIPV4(ip: string): ValidationResult { return { valid: false, message: _('Invalid IP address') }; } + +export function validateIPV6(ip: string): ValidationResult { + const stripped = ip.replace(/^\[/, '').replace(/\]$/, ''); + const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; + const ipv6CompressedRegex = + /^([0-9a-fA-F]{0,4}:)*:([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/; + + if (ipv6Regex.test(stripped) || ipv6CompressedRegex.test(stripped)) { + const colons = (stripped.match(/:/g) || []).length; + + if (colons >= 2 && colons <= 7) { + return { valid: true, message: _('Valid') }; + } + } + + return { valid: false, message: _('Invalid IPv6 address') }; +} + +export function validateIP(ip: string): ValidationResult { + const ipv4 = validateIPV4(ip); + + if (ipv4.valid) { + return ipv4; + } + + return validateIPV6(ip); +} diff --git a/fe-app-netshift/src/validators/validateSubnet.ts b/fe-app-netshift/src/validators/validateSubnet.ts index e7974a21..06032ee2 100644 --- a/fe-app-netshift/src/validators/validateSubnet.ts +++ b/fe-app-netshift/src/validators/validateSubnet.ts @@ -1,39 +1,59 @@ import { ValidationResult } from './types'; -import { validateIPV4 } from './validateIp'; +import { validateIPV4, validateIPV6 } from './validateIp'; export function validateSubnet(value: string): ValidationResult { - // Must be in form X.X.X.X or X.X.X.X/Y const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$/; - if (!subnetRegex.test(value)) { - return { - valid: false, - message: _('Invalid format. Use X.X.X.X or X.X.X.X/Y'), - }; - } + if (subnetRegex.test(value)) { + const [ip, cidr] = value.split('/'); - const [ip, cidr] = value.split('/'); + if (ip === '0.0.0.0') { + return { valid: false, message: _('IP address 0.0.0.0 is not allowed') }; + } - if (ip === '0.0.0.0') { - return { valid: false, message: _('IP address 0.0.0.0 is not allowed') }; - } + const ipCheck = validateIPV4(ip); + if (!ipCheck.valid) { + return ipCheck; + } + + if (cidr) { + const cidrNum = parseInt(cidr, 10); - const ipCheck = validateIPV4(ip); - if (!ipCheck.valid) { - return ipCheck; + if (cidrNum < 0 || cidrNum > 32) { + return { + valid: false, + message: _('CIDR must be between 0 and 32'), + }; + } + } + + return { valid: true, message: _('Valid') }; } - // Validate CIDR if present - if (cidr) { - const cidrNum = parseInt(cidr, 10); + const ipv6CidrRegex = /^([0-9a-fA-F:]+(?:\/[0-9]{1,3})?)$/; + if (ipv6CidrRegex.test(value)) { + const [ip, cidr] = value.split('/'); + const ipCheck = validateIPV6(ip); - if (cidrNum < 0 || cidrNum > 32) { - return { - valid: false, - message: _('CIDR must be between 0 and 32'), - }; + if (!ipCheck.valid) { + return ipCheck; } + + if (cidr) { + const cidrNum = parseInt(cidr, 10); + if (cidrNum < 0 || cidrNum > 128) { + return { + valid: false, + message: _('IPv6 CIDR must be between 0 and 128'), + }; + } + } + + return { valid: true, message: _('Valid') }; } - return { valid: true, message: _('Valid') }; + return { + valid: false, + message: _('Invalid format. Use X.X.X.X/Y or IPv6/Y'), + }; } diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 89f54c55..9a6e32e4 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -13,6 +13,25 @@ function validateIPV4(ip) { } return { valid: false, message: _("Invalid IP address") }; } +function validateIPV6(ip) { + const stripped = ip.replace(/^\[/, "").replace(/\]$/, ""); + const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; + const ipv6CompressedRegex = /^([0-9a-fA-F]{0,4}:)*:([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/; + if (ipv6Regex.test(stripped) || ipv6CompressedRegex.test(stripped)) { + const colons = (stripped.match(/:/g) || []).length; + if (colons >= 2 && colons <= 7) { + return { valid: true, message: _("Valid") }; + } + } + return { valid: false, message: _("Invalid IPv6 address") }; +} +function validateIP(ip) { + const ipv4 = validateIPV4(ip); + if (ipv4.valid) { + return ipv4; + } + return validateIPV6(ip); +} // src/validators/validateDomain.ts function validateDomain(domain, allowDotTLD = false) { @@ -40,18 +59,31 @@ function validateDNS(value) { if (!value) { return { valid: false, message: _("DNS server address cannot be empty") }; } - const cleanedValueWithoutPort = value.replace(/:(\d+)(?=\/|$)/, ""); - const cleanedIpWithoutPath = cleanedValueWithoutPort.split("/")[0]; + const valueBeforePath = value.split("/")[0]; + let cleanedValueWithoutPort = value; + let cleanedIpWithoutPath = valueBeforePath; + if (valueBeforePath.startsWith("[")) { + const closingBracketIndex = valueBeforePath.indexOf("]"); + if (closingBracketIndex > 0) { + cleanedIpWithoutPath = valueBeforePath.slice(1, closingBracketIndex); + } + } else if ((valueBeforePath.match(/:/g) || []).length < 2) { + cleanedValueWithoutPort = value.replace(/:(\d+)(?=\/|$)/, ""); + cleanedIpWithoutPath = cleanedValueWithoutPort.split("/")[0]; + } if (validateIPV4(cleanedIpWithoutPath).valid) { return { valid: true, message: _("Valid") }; } + if (validateIPV6(cleanedIpWithoutPath).valid) { + return { valid: true, message: _("Valid") }; + } if (validateDomain(cleanedValueWithoutPort).valid) { return { valid: true, message: _("Valid") }; } return { valid: false, message: _( - "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" + "Invalid DNS server format. Examples: 8.8.8.8, [::1], dns.example.com, or dns.example.com/dns-query for DoH" ) }; } @@ -102,30 +134,48 @@ function validatePath(value) { // src/validators/validateSubnet.ts function validateSubnet(value) { const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$/; - if (!subnetRegex.test(value)) { - return { - valid: false, - message: _("Invalid format. Use X.X.X.X or X.X.X.X/Y") - }; - } - const [ip, cidr] = value.split("/"); - if (ip === "0.0.0.0") { - return { valid: false, message: _("IP address 0.0.0.0 is not allowed") }; - } - const ipCheck = validateIPV4(ip); - if (!ipCheck.valid) { - return ipCheck; + if (subnetRegex.test(value)) { + const [ip, cidr] = value.split("/"); + if (ip === "0.0.0.0") { + return { valid: false, message: _("IP address 0.0.0.0 is not allowed") }; + } + const ipCheck = validateIPV4(ip); + if (!ipCheck.valid) { + return ipCheck; + } + if (cidr) { + const cidrNum = parseInt(cidr, 10); + if (cidrNum < 0 || cidrNum > 32) { + return { + valid: false, + message: _("CIDR must be between 0 and 32") + }; + } + } + return { valid: true, message: _("Valid") }; } - if (cidr) { - const cidrNum = parseInt(cidr, 10); - if (cidrNum < 0 || cidrNum > 32) { - return { - valid: false, - message: _("CIDR must be between 0 and 32") - }; + const ipv6CidrRegex = /^([0-9a-fA-F:]+(?:\/[0-9]{1,3})?)$/; + if (ipv6CidrRegex.test(value)) { + const [ip, cidr] = value.split("/"); + const ipCheck = validateIPV6(ip); + if (!ipCheck.valid) { + return ipCheck; + } + if (cidr) { + const cidrNum = parseInt(cidr, 10); + if (cidrNum < 0 || cidrNum > 128) { + return { + valid: false, + message: _("IPv6 CIDR must be between 0 and 128") + }; + } } + return { valid: true, message: _("Valid") }; } - return { valid: true, message: _("Valid") }; + return { + valid: false, + message: _("Invalid format. Use X.X.X.X/Y or IPv6/Y") + }; } // src/validators/bulkValidate.ts @@ -1173,7 +1223,10 @@ var DNS_SERVER_OPTIONS = { "9.9.9.9": "9.9.9.9 (Quad9)", "dns.adguard-dns.com": "dns.adguard-dns.com (AdGuard Default)", "unfiltered.adguard-dns.com": "unfiltered.adguard-dns.com (AdGuard Unfiltered)", - "family.adguard-dns.com": "family.adguard-dns.com (AdGuard Family)" + "family.adguard-dns.com": "family.adguard-dns.com (AdGuard Family)", + "2001:4860:4860::8888": "2001:4860:4860::8888 (Google IPv6)", + "2606:4700:4700::1111": "2606:4700:4700::1111 (Cloudflare IPv6)", + "2620:fe::fe": "2620:fe::fe (Quad9 IPv6)" }; var BOOTSTRAP_DNS_SERVER_OPTIONS = { "77.88.8.8": "77.88.8.8 (Yandex DNS)", @@ -1183,7 +1236,9 @@ var BOOTSTRAP_DNS_SERVER_OPTIONS = { "8.8.8.8": "8.8.8.8 (Google DNS)", "8.8.4.4": "8.8.4.4 (Google DNS)", "9.9.9.9": "9.9.9.9 (Quad9 DNS)", - "9.9.9.11": "9.9.9.11 (Quad9 DNS)" + "9.9.9.11": "9.9.9.11 (Quad9 DNS)", + "2001:4860:4860::8888": "2001:4860:4860::8888 (Google DNS IPv6)", + "2606:4700:4700::1111": "2606:4700:4700::1111 (Cloudflare DNS IPv6)" }; var DIAGNOSTICS_UPDATE_INTERVAL = 1e4; var CACHE_TIMEOUT = DIAGNOSTICS_UPDATE_INTERVAL - 1e3; @@ -5244,7 +5299,9 @@ return baseclass.extend({ svgEl, validateDNS, validateDomain, + validateIP, validateIPV4, + validateIPV6, validateOutboundJson, validatePath, validateProxyUrl, diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index c517e0de..7076070b 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -35,7 +35,9 @@ function createSectionContent(section) { form.TextValue, "proxy_string", _("Proxy Configuration URL"), - _("vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") + _( + "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", + ), ); o.depends({ connection_type: "proxy", proxy_config_type: "url" }); o.rows = 5; @@ -87,7 +89,9 @@ function createSectionContent(section) { form.Value, "subscription_url", _("Subscription URL"), - _("Enter the subscription URL to fetch proxy configurations from your provider"), + _( + "Enter the subscription URL to fetch proxy configurations from your provider", + ), ); o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o.placeholder = "https://example.com/api/sub"; @@ -125,7 +129,9 @@ function createSectionContent(section) { form.Flag, "subscription_group_by_countries", _("Группировать по странам"), - _("Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы"), + _( + "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", + ), ); o.default = "0"; o.rmempty = false; @@ -135,7 +141,9 @@ function createSectionContent(section) { form.DynamicList, "subscription_filter_include_keywords", _("Include servers by keyword"), - _("Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all."), + _( + "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", + ), ); o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o.rmempty = true; @@ -144,7 +152,9 @@ function createSectionContent(section) { form.DynamicList, "subscription_filter_exclude_keywords", _("Exclude servers by keyword"), - _("Drop subscription servers whose name contains any of these keywords (case-insensitive)."), + _( + "Drop subscription servers whose name contains any of these keywords (case-insensitive).", + ), ); o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o.rmempty = true; @@ -153,7 +163,9 @@ function createSectionContent(section) { form.DynamicList, "selector_proxy_links", _("Selector Proxy Links"), - _("vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") + _( + "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", + ), ); o.depends({ connection_type: "proxy", proxy_config_type: "selector" }); o.rmempty = false; @@ -176,7 +188,9 @@ function createSectionContent(section) { form.DynamicList, "urltest_proxy_links", _("URLTest Proxy Links"), - _("vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links") + _( + "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", + ), ); o.depends({ connection_type: "proxy", proxy_config_type: "urltest" }); o.rmempty = false; @@ -199,7 +213,7 @@ function createSectionContent(section) { form.ListValue, "urltest_check_interval", _("URLTest Check Interval"), - _("The interval between connectivity tests") + _("The interval between connectivity tests"), ); o.value("30s", _("Every 30 seconds")); o.value("1m", _("Every 1 minute")); @@ -213,7 +227,9 @@ function createSectionContent(section) { form.Value, "urltest_tolerance", _("URLTest Tolerance"), - _("The maximum difference in response times (ms) allowed when comparing servers") + _( + "The maximum difference in response times (ms) allowed when comparing servers", + ), ); o.default = "50"; o.rmempty = false; @@ -226,23 +242,38 @@ function createSectionContent(section) { const parsed = parseFloat(value); - if (/^[0-9]+$/.test(value) && !isNaN(parsed) && isFinite(parsed) && parsed >= 50 && parsed <= 1000) { + if ( + /^[0-9]+$/.test(value) && + !isNaN(parsed) && + isFinite(parsed) && + parsed >= 50 && + parsed <= 1000 + ) { return true; } - return _('Must be a number in the range of 50 - 1000'); + return _("Must be a number in the range of 50 - 1000"); }; o = section.option( form.Value, "urltest_testing_url", _("URLTest Testing URL"), - _("The URL used to test server connectivity") + _("The URL used to test server connectivity"), + ); + o.value( + "https://www.gstatic.com/generate_204", + "https://www.gstatic.com/generate_204 (Google)", + ); + o.value( + "https://cp.cloudflare.com/generate_204", + "https://cp.cloudflare.com/generate_204 (Cloudflare)", ); - o.value("https://www.gstatic.com/generate_204", "https://www.gstatic.com/generate_204 (Google)"); - o.value("https://cp.cloudflare.com/generate_204", "https://cp.cloudflare.com/generate_204 (Cloudflare)"); o.value("https://captive.apple.com", "https://captive.apple.com (Apple)"); - o.value("https://connectivity-check.ubuntu.com", "https://connectivity-check.ubuntu.com (Ubuntu)") + o.value( + "https://connectivity-check.ubuntu.com", + "https://connectivity-check.ubuntu.com (Ubuntu)", + ); o.default = "https://www.gstatic.com/generate_204"; o.rmempty = false; o.depends({ connection_type: "proxy", proxy_config_type: "urltest" }); @@ -272,6 +303,20 @@ function createSectionContent(section) { o.depends("connection_type", "proxy"); o.rmempty = false; + o = section.option( + form.Flag, + "global_proxy", + _("Global Proxy"), + _( + "Route all unmatched traffic through this section's outbound. " + + "When enabled, traffic not matching any other section's lists will go through this proxy. " + + "Use with Exclusion sections to route specific domains directly. " + + "Only one section can be global at a time.", + ), + ); + o.default = "0"; + o.rmempty = false; + o = section.option( widgets.DeviceSelect, "interface", @@ -368,7 +413,7 @@ function createSectionContent(section) { "community_lists", _("Community Lists"), _("Select a predefined list for routing") + - ' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>', + ' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>', ); o.placeholder = "Service list"; Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { @@ -575,7 +620,7 @@ function createSectionContent(section) { _("User Subnets List"), _( "Enter subnets in CIDR notation or single IP addresses, separated by commas, spaces, or newlines. " + - "You can add comments using //", + "You can add comments using //", ), ); o.placeholder = @@ -748,7 +793,7 @@ function createSectionContent(section) { _("Mixed Proxy Port"), _( "Specify the port number on which the mixed proxy will run for this section. " + - "Make sure the selected port is not used by another service", + "Make sure the selected port is not used by another service", ), ); o.default = "2080"; diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js index 7167bc6c..97a32453 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js @@ -193,9 +193,7 @@ function createSettingsContent(section) { } // Reject lan* - if ( - value.startsWith("lan") - ) { + if (value.startsWith("lan")) { return false; } @@ -289,7 +287,9 @@ function createSettingsContent(section) { form.Flag, "enable_yacd_wan_access", _("Enable YACD WAN Access"), - _("Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."), + _( + "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", + ), ); o.depends("enable_yacd", "1"); o.default = "0"; @@ -299,7 +299,9 @@ function createSettingsContent(section) { form.Value, "yacd_secret_key", _("YACD Secret Key"), - _("Secret key for authenticating remote access to YACD when WAN access is enabled."), + _( + "Secret key for authenticating remote access to YACD when WAN access is enabled.", + ), ); o.depends("enable_yacd_wan_access", "1"); o.rmempty = false; @@ -356,7 +358,11 @@ function createSettingsContent(section) { for (const secName in sections) { const sec = sections[secName]; - if (sec[".type"] === "section" && sec['connection_type'] !== 'block' && sec['connection_type'] !== 'exclusion') { + if ( + sec[".type"] === "section" && + sec["connection_type"] !== "block" && + sec["connection_type"] !== "exclusion" + ) { this.keylist.push(secName); this.vallist.push(secName); } @@ -427,9 +433,7 @@ function createSettingsContent(section) { form.ListValue, "log_level", _("Log Level"), - _( - "Select the log level for sing-box", - ), + _("Select the log level for sing-box"), ); o.value("trace", "Trace"); o.value("debug", "Debug"); @@ -452,6 +456,32 @@ function createSettingsContent(section) { o.default = "0"; o.rmempty = false; + o = section.option( + form.Flag, + "block_doh", + _("Block DoH Servers"), + _( + "Block direct connections to known public DNS-over-HTTPS (DoH) servers. " + + "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS. " + + "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers. " + + "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT.", + ), + ); + o.default = "0"; + o.rmempty = false; + + o = section.option( + form.Flag, + "enable_ipv6", + _("Enable IPv6 Support"), + _( + "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support. " + + "Use this only when the router has working IPv6 connectivity.", + ), + ); + o.default = "0"; + o.rmempty = false; + o = section.option( form.DynamicList, "routing_excluded_ips", @@ -466,7 +496,7 @@ function createSettingsContent(section) { return true; } - const validation = main.validateIPV4(value); + const validation = main.validateIP(value); if (validation.valid) { return true; diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index e43b8111..68d0c4eb 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -1,15 +1,15 @@ # RU translations for NETSHIFT package. # Copyright (C) 2026 THE NETSHIFT'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# yandexru45, 2026. +# spgsroot, yandexru45, 2026. # msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 21:43+0300\n" -"PO-Revision-Date: 2026-06-05 21:43+0300\n" -"Last-Translator: yandexru45\n" +"POT-Creation-Date: 2026-06-06 15:00+0800\n" +"PO-Revision-Date: 2026-06-06 15:00+0800\n" +"Last-Translator: spgsroot, yandexru45\n" "Language-Team: none\n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -29,12 +29,6 @@ msgstr "✘ Отключено" msgid "✘ Stopped" msgstr "✘ Остановлен" -msgid "Группировать по странам" -msgstr "" - -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" -msgstr "" - msgid "Active Connections" msgstr "Активные соединения" @@ -56,6 +50,9 @@ msgstr "Необходимо указать хотя бы одну действ msgid "Available actions" msgstr "Доступные действия" +msgid "Block DoH Servers" +msgstr "Блокировать DoH-серверы" + msgid "Bootsrap DNS" msgstr "Bootstrap DNS" @@ -102,7 +99,7 @@ msgid "Config File Path" msgstr "Путь к файлу конфигурации" msgid "Configuration for NetShift service" -msgstr "" +msgstr "Конфигурация службы NetShift" msgid "Configuration Type" msgstr "Тип конфигурации" @@ -132,7 +129,7 @@ msgid "Dashboard currently unavailable" msgstr "Дашборд сейчас недоступен" msgid "Delay in milliseconds before reloading NetShift after interface UP" -msgstr "" +msgstr "Задержка в миллисекундах перед перезагрузкой NetShift после поднятия интерфейса" msgid "Delay value cannot be empty" msgstr "Значение задержки не может быть пустым" @@ -218,6 +215,9 @@ msgstr "Включить встроенный DNS-резолвер для дом msgid "Enable DNS resolve to get real IP when routing" msgstr "Разрешать домены в реальные IP-адреса перед маршрутизацией в outbound" +msgid "Enable IPv6 Support" +msgstr "Включить поддержку IPv6" + msgid "Enable Mixed Proxy" msgstr "Включить смешанный прокси" @@ -246,22 +246,22 @@ msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addres msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса" msgid "Enter the subscription URL to fetch proxy configurations from your provider" -msgstr "" +msgstr "Введите URL подписки для получения конфигураций прокси от вашего провайдера" msgid "Every 1 minute" msgstr "Каждую минуту" msgid "Every 12 hours" -msgstr "" +msgstr "Каждые 12 часов" msgid "Every 3 hours" -msgstr "" +msgstr "Каждые 3 часа" msgid "Every 3 minutes" msgstr "Каждые 3 минуты" msgid "Every 30 minutes" -msgstr "" +msgstr "Каждые 30 минут" msgid "Every 30 seconds" msgstr "Каждые 30 секунд" @@ -270,13 +270,13 @@ msgid "Every 5 minutes" msgstr "Каждые 5 минут" msgid "Every 6 hours" -msgstr "" +msgstr "Каждые 6 часов" msgid "Every day" -msgstr "" +msgstr "Каждый день" msgid "Every hour" -msgstr "" +msgstr "Каждый час" msgid "Exclude NTP" msgstr "Исключить NTP" @@ -305,8 +305,11 @@ msgstr "Получить глобальную проверку" msgid "Global check" msgstr "Глобальная проверка" +msgid "Global Proxy" +msgstr "Глобальный прокси" + msgid "How often to automatically update the subscription" -msgstr "" +msgstr "Как часто автоматически обновлять подписку" msgid "HTTP error" msgstr "Ошибка HTTP" @@ -329,14 +332,14 @@ msgstr "Задержка при мониторинге интерфейсов" msgid "Interface monitoring for Bad WAN" msgstr "Мониторинг интерфейса для Bad WAN" -msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" -msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH" +msgid "Invalid DNS server format. Examples: 8.8.8.8, [::1], dns.example.com, or dns.example.com/dns-query for DoH" +msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, [::1], dns.example.com или dns.example.com/dns-query для DoH" msgid "Invalid domain address" msgstr "Неверный домен" -msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" -msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y" +msgid "Invalid format. Use X.X.X.X/Y or IPv6/Y" +msgstr "Неверный формат. Используйте X.X.X.X/Y или IPv6/Y" msgid "Invalid HY2 URL: insecure must be 0 or 1" msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1" @@ -380,6 +383,9 @@ msgstr "Неверный URL Hysteria2: неподдерживаемый тип msgid "Invalid IP address" msgstr "Неверный IP-адрес" +msgid "Invalid IPv6 address" +msgstr "Неверный IPv6-адрес" + msgid "Invalid JSON format" msgstr "Неверный формат JSON" @@ -482,6 +488,9 @@ msgstr "Неверный URL VMess: должен начинаться с vmess:/ msgid "IP address 0.0.0.0 is not allowed" msgstr "IP-адрес 0.0.0.0 не допускается" +msgid "IPv6 CIDR must be between 0 and 128" +msgstr "IPv6 CIDR должен быть от 0 до 128" + msgid "Issues detected" msgstr "Обнаружены проблемы" @@ -522,13 +531,13 @@ msgid "Must be a number in the range of 50 - 1000" msgstr "Должно быть числом от 50 до 1000" msgid "NetShift" -msgstr "" +msgstr "NetShift" msgid "NetShift Settings" -msgstr "" +msgstr "Настройки NetShift" msgid "NetShift will not modify your DHCP configuration" -msgstr "" +msgstr "NetShift не будет изменять вашу конфигурацию DHCP" msgid "Network Interface" msgstr "Сетевой интерфейс" @@ -597,7 +606,7 @@ msgid "Resolve real IP for routing" msgstr "Разрешение реальных IP-адресов" msgid "Restart NetShift" -msgstr "" +msgstr "Перезапустить NetShift" msgid "Route main DNS through proxy/VPN" msgstr "Основной DNS через прокси/VPN" @@ -750,19 +759,19 @@ msgid "Specify the path to the list file located on the router filesystem" msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора." msgid "Start NetShift" -msgstr "" +msgstr "Запустить NetShift" msgid "Stop NetShift" -msgstr "" +msgstr "Остановить NetShift" msgid "Subscription" -msgstr "" +msgstr "Подписка" msgid "Subscription Update Interval" -msgstr "" +msgstr "Интервал обновления подписки" msgid "Subscription URL" -msgstr "" +msgstr "URL подписки" msgid "Successfully copied!" msgstr "Успешно скопировано!" @@ -898,3 +907,9 @@ msgstr "Секретный ключ YACD" msgid "You can select Output Network Interface, by default autodetect" msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически." + +msgid "Группировать по странам" +msgstr "Группировать по странам" + +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index 37c06163..c70bb9d5 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -1,1262 +1,1285 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# yandexru45 <sukadark228@gmail.com>, 2026. +# spgsroot, yandexru45, 2026. #, fuzzy msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-05 18:43+0300\n" -"PO-Revision-Date: 2026-06-05 18:43+0300\n" -"Last-Translator: yandexru45 <sukadark228@gmail.com>\n" +"POT-Creation-Date: 2026-06-06 07:00+0800\n" +"PO-Revision-Date: 2026-06-06 07:00+0800\n" +"Last-Translator: spgsroot, yandexru45\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src\netshift\tabs\dashboard\initController.ts:345 +#: src/netshift/tabs/dashboard/initController.ts:345 msgid "✔ Enabled" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:356 +#: src/netshift/tabs/dashboard/initController.ts:356 msgid "✔ Running" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:346 +#: src/netshift/tabs/dashboard/initController.ts:346 msgid "✘ Disabled" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:357 +#: src/netshift/tabs/dashboard/initController.ts:357 msgid "✘ Stopped" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:127 -msgid "Группировать по странам" -msgstr "" - -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:128 -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" -msgstr "" - -#: src\netshift\tabs\dashboard\initController.ts:307 +#: src/netshift/tabs/dashboard/initController.ts:307 msgid "Active Connections" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:106 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:106 msgid "Additional marking rules found" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:292 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:269 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:300 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:514 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:559 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:595 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:640 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:47 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:47 msgid "Available actions" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:65 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:462 +msgid "Block DoH Servers" +msgstr "" + +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:65 msgid "Bootsrap DNS" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:45 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 msgid "Bootstrap DNS server" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:58 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:58 msgid "Browser is not using FakeIP" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:57 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:57 msgid "Browser is using FakeIP correctly" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:393 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:399 msgid "Cache File Path" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:407 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:413 msgid "Cache file path cannot be empty" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:27 -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:28 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:27 -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:25 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:27 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:28 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:27 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:25 msgid "Cannot receive checks result" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:15 -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:15 -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:13 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:15 -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:13 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:15 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:15 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:13 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:15 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:13 msgid "Checking, please wait" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getCheckTitle.ts:2 +#: src/netshift/tabs/diagnostic/helpers/getCheckTitle.ts:2 msgid "checks" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:26 +#: src/netshift/tabs/diagnostic/helpers/getMeta.ts:26 msgid "Checks failed" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:13 +#: src/netshift/tabs/diagnostic/helpers/getMeta.ts:13 msgid "Checks passed" msgstr "" -#: src\validators\validateSubnet.ts:33 +#: src/validators/validateSubnet.ts:25 msgid "CIDR must be between 0 and 32" msgstr "" -#: src\partials\modal\renderModal.ts:26 +#: src/partials/modal/renderModal.ts:26 msgid "Close" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:369 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:414 msgid "Community Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:380 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:386 msgid "Config File Path" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:27 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:27 msgid "Configuration for NetShift service" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:23 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:23 msgid "Configuration Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:12 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:12 msgid "Connection Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:26 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:26 msgid "Connection URL" msgstr "" -#: src\partials\modal\renderModal.ts:20 +#: src/partials/modal/renderModal.ts:20 msgid "Copy" msgstr "" -#: src\netshift\methods\shell\index.ts:157 -#: src\netshift\methods\shell\pollSingBoxComponentAction.ts:65 +#: src/netshift/methods/shell/index.ts:157 +#: src/netshift/methods/shell/pollSingBoxComponentAction.ts:65 msgid "Core switch failed" msgstr "" -#: src\netshift\methods\shell\pollSingBoxComponentAction.ts:82 +#: src/netshift/methods/shell/pollSingBoxComponentAction.ts:82 msgid "Core switch timed out" msgstr "" -#: src\netshift\tabs\dashboard\partials\renderWidget.ts:22 +#: src/netshift/tabs/dashboard/partials/renderWidget.ts:22 msgid "Currently unavailable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:80 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:80 msgid "Dashboard" msgstr "" -#: src\netshift\tabs\dashboard\partials\renderSections.ts:19 +#: src/netshift/tabs/dashboard/partials/renderSections.ts:19 msgid "Dashboard currently unavailable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:267 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:265 msgid "Delay in milliseconds before reloading NetShift after interface UP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:274 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:272 msgid "Delay value cannot be empty" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:93 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:93 msgid "DHCP has DNS server" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:65 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:65 msgid "Diagnostics" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:83 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:83 msgid "Disable autostart" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:310 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:312 msgid "Disable QUIC" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:311 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:313 msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:460 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:540 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585 msgid "Disabled" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:88 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:88 msgid "DNS on router" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:79 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:79 msgid "DNS outbound section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:337 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:15 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:338 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:16 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:334 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:12 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:379 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12 msgid "DNS Protocol Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:113 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:113 msgid "DNS Rewrite TTL" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:347 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:24 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:392 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24 msgid "DNS Server" msgstr "" -#: src\validators\validateDns.ts:7 +#: src/validators/validateDns.ts:7 msgid "DNS server address cannot be empty" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:26 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26 msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369 msgid "Domain Resolver" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:371 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:377 msgid "Dont Touch My DHCP!" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:241 -#: src\netshift\tabs\dashboard\initController.ts:275 +#: src/netshift/tabs/dashboard/initController.ts:241 +#: src/netshift/tabs/dashboard/initController.ts:275 msgid "Downlink" msgstr "" -#: src\partials\modal\renderModal.ts:15 +#: src/partials/modal/renderModal.ts:15 msgid "Download" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:333 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:335 msgid "Download Lists via Proxy/VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:342 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:344 msgid "Download Lists via specific proxy section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:334 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:343 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:336 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:345 msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:147 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:461 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:541 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586 msgid "Dynamic List" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:93 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:93 msgid "Enable autostart" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:325 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:764 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:809 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:735 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:476 +msgid "Enable IPv6 Support" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:780 msgid "Enable Mixed Proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:171 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:171 msgid "Enable Output Network Interface" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:736 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:781 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:282 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:280 msgid "Enable YACD" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:291 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:289 msgid "Enable YACD WAN Access" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:67 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:69 msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:496 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:541 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:470 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:550 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:595 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:90 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92 msgid "Enter the subscription URL to fetch proxy configurations from your provider" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:205 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219 msgid "Every 1 minute" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:119 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123 msgid "Every 12 hours" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:117 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121 msgid "Every 3 hours" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:206 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220 msgid "Every 3 minutes" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:115 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119 msgid "Every 30 minutes" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:204 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:218 msgid "Every 30 seconds" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:207 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:221 msgid "Every 5 minutes" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:118 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122 msgid "Every 6 hours" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:120 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:124 msgid "Every day" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:116 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:120 msgid "Every hour" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:447 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:451 msgid "Exclude NTP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:448 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:452 msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:146 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154 msgid "Exclude servers by keyword" msgstr "" -#: src\helpers\copyToClipboard.ts:12 +#: src/helpers/copyToClipboard.ts:12 msgid "Failed to copy!" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:229 -#: src\netshift\tabs\diagnostic\initController.ts:233 -#: src\netshift\tabs\diagnostic\initController.ts:263 -#: src\netshift\tabs\diagnostic\initController.ts:267 -#: src\netshift\tabs\diagnostic\initController.ts:304 -#: src\netshift\tabs\diagnostic\initController.ts:308 -#: src\netshift\tabs\diagnostic\initController.ts:347 -#: src\netshift\tabs\diagnostic\initController.ts:351 +#: src/netshift/tabs/diagnostic/initController.ts:229 +#: src/netshift/tabs/diagnostic/initController.ts:233 +#: src/netshift/tabs/diagnostic/initController.ts:263 +#: src/netshift/tabs/diagnostic/initController.ts:267 +#: src/netshift/tabs/diagnostic/initController.ts:304 +#: src/netshift/tabs/diagnostic/initController.ts:308 +#: src/netshift/tabs/diagnostic/initController.ts:347 +#: src/netshift/tabs/diagnostic/initController.ts:351 msgid "Failed to execute!" msgstr "" -#: src\netshift\methods\custom\getDashboardSections.ts:150 -#: src\netshift\methods\custom\getDashboardSections.ts:181 -#: src\netshift\methods\custom\getDashboardSections.ts:218 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:58 +#: src/netshift/methods/custom/getDashboardSections.ts:150 +#: src/netshift/methods/custom/getDashboardSections.ts:181 +#: src/netshift/methods/custom/getDashboardSections.ts:218 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:58 msgid "Fastest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:708 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:753 msgid "Fully Routed IPs" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:102 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:102 msgid "Get global check" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:224 +#: src/netshift/tabs/diagnostic/initController.ts:224 msgid "Global check" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:113 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309 +msgid "Global Proxy" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117 msgid "How often to automatically update the subscription" msgstr "" -#: src\netshift\api.ts:27 +#: src/netshift/api.ts:27 msgid "HTTP error" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:137 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:143 msgid "Include servers by keyword" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129 msgid "Install extended" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:129 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129 msgid "Install stable" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:234 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:232 msgid "Interface Monitoring" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:266 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:264 msgid "Interface Monitoring Delay" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:235 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:233 msgid "Interface monitoring for Bad WAN" msgstr "" -#: src\validators\validateDns.ts:23 -msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH" +#: src/validators/validateDns.ts:39 +msgid "Invalid DNS server format. Examples: 8.8.8.8, [::1], dns.example.com, or dns.example.com/dns-query for DoH" msgstr "" -#: src\validators\validateDomain.ts:18 -#: src\validators\validateDomain.ts:27 +#: src/validators/validateDomain.ts:18 +#: src/validators/validateDomain.ts:27 msgid "Invalid domain address" msgstr "" -#: src\validators\validateSubnet.ts:11 -msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" +#: src/validators/validateSubnet.ts:57 +msgid "Invalid format. Use X.X.X.X/Y or IPv6/Y" msgstr "" -#: src\validators\validateHysteriaUrl.ts:90 +#: src/validators/validateHysteriaUrl.ts:90 msgid "Invalid HY2 URL: insecure must be 0 or 1" msgstr "" -#: src\validators\validateHysteriaUrl.ts:77 +#: src/validators/validateHysteriaUrl.ts:77 msgid "Invalid HY2 URL: invalid port number" msgstr "" -#: src\validators\validateHysteriaUrl.ts:30 +#: src/validators/validateHysteriaUrl.ts:30 msgid "Invalid HY2 URL: missing credentials/server" msgstr "" -#: src\validators\validateHysteriaUrl.ts:47 +#: src/validators/validateHysteriaUrl.ts:47 msgid "Invalid HY2 URL: missing host" msgstr "" -#: src\validators\validateHysteriaUrl.ts:41 +#: src/validators/validateHysteriaUrl.ts:41 msgid "Invalid HY2 URL: missing host & port" msgstr "" -#: src\validators\validateHysteriaUrl.ts:36 +#: src/validators/validateHysteriaUrl.ts:36 msgid "Invalid HY2 URL: missing password" msgstr "" -#: src\validators\validateHysteriaUrl.ts:50 +#: src/validators/validateHysteriaUrl.ts:50 msgid "Invalid HY2 URL: missing port" msgstr "" -#: src\validators\validateHysteriaUrl.ts:18 +#: src/validators/validateHysteriaUrl.ts:18 msgid "Invalid HY2 URL: must not contain spaces" msgstr "" -#: src\validators\validateHysteriaUrl.ts:12 +#: src/validators/validateHysteriaUrl.ts:12 msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" msgstr "" -#: src\validators\validateHysteriaUrl.ts:108 +#: src/validators/validateHysteriaUrl.ts:108 msgid "Invalid HY2 URL: obfs-password required when obfs is set" msgstr "" -#: src\validators\validateHysteriaUrl.ts:122 +#: src/validators/validateHysteriaUrl.ts:122 msgid "Invalid HY2 URL: parsing failed" msgstr "" -#: src\validators\validateHysteriaUrl.ts:116 +#: src/validators/validateHysteriaUrl.ts:116 msgid "Invalid HY2 URL: sni cannot be empty" msgstr "" -#: src\validators\validateHysteriaUrl.ts:98 +#: src/validators/validateHysteriaUrl.ts:98 msgid "Invalid HY2 URL: unsupported obfs type" msgstr "" -#: src\validators\validateIp.ts:11 +#: src/validators/validateIp.ts:11 msgid "Invalid IP address" msgstr "" -#: src\validators\validateOutboundJson.ts:9 +#: src/validators/validateIp.ts:28 +msgid "Invalid IPv6 address" +msgstr "" + +#: src/validators/validateOutboundJson.ts:9 msgid "Invalid JSON format" msgstr "" -#: src\validators\validatePath.ts:22 +#: src/validators/validatePath.ts:22 msgid "Invalid path format. Path must start with \"/\" and contain valid characters" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:85 +#: src/validators/validateShadowsocksUrl.ts:85 msgid "Invalid port number. Must be between 1 and 65535" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:37 +#: src/validators/validateShadowsocksUrl.ts:37 msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:27 +#: src/validators/validateShadowsocksUrl.ts:27 msgid "Invalid Shadowsocks URL: missing credentials" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:46 +#: src/validators/validateShadowsocksUrl.ts:46 msgid "Invalid Shadowsocks URL: missing method and password separator \":\"" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:76 +#: src/validators/validateShadowsocksUrl.ts:76 msgid "Invalid Shadowsocks URL: missing port" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:67 +#: src/validators/validateShadowsocksUrl.ts:67 msgid "Invalid Shadowsocks URL: missing server" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:58 +#: src/validators/validateShadowsocksUrl.ts:58 msgid "Invalid Shadowsocks URL: missing server address" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:16 +#: src/validators/validateShadowsocksUrl.ts:16 msgid "Invalid Shadowsocks URL: must not contain spaces" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:8 +#: src/validators/validateShadowsocksUrl.ts:8 msgid "Invalid Shadowsocks URL: must start with ss://" msgstr "" -#: src\validators\validateShadowsocksUrl.ts:91 +#: src/validators/validateShadowsocksUrl.ts:91 msgid "Invalid Shadowsocks URL: parsing failed" msgstr "" -#: src\validators\validateSocksUrl.ts:73 +#: src/validators/validateSocksUrl.ts:73 msgid "Invalid SOCKS URL: invalid host format" msgstr "" -#: src\validators\validateSocksUrl.ts:63 +#: src/validators/validateSocksUrl.ts:63 msgid "Invalid SOCKS URL: invalid port number" msgstr "" -#: src\validators\validateSocksUrl.ts:42 +#: src/validators/validateSocksUrl.ts:42 msgid "Invalid SOCKS URL: missing host and port" msgstr "" -#: src\validators\validateSocksUrl.ts:51 +#: src/validators/validateSocksUrl.ts:51 msgid "Invalid SOCKS URL: missing hostname or IP" msgstr "" -#: src\validators\validateSocksUrl.ts:56 +#: src/validators/validateSocksUrl.ts:56 msgid "Invalid SOCKS URL: missing port" msgstr "" -#: src\validators\validateSocksUrl.ts:34 +#: src/validators/validateSocksUrl.ts:34 msgid "Invalid SOCKS URL: missing username" msgstr "" -#: src\validators\validateSocksUrl.ts:19 +#: src/validators/validateSocksUrl.ts:19 msgid "Invalid SOCKS URL: must not contain spaces" msgstr "" -#: src\validators\validateSocksUrl.ts:10 +#: src/validators/validateSocksUrl.ts:10 msgid "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" msgstr "" -#: src\validators\validateSocksUrl.ts:77 +#: src/validators/validateSocksUrl.ts:77 msgid "Invalid SOCKS URL: parsing failed" msgstr "" -#: src\validators\validateTrojanUrl.ts:15 +#: src/validators/validateTrojanUrl.ts:15 msgid "Invalid Trojan URL: must not contain spaces" msgstr "" -#: src\validators\validateTrojanUrl.ts:8 +#: src/validators/validateTrojanUrl.ts:8 msgid "Invalid Trojan URL: must start with trojan://" msgstr "" -#: src\validators\validateTrojanUrl.ts:56 +#: src/validators/validateTrojanUrl.ts:56 msgid "Invalid Trojan URL: parsing failed" msgstr "" -#: src\validators\validateUrl.ts:8 -#: src\validators\validateUrl.ts:31 +#: src/validators/validateUrl.ts:8 +#: src/validators/validateUrl.ts:31 msgid "Invalid URL format" msgstr "" -#: src\validators\validateVlessUrl.ts:110 +#: src/validators/validateVlessUrl.ts:110 msgid "Invalid VLESS URL: parsing failed" msgstr "" -#: src\validators\validateVmessUrl.ts:82 +#: src/validators/validateVmessUrl.ts:82 msgid "Invalid VMess URL: invalid port" msgstr "" -#: src\validators\validateVmessUrl.ts:40 +#: src/validators/validateVmessUrl.ts:40 msgid "Invalid VMess URL: malformed base64" msgstr "" -#: src\validators\validateVmessUrl.ts:50 -#: src\validators\validateVmessUrl.ts:57 +#: src/validators/validateVmessUrl.ts:50 +#: src/validators/validateVmessUrl.ts:57 msgid "Invalid VMess URL: malformed JSON" msgstr "" -#: src\validators\validateVmessUrl.ts:66 +#: src/validators/validateVmessUrl.ts:66 msgid "Invalid VMess URL: missing address" msgstr "" -#: src\validators\validateVmessUrl.ts:73 +#: src/validators/validateVmessUrl.ts:73 msgid "Invalid VMess URL: missing id" msgstr "" -#: src\validators\validateVmessUrl.ts:25 +#: src/validators/validateVmessUrl.ts:25 msgid "Invalid VMess URL: must not contain spaces" msgstr "" -#: src\validators\validateVmessUrl.ts:7 +#: src/validators/validateVmessUrl.ts:7 msgid "Invalid VMess URL: must start with vmess://" msgstr "" -#: src\validators\validateSubnet.ts:18 +#: src/validators/validateSubnet.ts:11 msgid "IP address 0.0.0.0 is not allowed" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getMeta.ts:20 +#: src/validators/validateSubnet.ts:47 +msgid "IPv6 CIDR must be between 0 and 128" +msgstr "" + +#: src/netshift/tabs/diagnostic/helpers/getMeta.ts:20 msgid "Issues detected" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:138 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:144 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:48 +#: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:48 msgid "Latest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:321 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:323 msgid "List Update Frequency" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:616 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661 msgid "Local Domain Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:639 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:684 msgid "Local Subnet Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:429 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:435 msgid "Log Level" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:72 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:72 msgid "Main DNS" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runDnsCheck.ts:81 +#: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:81 msgid "Main DNS via outbound" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:311 +#: src/netshift/tabs/dashboard/initController.ts:311 msgid "Memory Usage" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:748 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793 msgid "Mixed Proxy Port" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:243 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:241 msgid "Monitored Interfaces" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:233 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:255 msgid "Must be a number in the range of 50 - 1000" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:343 +#: src/netshift/tabs/dashboard/initController.ts:343 msgid "NetShift" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:26 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:26 msgid "NetShift Settings" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:372 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:378 msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:278 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323 msgid "Network Interface" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:105 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:105 msgid "No other marking rules found" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderCheckSection.ts:189 +#: src/netshift/tabs/diagnostic/partials/renderCheckSection.ts:189 msgid "Not implement yet" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:74 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:80 -#: src\netshift\tabs\diagnostic\checks\runSectionsCheck.ts:99 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:74 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:80 +#: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:99 msgid "Not responding" msgstr "" -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:59 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:67 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:75 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:83 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:91 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:59 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:67 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:75 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:83 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:91 msgid "Not running" msgstr "" -#: src\helpers\withTimeout.ts:7 +#: src/helpers/withTimeout.ts:7 msgid "Operation timed out" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:30 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:30 msgid "Outbound Config" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:66 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:68 msgid "Outbound Configuration" msgstr "" -#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:38 +#: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:38 msgid "Outdated" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:180 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:180 msgid "Output Network Interface" msgstr "" -#: src\validators\validatePath.ts:7 +#: src/validators/validatePath.ts:7 msgid "Path cannot be empty" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:411 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:417 msgid "Path must be absolute (start with /)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:420 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:426 msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:415 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:421 msgid "Path must end with cache.db" msgstr "" -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:107 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:115 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:123 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:131 -#: src\netshift\tabs\diagnostic\diagnostic.store.ts:139 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:107 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:115 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:123 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:131 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:139 msgid "Pending" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:37 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:37 msgid "Proxy Configuration URL" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:66 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:66 msgid "Proxy traffic is not routed via FakeIP" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:65 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:65 msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:403 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:448 msgid "Regional options cannot be used together" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:662 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:707 msgid "Remote Domain Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:685 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:730 msgid "Remote Subnet Lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:763 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808 msgid "Resolve real IP for routing" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:53 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:53 msgid "Restart NetShift" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:68 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:68 msgid "Route main DNS through proxy/VPN" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:51 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:51 msgid "Router DNS is not routed through sing-box" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runFakeIPCheck.ts:50 +#: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:50 msgid "Router DNS is routed through sing-box" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:458 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:488 msgid "Routing Excluded IPs" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:79 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:79 msgid "Rules mangle counters" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:74 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:74 msgid "Rules mangle exist" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:89 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:89 msgid "Rules mangle output counters" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:84 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:84 msgid "Rules mangle output exist" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:99 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 msgid "Rules proxy counters" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:94 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:94 msgid "Rules proxy exist" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderRunAction.ts:15 +#: src/netshift/tabs/diagnostic/partials/renderRunAction.ts:15 msgid "Run Diagnostic" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:422 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467 msgid "Russia inside restrictions" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:302 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:302 msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:36 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:36 msgid "Sections" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:370 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:415 msgid "Select a predefined list for routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:13 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:13 msgid "Select between VPN and Proxy connection methods for traffic routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:13 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:13 msgid "Select DNS protocol to use" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:322 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:324 msgid "Select how often the domain or subnet lists are updated automatically" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:24 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:24 msgid "Select how to configure the proxy" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:279 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 msgid "Select network interface for VPN connection" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:348 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:25 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:393 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25 msgid "Select or enter DNS server address" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:394 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:400 msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:381 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:387 msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:335 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:380 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:458 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:503 msgid "Select the list type for adding custom domains" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:538 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:583 msgid "Select the list type for adding custom subnets" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:430 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:436 msgid "Select the log level for sing-box" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:135 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:135 msgid "Select the network interface from which the traffic will originate" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:181 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:181 msgid "Select the network interface to which the traffic will originate" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:244 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:242 msgid "Select the WAN interfaces to be monitored" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:27 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:27 msgid "Selector" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:155 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165 msgid "Selector Proxy Links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:69 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:69 msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:340 +#: src/netshift/tabs/dashboard/initController.ts:340 msgid "Services info" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\netshift.js:49 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:49 msgid "Settings" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:292 -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:120 +#: src/netshift/tabs/diagnostic/initController.ts:292 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:120 msgid "Show sing-box config" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:354 +#: src/netshift/tabs/dashboard/initController.ts:354 msgid "Sing-box" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:77 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:77 msgid "Sing-box autostart disabled" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:342 +#: src/netshift/tabs/diagnostic/initController.ts:342 msgid "Sing-box core changed, version:" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:62 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:62 msgid "Sing-box installed" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:87 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:87 msgid "Sing-box listening ports" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:82 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:82 msgid "Sing-box process running" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:72 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:72 msgid "Sing-box service exist" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runSingBoxCheck.ts:67 +#: src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:67 msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:134 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:134 msgid "Source Network Interface" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:459 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:489 msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:709 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:754 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:663 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:708 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:686 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:731 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:617 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:640 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:685 msgid "Specify the path to the list file located on the router filesystem" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:73 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:73 msgid "Start NetShift" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:63 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:63 msgid "Stop NetShift" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:29 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:29 msgid "Subscription" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:112 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116 msgid "Subscription Update Interval" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:89 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:91 msgid "Subscription URL" msgstr "" -#: src\helpers\copyToClipboard.ts:10 +#: src/helpers/copyToClipboard.ts:10 msgid "Successfully copied!" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:331 +#: src/netshift/tabs/diagnostic/initController.ts:331 msgid "Switching sing-box core, this may take a few minutes…" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:304 +#: src/netshift/tabs/dashboard/initController.ts:304 msgid "System info" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderSystemInfo.ts:21 +#: src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts:21 msgid "System information" msgstr "" -#: src\netshift\tabs\diagnostic\checks\runNftCheck.ts:69 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:69 msgid "Table exist" msgstr "" -#: src\netshift\tabs\dashboard\partials\renderSections.ts:108 +#: src/netshift/tabs/dashboard/partials/renderSections.ts:108 msgid "Test latency" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:462 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:542 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:507 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 msgid "Text List" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:46 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:46 msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:202 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216 msgid "The interval between connectivity tests" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:216 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:240 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:262 msgid "The URL used to test server connectivity" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:114 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:114 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:238 +#: src/netshift/tabs/dashboard/initController.ts:238 msgid "Traffic" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:268 +#: src/netshift/tabs/dashboard/initController.ts:268 msgid "Traffic Total" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:25 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:25 msgid "Troubleshooting" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:125 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:125 msgid "TTL must be a positive number" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:120 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:120 msgid "TTL value cannot be empty" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:339 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:17 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:268 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299 msgid "UDP over TCP" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:39 -#: src\netshift\tabs\diagnostic\initController.ts:40 -#: src\netshift\tabs\diagnostic\initController.ts:41 -#: src\netshift\tabs\diagnostic\initController.ts:42 -#: src\netshift\tabs\diagnostic\initController.ts:43 -#: src\netshift\tabs\diagnostic\initController.ts:44 -#: src\netshift\tabs\diagnostic\helpers\getNetshiftVersionRow.ts:7 +#: src/netshift/tabs/diagnostic/initController.ts:39 +#: src/netshift/tabs/diagnostic/initController.ts:40 +#: src/netshift/tabs/diagnostic/initController.ts:41 +#: src/netshift/tabs/diagnostic/initController.ts:42 +#: src/netshift/tabs/diagnostic/initController.ts:43 +#: src/netshift/tabs/diagnostic/initController.ts:44 +#: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7 msgid "unknown" msgstr "" -#: src\netshift\api.ts:40 +#: src/netshift/api.ts:40 msgid "Unknown error" msgstr "" -#: src\netshift\tabs\dashboard\initController.ts:240 -#: src\netshift\tabs\dashboard\initController.ts:271 +#: src/netshift/tabs/dashboard/initController.ts:240 +#: src/netshift/tabs/dashboard/initController.ts:271 msgid "Uplink" msgstr "" -#: src\validators\validateProxyUrl.ts:42 +#: src/validators/validateProxyUrl.ts:42 msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" -#: src\validators\validateUrl.ts:17 +#: src/validators/validateUrl.ts:17 msgid "URL must use one of the following protocols:" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:28 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:28 msgid "URLTest" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:201 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:215 msgid "URLTest Check Interval" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:178 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:190 msgid "URLTest Proxy Links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:239 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:261 msgid "URLTest Testing URL" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:215 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229 msgid "URLTest Tolerance" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:457 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:502 msgid "User Domain List Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:469 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514 msgid "User Domains" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:495 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540 msgid "User Domains List" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:537 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:582 msgid "User Subnet List Type" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:549 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594 msgid "User Subnets" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:575 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:620 msgid "User Subnets List" msgstr "" -#: src\validators\validateDns.ts:14 -#: src\validators\validateDns.ts:18 -#: src\validators\validateDomain.ts:13 -#: src\validators\validateDomain.ts:30 -#: src\validators\validateHysteriaUrl.ts:120 -#: src\validators\validateIp.ts:8 -#: src\validators\validateOutboundJson.ts:7 -#: src\validators\validatePath.ts:16 -#: src\validators\validateShadowsocksUrl.ts:95 -#: src\validators\validateSocksUrl.ts:80 -#: src\validators\validateSubnet.ts:38 -#: src\validators\validateTrojanUrl.ts:59 -#: src\validators\validateUrl.ts:28 -#: src\validators\validateVlessUrl.ts:108 -#: src\validators\validateVmessUrl.ts:86 +#: src/validators/validateDns.ts:26 +#: src/validators/validateDns.ts:30 +#: src/validators/validateDns.ts:34 +#: src/validators/validateDomain.ts:13 +#: src/validators/validateDomain.ts:30 +#: src/validators/validateHysteriaUrl.ts:120 +#: src/validators/validateIp.ts:8 +#: src/validators/validateIp.ts:24 +#: src/validators/validateOutboundJson.ts:7 +#: src/validators/validatePath.ts:16 +#: src/validators/validateShadowsocksUrl.ts:95 +#: src/validators/validateSocksUrl.ts:80 +#: src/validators/validateSubnet.ts:30 +#: src/validators/validateSubnet.ts:52 +#: src/validators/validateTrojanUrl.ts:59 +#: src/validators/validateUrl.ts:28 +#: src/validators/validateVlessUrl.ts:108 +#: src/validators/validateVmessUrl.ts:86 msgid "Valid" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:528 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:607 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:652 msgid "Validation errors:" msgstr "" -#: src\netshift\tabs\diagnostic\initController.ts:258 -#: src\netshift\tabs\diagnostic\partials\renderAvailableActions.ts:111 +#: src/netshift/tabs/diagnostic/initController.ts:258 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:111 msgid "View logs" msgstr "" -#: src\netshift\tabs\diagnostic\partials\renderWikiDisclaimer.ts:31 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:31 msgid "Visit Wiki" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:38 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:156 -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:179 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:38 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:191 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:405 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:450 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\section.js:424 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:469 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:80 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80 msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:301 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:301 msgid "YACD Secret Key" msgstr "" -#: ..\luci-app-netshift\htdocs\luci-static\resources\view\netshift\settings.js:172 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:172 msgid "You can select Output Network Interface, by default autodetect" msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131 +msgid "Группировать по странам" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:132 +msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgstr "" diff --git a/netshift/files/etc/config/netshift b/netshift/files/etc/config/netshift index 00a1d29a..ff30f208 100644 --- a/netshift/files/etc/config/netshift +++ b/netshift/files/etc/config/netshift @@ -21,6 +21,8 @@ config settings 'settings' option log_level 'warn' option exclude_ntp '0' option shutdown_correctly '0' + option block_doh '0' + option enable_ipv6 '0' #list routing_excluded_ips '192.168.1.3' config section 'main' @@ -28,6 +30,7 @@ config section 'main' option proxy_config_type 'url' option proxy_string '' option enable_udp_over_tcp '0' + option global_proxy '0' list community_lists 'russia_inside' #option user_domain_list_type 'dynamic' #list user_domains '2ip.ru' diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 2b5ab10e..cd3e4643 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -720,14 +720,12 @@ stop_main() { fi log "Flush ip rule" - if ip rule list | grep -q "netshift"; then - ip rule del fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 - fi + ip -4 rule del fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 2>/dev/null + ip -6 rule del fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 2>/dev/null log "Flush ip route" - if ip route list table "$RT_TABLE_NAME" > /dev/null 2>&1; then - ip route flush table "$RT_TABLE_NAME" - fi + ip -4 route flush table "$RT_TABLE_NAME" 2>/dev/null + ip -6 route flush table "$RT_TABLE_NAME" 2>/dev/null log "Stop sing-box" /etc/init.d/sing-box stop @@ -747,14 +745,28 @@ start() { config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 if [ "$dont_touch_dhcp" -eq 0 ]; then - dnsmasq_configure + dnsmasq_configure force fi uci_set "netshift" "settings" "shutdown_correctly" 0 uci commit "netshift" && config_load "$NETSHIFT_CONFIG" + + monitor_sing_box & + echo $! > /var/run/netshift_monitor.pid + log "Started sing-box health monitor with PID $!" "info" } stop() { + if [ -f /var/run/netshift_monitor.pid ]; then + local monitor_pid + monitor_pid="$(cat /var/run/netshift_monitor.pid 2>/dev/null)" + if [ -n "$monitor_pid" ] && kill -0 "$monitor_pid" 2>/dev/null; then + kill "$monitor_pid" 2>/dev/null + log "Stopped sing-box health monitor" "info" + fi + rm -f /var/run/netshift_monitor.pid + fi + local dont_touch_dhcp config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 if [ "$dont_touch_dhcp" -eq 0 ]; then @@ -767,6 +779,70 @@ stop() { uci commit "netshift" && config_load "$NETSHIFT_CONFIG" } +monitor_sing_box() { + local crash_count=0 + local backoff=0 + + while true; do + sleep "$MONITOR_CHECK_INTERVAL" + + if sing_box_process_exists; then + crash_count=0 + backoff=0 + continue + fi + + config_load "$NETSHIFT_CONFIG" + local shutdown_correctly + config_get shutdown_correctly "settings" "shutdown_correctly" + if [ "$shutdown_correctly" -eq 1 ]; then + log "sing-box shutdown detected, exiting monitor" "info" + break + fi + + crash_count=$((crash_count + 1)) + log "sing-box process died unexpectedly (crash #$crash_count), restoring DNS" "warn" + + dnsmasq_restore + + if [ "$crash_count" -ge "$MONITOR_MAX_CRASHES" ]; then + log "sing-box crashed $crash_count times consecutively, giving up. DNS has been restored." "error" + break + fi + + backoff=$((MONITOR_BACKOFF_BASE * (1 << (crash_count - 1)))) + [ "$backoff" -gt "$MONITOR_BACKOFF_MAX" ] && backoff="$MONITOR_BACKOFF_MAX" + log "Waiting ${backoff}s before attempting sing-box restart" "warn" + sleep "$backoff" + + config_load "$NETSHIFT_CONFIG" + config_get shutdown_correctly "settings" "shutdown_correctly" + if [ "$shutdown_correctly" -eq 1 ]; then + log "Clean shutdown requested, exiting monitor" "info" + break + fi + + log "Attempting sing-box recovery restart" "warn" + stop_main + start_main + if [ $? -eq 0 ]; then + config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 + if [ "$dont_touch_dhcp" -eq 0 ]; then + dnsmasq_configure force + fi + log "NetShift recovered successfully after sing-box crash" "info" + else + log "Recovery restart failed, will retry" "error" + fi + done + + rm -f /var/run/netshift_monitor.pid +} + +sing_box_process_exists() { + pgrep "sing-box" > /dev/null 2>&1 || pidof sing-box > /dev/null 2>&1 +} + reload() { log "NetShift reload" stop @@ -819,21 +895,35 @@ br_netfilter_disable() { route_table_rule_mark() { grep -q "105 $RT_TABLE_NAME" /etc/iproute2/rt_tables || echo "105 $RT_TABLE_NAME" >> /etc/iproute2/rt_tables - if ! ip route list table "$RT_TABLE_NAME" 2> /dev/null | grep -q "local default dev lo scope host"; then - log "Added route for tproxy" "debug" - ip route add local 0.0.0.0/0 dev lo table "$RT_TABLE_NAME" - else - log "Route for tproxy exists" "debug" + log "Configure IPv4 route for tproxy" "debug" + ip -4 route replace local 0.0.0.0/0 dev lo table "$RT_TABLE_NAME" 2>/dev/null || \ + ip -4 route add local 0.0.0.0/0 dev lo table "$RT_TABLE_NAME" 2>/dev/null + + if netshift_ipv6_enabled; then + log "Configure IPv6 route for tproxy" "debug" + ip -6 route replace local ::/0 dev lo table "$RT_TABLE_NAME" 2>/dev/null || \ + ip -6 route add local ::/0 dev lo table "$RT_TABLE_NAME" 2>/dev/null fi - if ! ip rule list | grep -q "from all fwmark $NFT_FAKEIP_MARK/$NFT_FAKEIP_MARK lookup $RT_TABLE_NAME"; then - log "Create marking rule" "debug" - ip -4 rule add fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 - else - log "Marking rule exist" "debug" + log "Configure IPv4 marking rule" "debug" + ip -4 rule del fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 2>/dev/null + ip -4 rule add fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 2>/dev/null + + if netshift_ipv6_enabled; then + log "Configure IPv6 marking rule" "debug" + ip -6 rule del fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 2>/dev/null + ip -6 rule add fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table "$RT_TABLE_NAME" priority 105 2>/dev/null fi } +netshift_ipv6_enabled() { + local enable_ipv6 + config_get_bool enable_ipv6 "settings" "enable_ipv6" 0 + + [ "$enable_ipv6" -eq 1 ] || return 1 + ip -6 addr show dev lo 2>/dev/null | grep -q '::1' +} + nft_init_interfaces_set() { nft_create_ifname_set "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME" @@ -867,6 +957,17 @@ create_nft_rules() { 240.0.0.0-255.255.255.255 }' + if netshift_ipv6_enabled; then + log "Create localv6 set" + nft_create_ipv6_set "$NFT_TABLE_NAME" "$NFT_LOCALV6_SET_NAME" + nft add element inet "$NFT_TABLE_NAME" localv6 '{ + ::1, + fc00::/7, + fe80::/10, + ff00::/8 + }' + fi + log "Create common set" nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" @@ -879,20 +980,25 @@ create_nft_rules() { nft add chain inet "$NFT_TABLE_NAME" proxy '{ type filter hook prerouting priority -100; policy accept; }' nft add rule inet "$NFT_TABLE_NAME" mangle ct status dnat return - nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter - nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter - nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter - nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_LOCALV4_SET_NAME" return + if netshift_ipv6_enabled; then + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip6 daddr "@$NFT_LOCALV6_SET_NAME" return + fi + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter - nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto tcp tproxy ip to 127.0.0.1:1602 counter - nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto udp tproxy ip to 127.0.0.1:1602 counter + nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto tcp tproxy ip to "$SB_TPROXY_INBOUND_ADDRESS:$SB_TPROXY_INBOUND_PORT" counter + nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto udp tproxy ip to "$SB_TPROXY_INBOUND_ADDRESS:$SB_TPROXY_INBOUND_PORT" counter + if netshift_ipv6_enabled; then + nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto tcp tproxy ip6 to "$SB_TPROXY_INBOUND_ADDRESS_V6:$SB_TPROXY_INBOUND_PORT_V6" counter + nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto udp tproxy ip6 to "$SB_TPROXY_INBOUND_ADDRESS_V6:$SB_TPROXY_INBOUND_PORT_V6" counter + fi nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_LOCALV4_SET_NAME" return + if netshift_ipv6_enabled; then + nft add rule inet "$NFT_TABLE_NAME" mangle_output ip6 daddr "@$NFT_LOCALV6_SET_NAME" return + fi nft add rule inet "$NFT_TABLE_NAME" mangle_output meta mark "$NFT_OUTBOUND_MARK" counter return - nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter - nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter - nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter - nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter local exclude_ntp config_get_bool exclude_ntp "settings" "exclude_ntp" "0" @@ -913,14 +1019,45 @@ backup_dnsmasq_config_option() { fi } +uci_remove_quiet() { + local config="$1" + local section="$2" + local option="$3" + + uci -q delete "$config.$section.$option" 2>/dev/null +} + +dnsmasq_is_configured_for_netshift() { + local servers server noresolv cachesize has_netshift_server=0 + + servers="$(uci_get "dhcp" "@dnsmasq[0]" "server")" + for server in $servers; do + if [ "$server" = "$SB_DNS_INBOUND_ADDRESS" ]; then + has_netshift_server=1 + break + fi + done + + noresolv="$(uci_get "dhcp" "@dnsmasq[0]" "noresolv")" + cachesize="$(uci_get "dhcp" "@dnsmasq[0]" "cachesize")" + + [ "$has_netshift_server" -eq 1 ] && [ "$noresolv" = "1" ] && [ "$cachesize" = "0" ] +} + dnsmasq_configure() { - local shutdown_correctly + local force shutdown_correctly + force="$1" config_get shutdown_correctly "settings" "shutdown_correctly" - if [ "$shutdown_correctly" -eq 0 ]; then + if [ "$force" != "force" ] && [ "$shutdown_correctly" -eq 0 ]; then log "Previous shutdown of netshift was not correct, reconfiguration of dnsmasq is not required" return 0 fi + if dnsmasq_is_configured_for_netshift; then + log "dnsmasq is already configured for sing-box" + return 0 + fi + log "Backup dnsmasq configuration" current_servers="$(uci_get "dhcp" "@dnsmasq[0]" "server")" if [ -n "$current_servers" ]; then @@ -929,7 +1066,7 @@ dnsmasq_configure() { uci_add_list "dhcp" "@dnsmasq[0]" "netshift_server" "$server" fi done - uci_remove "dhcp" "@dnsmasq[0]" "server" + uci_remove_quiet "dhcp" "@dnsmasq[0]" "server" fi backup_dnsmasq_config_option "noresolv" "netshift_noresolv" @@ -957,11 +1094,11 @@ dnsmasq_restore() { log "Restoring cachesize" "debug" cachesize="$(uci_get "dhcp" "@dnsmasq[0]" "netshift_cachesize")" if [ -z "$cachesize" ]; then - uci_remove "dhcp" "@dnsmasq[0]" "cachesize" + uci_remove_quiet "dhcp" "@dnsmasq[0]" "cachesize" uci_set "dhcp" "@dnsmasq[0]" "cachesize" 150 else uci_set "dhcp" "@dnsmasq[0]" "cachesize" "$cachesize" - uci_remove "dhcp" "@dnsmasq[0]" "netshift_cachesize" + uci_remove_quiet "dhcp" "@dnsmasq[0]" "netshift_cachesize" fi log "Restoring noresolv" "debug" @@ -970,18 +1107,18 @@ dnsmasq_restore() { uci_set "dhcp" "@dnsmasq[0]" "noresolv" 0 else uci_set "dhcp" "@dnsmasq[0]" "noresolv" "$noresolv" - uci_remove "dhcp" "@dnsmasq[0]" "netshift_noresolv" + uci_remove_quiet "dhcp" "@dnsmasq[0]" "netshift_noresolv" fi log "Restoring DNS servers" "debug" - uci_remove "dhcp" "@dnsmasq[0]" "server" + uci_remove_quiet "dhcp" "@dnsmasq[0]" "server" resolvfile="/tmp/resolv.conf.d/resolv.conf.auto" backup_servers="$(uci_get "dhcp" "@dnsmasq[0]" "netshift_server")" if [ -n "$backup_servers" ]; then for server in $backup_servers; do uci_add_list "dhcp" "@dnsmasq[0]" "server" "$server" done - uci_remove "dhcp" "@dnsmasq[0]" "netshift_server" + uci_remove_quiet "dhcp" "@dnsmasq[0]" "netshift_server" elif file_exists "$resolvfile"; then log "Backup DNS servers not found, using default resolvfile" "debug" uci_set "dhcp" "@dnsmasq[0]" "resolvfile" "$resolvfile" @@ -1399,6 +1536,18 @@ sing_box_configure_inbounds() { config=$( sing_box_cm_add_direct_inbound "$config" "$SB_DNS_INBOUND_TAG" "$SB_DNS_INBOUND_ADDRESS" "$SB_DNS_INBOUND_PORT" ) + if netshift_ipv6_enabled; then + config=$( + sing_box_cm_add_tproxy_inbound \ + "$config" "${SB_TPROXY_INBOUND_TAG}-v6" "$SB_TPROXY_INBOUND_ADDRESS_V6" "$SB_TPROXY_INBOUND_PORT_V6" true true + ) + # Local processes may query this inbound directly via [::1]:5354. + # dnsmasq intentionally keeps using 127.0.0.42:53: router clients cannot + # reach loopback ::1, and the IPv4 DNS inbound resolves AAAA records too. + config=$( + sing_box_cm_add_direct_inbound "$config" "${SB_DNS_INBOUND_TAG}-v6" "$SB_DNS_INBOUND_ADDRESS_V6" "$SB_DNS_INBOUND_PORT_V6" + ) + fi } sing_box_configure_outbounds() { @@ -1803,7 +1952,13 @@ configure_outbound_handler() { sing_box_configure_dns() { log "Configure the DNS section of a sing-box JSON configuration" - config=$(sing_box_cm_configure_dns "$config" "$SB_DNS_SERVER_TAG" "ipv4_only" true) + local dns_strategy + if netshift_ipv6_enabled; then + dns_strategy="prefer_ipv4" + else + dns_strategy="ipv4_only" + fi + config=$(sing_box_cm_configure_dns "$config" "$SB_DNS_SERVER_TAG" "$dns_strategy" true) log "Adding DNS Servers" "debug" local dns_type dns_server bootstrap_dns_server dns_domain_resolver dns_server_address @@ -1811,6 +1966,13 @@ sing_box_configure_dns() { config_get dns_server "settings" "dns_server" "1.1.1.1" config_get bootstrap_dns_server "settings" "bootstrap_dns_server" "77.88.8.8" + local block_doh + config_get_bool block_doh "settings" "block_doh" 0 + if [ "$block_doh" -eq 1 ] && [ "$dns_type" = "doh" ]; then + log "DoH blocking is enabled but upstream DNS type is 'doh'. Your own DNS queries will be blocked." "warn" + log "Switch dns_type to 'udp' or 'dot' in NetShift settings to avoid self-blocking." "warn" + fi + dns_server_address="$(url_get_host "$dns_server")" if ! is_ipv4 "$dns_server_address"; then dns_domain_resolver=$SB_BOOTSTRAP_SERVER_TAG @@ -1824,12 +1986,17 @@ sing_box_configure_dns() { config=$(sing_box_cm_add_udp_dns_server "$config" "$SB_BOOTSTRAP_SERVER_TAG" "$bootstrap_dns_server" 53) config=$(sing_box_cf_add_dns_server "$config" "$dns_type" "$SB_DNS_SERVER_TAG" "$dns_server" "$dns_domain_resolver" "$dns_detour_tag") - config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE") + if netshift_ipv6_enabled; then + config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE" "$SB_FAKEIP_INET6_RANGE") + else + config=$(sing_box_cm_add_fakeip_dns_server "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_INET4_RANGE" "") + fi log "Adding DNS Rules" local rewrite_ttl service_domains config_get rewrite_ttl "settings" "dns_rewrite_ttl" "60" + log "Adding DNS-level DoH prevention rules" "debug" config=$(sing_box_cm_add_dns_reject_rule "$config" "query_type" "HTTPS") config=$(sing_box_cm_add_dns_reject_rule "$config" "domain_suffix" '"use-application-dns.net"') config=$(sing_box_cm_add_dns_route_rule "$config" "$SB_FAKEIP_DNS_SERVER_TAG" "$SB_FAKEIP_DNS_RULE_TAG") @@ -1841,17 +2008,33 @@ sing_box_configure_dns() { sing_box_configure_route() { log "Configure the route section of a sing-box JSON configuration" + local route_final global_proxy_section + route_final="$SB_DIRECT_OUTBOUND_TAG" + global_proxy_section="$(get_global_proxy_section)" + if [ -n "$global_proxy_section" ]; then + if subscription_outbound_is_unavailable "$global_proxy_section"; then + log "Global proxy section '$global_proxy_section' is unavailable; routing unmatched traffic directly until it recovers" "warn" + else + route_final="$(get_outbound_tag_by_section "$global_proxy_section")" + log "Global proxy mode enabled: routing all unmatched traffic through section '$global_proxy_section' (outbound: $route_final)" "info" + fi + fi + local output_network_interface config_get output_network_interface "settings" "output_network_interface" if [ -z "$output_network_interface" ]; then - config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" true "$SB_DNS_SERVER_TAG") + config=$(sing_box_cm_configure_route "$config" "$route_final" true "$SB_DNS_SERVER_TAG") else - config=$(sing_box_cm_configure_route "$config" "$SB_DIRECT_OUTBOUND_TAG" false "$SB_DNS_SERVER_TAG" \ + config=$(sing_box_cm_configure_route "$config" "$route_final" false "$SB_DNS_SERVER_TAG" \ "$output_network_interface") fi - local sniff_inbounds - sniff_inbounds=$(comma_string_to_json_array "$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG") + local sniff_inbounds sniff_inbounds_csv + sniff_inbounds_csv="$SB_TPROXY_INBOUND_TAG,$SB_DNS_INBOUND_TAG" + if netshift_ipv6_enabled; then + sniff_inbounds_csv="$sniff_inbounds_csv,${SB_TPROXY_INBOUND_TAG}-v6,${SB_DNS_INBOUND_TAG}-v6" + fi + sniff_inbounds=$(comma_string_to_json_array "$sniff_inbounds_csv") config=$(sing_box_cm_sniff_route_rule "$config" "inbound" "$sniff_inbounds") config=$(sing_box_cm_add_hijack_dns_route_rule "$config" "protocol" "dns") @@ -1862,6 +2045,14 @@ sing_box_configure_route() { config=$(sing_box_cf_add_single_key_reject_rule "$config" "$SB_TPROXY_INBOUND_TAG" "protocol" "quic") fi + local block_doh + config_get_bool block_doh "settings" "block_doh" 0 + if [ "$block_doh" -eq 1 ]; then + log "DoH blocking enabled: adding route-level DoH IP block rules" "info" + config=$(sing_box_cm_add_doh_block_route_rule "$config" "$SB_DOH_BLOCK_RULE_TAG" "$SB_TPROXY_INBOUND_TAG" \ + "$DOH_BLOCK_IPV4_CIDRS" "$DOH_BLOCK_IPV6_CIDRS") + fi + local first_outbound_section first_outbound_section="$(get_first_outbound_section)" if subscription_outbound_is_unavailable "$first_outbound_section"; then @@ -1933,7 +2124,7 @@ configure_common_reject_route_rule() { } configure_common_direct_route_rule() { - local exclusion_sections exclusion_section_list_enabled + local exclusion_sections exclusion_section_list_enabled global_proxy_direct_needed exclusion_sections="$(get_sections_by_connection_type "exclusion")" exclusion_section_list_enabled=0 @@ -1944,19 +2135,35 @@ configure_common_direct_route_rule() { break fi done - if [ "$exclusion_section_list_enabled" -eq 1 ]; then - config=$(sing_box_cm_add_route_rule "$config" "$SB_EXCLUSION_RULE_TAG" "$SB_TPROXY_INBOUND_TAG" \ - "$SB_DIRECT_OUTBOUND_TAG") - else - log "Exclusion sections does not have any enabled list, route rule is not required" "warn" - fi + fi + + global_proxy_direct_needed=0 + if [ "$exclusion_section_list_enabled" -eq 0 ]; then + config_foreach _check_global_proxy_direct_needed "section" + fi + + if [ "$exclusion_section_list_enabled" -eq 1 ] || [ "$global_proxy_direct_needed" -eq 1 ]; then + config=$(sing_box_cm_add_route_rule "$config" "$SB_EXCLUSION_RULE_TAG" "$SB_TPROXY_INBOUND_TAG" \ + "$SB_DIRECT_OUTBOUND_TAG") + else + log "No direct exclusion rules required" "warn" + fi +} + +_check_global_proxy_direct_needed() { + local section="$1" + local global_proxy + + config_get_bool global_proxy "$section" "global_proxy" 0 + if [ "$global_proxy" -eq 1 ] && section_has_enabled_lists "$section"; then + global_proxy_direct_needed=1 fi } include_source_ip_in_routing_handler() { local source_ip="$1" local rule_tag="$2" - nft_list_all_traffic_from_ip "$source_ip" + config=$(sing_box_cm_patch_route_rule "$config" "$rule_tag" "source_ip_cidr" "$source_ip") } @@ -2008,7 +2215,7 @@ configure_routing_for_section_lists() { fi local community_lists user_domain_list_type user_subnet_list_type local_domain_lists local_subnet_lists \ - remote_domain_lists remote_subnet_lists section_connection_type route_rule_tag resolve_real_ip_for_routing + remote_domain_lists remote_subnet_lists section_connection_type route_rule_tag resolve_real_ip_for_routing outbound_tag config_get community_lists "$section" "community_lists" config_get user_domain_list_type "$section" "user_domain_list_type" "disabled" config_get user_subnet_list_type "$section" "user_subnet_list_type" "disabled" @@ -2021,12 +2228,18 @@ configure_routing_for_section_lists() { case "$section_connection_type" in proxy | vpn) - route_rule_tag="$(gen_id)" - if subscription_outbound_is_unavailable "$section"; then - config="$(sing_box_cm_add_reject_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG")" + local global_proxy + config_get_bool global_proxy "$section" "global_proxy" 0 + if [ "$global_proxy" -eq 1 ]; then + route_rule_tag="$SB_EXCLUSION_RULE_TAG" else - outbound_tag=$(get_outbound_tag_by_section "$section") - config=$(sing_box_cm_add_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag") + route_rule_tag="$(gen_id)" + if subscription_outbound_is_unavailable "$section"; then + config="$(sing_box_cm_add_reject_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG")" + else + outbound_tag=$(get_outbound_tag_by_section "$section") + config=$(sing_box_cm_add_route_rule "$config" "$route_rule_tag" "$SB_TPROXY_INBOUND_TAG" "$outbound_tag") + fi fi ;; block) @@ -2721,6 +2934,24 @@ get_first_outbound_section() { echo "$first_section" } +_determine_global_proxy_section() { + local section="$1" + local global_proxy + + config_get_bool global_proxy "$section" "global_proxy" 0 + if [ "$global_proxy" -eq 1 ] && section_has_configured_outbound "$section"; then + [ -z "$global_section" ] && global_section="$section" + fi +} + +get_global_proxy_section() { + local global_section="" + + config_foreach _determine_global_proxy_section "section" + + echo "$global_section" +} + get_sections_by_connection_type() { local connection_type="$1" @@ -2784,22 +3015,63 @@ get_service_listen_address() { echo "$service_listen_address" } -## nftables -nft_list_all_traffic_from_ip() { - local ip="$1" +# Diagnostics +sing_box_fetch_ifconfig_with_timeout() { + local config_dir="$1" + local config_path="$2" + local outbound_tag="$3" + local output_file fetch_pid watchdog_pid fetch_status - if ! nft list chain inet "$NFT_TABLE_NAME" mangle | grep -q "ip saddr $ip"; then - nft insert rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip saddr "$ip" \ - meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter - nft insert rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip saddr "$ip" \ - meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter - nft insert rule inet "$NFT_TABLE_NAME" mangle ip saddr "$ip" ip daddr @localv4 return - fi + output_file="$(mktemp /tmp/netshift-check-proxy-response.XXXXXX)" || return 1 + + sing-box tools fetch -D "$config_dir" -c "$config_path" --outbound "$outbound_tag" ifconfig.me \ + > "$output_file" 2>/dev/null & + fetch_pid=$! + + ( + sleep 20 + kill "$fetch_pid" 2>/dev/null + ) & + watchdog_pid=$! + + wait "$fetch_pid" 2>/dev/null + fetch_status=$? + + kill "$watchdog_pid" 2>/dev/null + wait "$watchdog_pid" 2>/dev/null + + cat "$output_file" + rm -f "$output_file" + return "$fetch_status" +} + +get_clash_selected_outbound_tag() { + local outbound_tag="$1" + local controller_address encoded_tag selected_tag i + + controller_address="$(get_service_listen_address 2>/dev/null)" + [ -z "$controller_address" ] && controller_address="127.0.0.1" + + i=0 + while [ "$i" -lt 5 ]; do + encoded_tag="$(printf '%s' "$outbound_tag" | jq -sRr @uri 2>/dev/null)" + selected_tag="$(curl -m 3 -s "http://$controller_address:$SB_CLASH_API_CONTROLLER_PORT/proxies/$encoded_tag" \ + | jq -r '.now // ""' 2>/dev/null)" + + if [ -z "$selected_tag" ] || [ "$selected_tag" = "$outbound_tag" ]; then + echo "$outbound_tag" + return 0 + fi + + outbound_tag="$selected_tag" + i=$((i + 1)) + done + + echo "$outbound_tag" } -# Diagnotics check_proxy() { - local sing_box_config_path + local sing_box_config_path check_config_path config_dir first_outbound_section outbound_tag response ip config_get sing_box_config_path "settings" "config_path" if ! command -v sing-box > /dev/null 2>&1; then @@ -2846,30 +3118,87 @@ check_proxy() { nolog "Checking proxy connection..." + first_outbound_section="$(get_first_outbound_section)" + if [ -z "$first_outbound_section" ]; then + nolog "No outbound section configured" + return 1 + fi + + outbound_tag="$(get_outbound_tag_by_section "$first_outbound_section")" + config_dir="${sing_box_config_path%/*}" + [ "$config_dir" = "$sing_box_config_path" ] && config_dir="." + + check_config_path="$(mktemp /tmp/netshift-check-proxy.XXXXXX)" || { + nolog "Failed to create temporary sing-box configuration" + return 1 + } + + if ! jq ' + if .experimental.cache_file then .experimental.cache_file.enabled = false else . end + | .dns.rules = [] + | .route.rules = [] + | .route.rule_set = [] + ' "$sing_box_config_path" > "$check_config_path"; then + rm -f "$check_config_path" + nolog "Failed to prepare temporary sing-box configuration" + return 1 + fi + + if ! jq -e --arg tag "$outbound_tag" '.outbounds[]? | select(.tag == $tag)' \ + "$check_config_path" > /dev/null; then + rm -f "$check_config_path" + nolog "Outbound '$outbound_tag' not found in sing-box configuration" + return 1 + fi + + if jq -e --arg tag "$outbound_tag" \ + '.outbounds[]? | select(.tag == $tag and (.type == "selector" or .type == "urltest"))' \ + "$check_config_path" > /dev/null; then + outbound_tag="$(get_clash_selected_outbound_tag "$outbound_tag")" + fi + + if [ -z "$outbound_tag" ]; then + rm -f "$check_config_path" + nolog "No testable proxy outbound found in sing-box configuration" + return 1 + fi + for attempt in $(seq 1 5); do - response=$(sing-box tools fetch ifconfig.me -D /etc/sing-box 2> /dev/null) + response="$(sing_box_fetch_ifconfig_with_timeout "$config_dir" "$check_config_path" "$outbound_tag")" if echo "$response" | grep -q "^<html\|403 Forbidden"; then + if [ "$attempt" -eq 5 ]; then + nolog "Failed to get valid IP address after 5 attempts" + nolog "Error response: $response" + rm -f "$check_config_path" + return 1 + fi continue fi if echo "$response" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then - ip=$(echo "$response" | sed -n 's/^[0-9]\+\.[0-9]\+\.[0-9]\+\.\([0-9]\+\)$/X.X.X.\1/p') + ip="$(echo "$response" | sed -n 's/^[0-9]\+\.[0-9]\+\.[0-9]\+\.\([0-9]\+\)$/X.X.X.\1/p')" nolog "$ip - should match proxy IP" + rm -f "$check_config_path" return 0 - elif echo "$response" | grep -q "^[0-9a-fA-F:]*::[0-9a-fA-F:]*$\|^[0-9a-fA-F:]\+$"; then - ip=$(echo "$response" | sed 's/\([0-9a-fA-F]\+:[0-9a-fA-F]\+:[0-9a-fA-F]\+\):.*/\1:XXXX:XXXX:XXXX/') + elif echo "$response" | grep -Eq '^([0-9a-fA-F]*:)[0-9a-fA-F:]+$'; then + ip="$(echo "$response" | sed 's/\([0-9a-fA-F]\+:[0-9a-fA-F]\+:[0-9a-fA-F]\+\):.*/\1:XXXX:XXXX:XXXX/')" nolog "$ip - should match proxy IP" + rm -f "$check_config_path" return 0 fi - if [ $attempt -eq 5 ]; then + if [ "$attempt" -eq 5 ]; then nolog "Failed to get valid IP address after 5 attempts" if [ -z "$response" ]; then nolog "Error: Empty response" else nolog "Error response: $response" fi + rm -f "$check_config_path" return 1 fi done + + rm -f "$check_config_path" + return 1 } check_nft() { @@ -3374,7 +3703,7 @@ check_sing_box() { fi # Check if process is running - if pgrep "sing-box" > /dev/null 2>&1; then + if sing_box_process_exists; then sing_box_process_running=1 fi @@ -3399,7 +3728,15 @@ check_sing_box() { } check_fakeip() { - curl -m 3 -s "https://$FAKEIP_TEST_DOMAIN/check" | jq . + local fakeip_address + + fakeip_address="$(dig +short @"$SB_DNS_INBOUND_ADDRESS" "$FAKEIP_TEST_DOMAIN" 2>/dev/null | sed -n '1p')" + if echo "$fakeip_address" | grep -q '^198\.18\.'; then + jq -n --arg ip "$fakeip_address" '{fakeip: true, IP: $ip}' + return 0 + fi + + jq -n --arg ip "$fakeip_address" '{fakeip: false, IP: $ip}' } ####################################### diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 1d30daaa..2aae11d1 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -30,6 +30,7 @@ RT_TABLE_NAME="netshift" ## nft NFT_TABLE_NAME="NetShiftTable" NFT_LOCALV4_SET_NAME="localv4" +NFT_LOCALV6_SET_NAME="localv6" NFT_COMMON_SET_NAME="netshift_subnets" NFT_DISCORD_SET_NAME="netshift_discord_subnets" NFT_INTERFACE_SET_NAME="interfaces" @@ -38,6 +39,11 @@ NFT_OUTBOUND_MARK="0x00200000" ## sing-box SB_REQUIRED_VERSION="1.12.0" +# Monitoring +MONITOR_CHECK_INTERVAL=10 +MONITOR_MAX_CRASHES=5 +MONITOR_BACKOFF_BASE=10 +MONITOR_BACKOFF_MAX=300 # Core-switch connectivity self-heal (task-009). Hosts probed before a core # swap, depending on direction: the stable (stock) install pulls from the # OpenWrt package feeds, the extended install pulls from the GitHub API. @@ -57,6 +63,7 @@ UPDATES_LIBCRONET_LIB="/usr/lib/libcronet.so" SB_DNS_SERVER_TAG="dns-server" SB_FAKEIP_DNS_SERVER_TAG="fakeip-server" SB_FAKEIP_INET4_RANGE="198.18.0.0/15" +SB_FAKEIP_INET6_RANGE="fd00:ec3a::/32" SB_BOOTSTRAP_SERVER_TAG="bootstrap-dns-server" SB_FAKEIP_DNS_RULE_TAG="fakeip-dns-rule-tag" SB_INVERT_FAKEIP_DNS_RULE_TAG="invert-fakeip-dns-rule-tag" @@ -64,9 +71,13 @@ SB_INVERT_FAKEIP_DNS_RULE_TAG="invert-fakeip-dns-rule-tag" SB_TPROXY_INBOUND_TAG="tproxy-in" SB_TPROXY_INBOUND_ADDRESS="127.0.0.1" SB_TPROXY_INBOUND_PORT=1602 +SB_TPROXY_INBOUND_ADDRESS_V6="::1" +SB_TPROXY_INBOUND_PORT_V6=1603 SB_DNS_INBOUND_TAG="dns-in" SB_DNS_INBOUND_ADDRESS="127.0.0.42" SB_DNS_INBOUND_PORT=53 +SB_DNS_INBOUND_ADDRESS_V6="::1" +SB_DNS_INBOUND_PORT_V6=5354 SB_SERVICE_MIXED_INBOUND_TAG="service-mixed-in" SB_SERVICE_MIXED_INBOUND_ADDRESS="127.0.0.1" SB_SERVICE_MIXED_INBOUND_PORT=4534 @@ -75,9 +86,14 @@ SB_DIRECT_OUTBOUND_TAG="direct-out" # Route SB_REJECT_RULE_TAG="reject-rule-tag" SB_EXCLUSION_RULE_TAG="exclusion-rule-tag" +SB_DOH_BLOCK_RULE_TAG="doh-block-rule-tag" # Experimental SB_CLASH_API_CONTROLLER_PORT=9090 +## DoH blocking +DOH_BLOCK_IPV4_CIDRS="1.1.1.1/32 1.0.0.1/32 8.8.8.8/32 8.8.4.4/32 9.9.9.9/32 9.9.9.11/32 149.112.112.112/32 208.67.222.222/32 208.67.220.220/32 94.140.14.14/32 94.140.15.15/32 77.88.8.8/32 77.88.8.1/32" +DOH_BLOCK_IPV6_CIDRS="2606:4700:4700::1111/128 2606:4700:4700::1001/128 2001:4860:4860::8888/128 2001:4860:4860::8844/128 2620:fe::fe/128 2620:fe::9/128 2620:119:35::35/128 2620:119:53::53/128 2a10:50c0::ad1:ff/128 2a10:50c0::ad2:ff/128 2a02:6b8::feed:0ff/128 2a02:6b8:0:1::feed:0ff/128" + ## Lists GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main" SRS_MAIN_URL="https://github.com/itdoginfo/allow-domains/releases/latest/download" @@ -91,4 +107,12 @@ SUBNETS_HETZNER="${GITHUB_RAW_URL}/Subnets/IPv4/hetzner.lst" SUBNETS_OVH="${GITHUB_RAW_URL}/Subnets/IPv4/ovh.lst" SUBNETS_DIGITALOCEAN="${GITHUB_RAW_URL}/Subnets/IPv4/digitalocean.lst" SUBNETS_CLOUDFRONT="${GITHUB_RAW_URL}/Subnets/IPv4/cloudfront.lst" +SUBNETS_TWITTER_V6="${GITHUB_RAW_URL}/Subnets/IPv6/twitter.lst" +SUBNETS_META_V6="${GITHUB_RAW_URL}/Subnets/IPv6/meta.lst" +SUBNETS_DISCORD_V6="${GITHUB_RAW_URL}/Subnets/IPv6/discord.lst" +SUBNETS_CLOUDFLARE_V6="${GITHUB_RAW_URL}/Subnets/IPv6/cloudflare.lst" +SUBNETS_HETZNER_V6="${GITHUB_RAW_URL}/Subnets/IPv6/hetzner.lst" +SUBNETS_OVH_V6="${GITHUB_RAW_URL}/Subnets/IPv6/ovh.lst" +SUBNETS_DIGITALOCEAN_V6="${GITHUB_RAW_URL}/Subnets/IPv6/digitalocean.lst" +SUBNETS_CLOUDFRONT_V6="${GITHUB_RAW_URL}/Subnets/IPv6/cloudfront.lst" COMMUNITY_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube hdrezka tiktok google_ai google_play hodca discord meta twitter cloudflare cloudfront digitalocean hetzner ovh telegram roblox" diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index 86ecafbf..30123b6c 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -13,6 +13,30 @@ is_ipv4_cidr() { echo "$ip" | grep -Eq "$regex" } +is_ipv6() { + local ip="$1" + local regex='^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$' + echo "$ip" | grep -Eq "$regex" +} + +is_ipv6_cidr() { + local ip="$1" + local addr mask + addr="${ip%/*}" + mask="${ip#*/}" + + case "$ip" in + */*) ;; + *) return 1 ;; + esac + + is_ipv6 "$addr" && [ "$mask" -ge 0 ] 2>/dev/null && [ "$mask" -le 128 ] 2>/dev/null +} + +is_ip() { + is_ipv4 "$1" || is_ipv6 "$1" +} + is_ipv4_ip_or_ipv4_cidr() { is_ipv4 "$1" || is_ipv4_cidr "$1" } @@ -147,7 +171,11 @@ url_get_host() { url="${url#*@}" url="${url%%[/?#]*}" - echo "${url%%:*}" + case "$url" in + \[*\]) echo "${url#\[}" | sed 's/\]$//' ;; + \[*\]*) echo "${url#\[}" | sed 's/\].*//' ;; + *) echo "${url%%:*}" ;; + esac } # Extracts the port number from a URL @@ -159,6 +187,7 @@ url_get_port() { url="${url%%[/?#]*}" case "$url" in + \[*\]:*) echo "${url##*]:}" ;; *:*) echo "${url#*:}" ;; *) echo "" ;; esac diff --git a/netshift/files/usr/lib/nft.sh b/netshift/files/usr/lib/nft.sh index a9124653..06a6d4e7 100644 --- a/netshift/files/usr/lib/nft.sh +++ b/netshift/files/usr/lib/nft.sh @@ -14,6 +14,13 @@ nft_create_ipv4_set() { nft add set inet "$table" "$name" '{ type ipv4_addr; flags interval; auto-merge; }' } +nft_create_ipv6_set() { + local table="$1" + local name="$2" + + nft add set inet "$table" "$name" '{ type ipv6_addr; flags interval; auto-merge; }' +} + nft_create_ifname_set() { local table="$1" local name="$2" @@ -68,4 +75,4 @@ nft_add_set_elements_from_file_chunked() { log "Adding $count elements to nft set $nft_set_name" "debug" nft_add_set_elements "$nft_table_name" "$nft_set_name" "$array" fi -} \ No newline at end of file +} diff --git a/netshift/files/usr/lib/sing_box_config_manager.sh b/netshift/files/usr/lib/sing_box_config_manager.sh index 751c7a06..1e4c2c9d 100644 --- a/netshift/files/usr/lib/sing_box_config_manager.sh +++ b/netshift/files/usr/lib/sing_box_config_manager.sh @@ -223,15 +223,20 @@ sing_box_cm_add_fakeip_dns_server() { local config="$1" local tag="$2" local inet4_range="$3" + local inet6_range="$4" echo "$config" | jq \ --arg tag "$tag" \ --arg inet4_range "$inet4_range" \ - '.dns.servers += [{ - type: "fakeip", - tag: $tag, - inet4_range: $inet4_range, - }]' + --arg inet6_range "$inet6_range" \ + '.dns.servers += [( + { + type: "fakeip", + tag: $tag, + inet4_range: $inet4_range + } + + (if $inet6_range != "" then { inet6_range: $inet6_range } else {} end) + )]' } ####################################### @@ -1353,6 +1358,51 @@ sing_box_cm_add_reject_route_rule() { }]' } +####################################### +# Add a DoH blocking reject route rule with an inline ruleset containing known +# public DoH server IP ranges. +# Arguments: +# config: string (JSON), sing-box configuration to modify +# tag: string, identifier for the route rule and ruleset +# inbound: string, inbound tag to match +# doh_ipv4_cidrs: string, space-separated IPv4 CIDRs to block +# doh_ipv6_cidrs: string, space-separated IPv6 CIDRs to block +# Outputs: +# Writes updated JSON configuration to stdout +####################################### +sing_box_cm_add_doh_block_route_rule() { + local config="$1" + local tag="$2" + local inbound="$3" + local doh_ipv4_cidrs="$4" + local doh_ipv6_cidrs="${5:-}" + + local ruleset_tag cidrs_json + ruleset_tag="${tag}-ruleset" + cidrs_json=$(printf '%s %s' "$doh_ipv4_cidrs" "$doh_ipv6_cidrs" | jq -R 'split(" ") | map(select(. != ""))') + + config=$(echo "$config" | jq \ + --arg tag "$ruleset_tag" \ + --argjson ip_cidr "$cidrs_json" \ + '.route.rule_set += [{ + type: "inline", + tag: $tag, + rules: [{ ip_cidr: $ip_cidr }] + }]') + + echo "$config" | jq \ + --arg service_tag "$SERVICE_TAG" \ + --arg tag "$tag" \ + --arg inbound "$inbound" \ + --arg ruleset_tag "$ruleset_tag" \ + '.route.rules += [{ + action: "reject", + inbound: $inbound, + rule_set: $ruleset_tag, + $service_tag: $tag + }]' +} + ####################################### # Add a hijack-dns rule to the route section of a sing-box JSON configuration. # Arguments: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 8cde353f..1fa815c5 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -387,7 +387,7 @@ test_sing_box_config() { # Test with inline ruleset (DoH blocking) jq '.route.rule_set += [{ type: "inline", tag: "doh-block", - rules: [{ ip_cidr: ["1.1.1.1/32", "8.8.8.8/32"] }] + rules: [{ ip_cidr: ["1.1.1.1/32", "8.8.8.8/32", "2606:4700:4700::1111/128", "2001:4860:4860::8888/128"] }] }]' "$test_config" > "${test_config}.4" if sing-box -c "${test_config}.4" check > /dev/null 2>&1; then @@ -682,6 +682,12 @@ out2=$(sing_box_cm_add_vmess_outbound "$base_config" "vmess-out" "example.com" " echo "$out2" | jq -e '.outbounds[0].security == "aes-128-gcm"' >/dev/null 2>&1 && echo 'cm-vmess-security-explicit:OK' || echo 'cm-vmess-security-explicit:FAIL' echo "$out2" | jq -e '.outbounds[0].alter_id == 64' >/dev/null 2>&1 && echo 'cm-vmess-aid-number:OK' || echo 'cm-vmess-aid-number:FAIL' +doh_cfg='{"route":{"rules":[],"rule_set":[]}}' +doh_out=$(sing_box_cm_add_doh_block_route_rule "$doh_cfg" "doh-block" "tproxy-in" \ + "1.1.1.1/32 8.8.8.8/32" "2606:4700:4700::1111/128 2001:4860:4860::8888/128") +echo "$doh_out" | jq -e '.route.rule_set[0].rules[0].ip_cidr | index("1.1.1.1/32") and index("2606:4700:4700::1111/128")' >/dev/null 2>&1 && echo 'cm-doh-cidrs-v4-v6:OK' || echo 'cm-doh-cidrs-v4-v6:FAIL' +echo "$doh_out" | jq -e '.route.rules[0].action == "reject" and .route.rules[0].rule_set == "doh-block-ruleset" and .route.rules[0].inbound == "tproxy-in"' >/dev/null 2>&1 && echo 'cm-doh-route-rule:OK' || echo 'cm-doh-route-rule:FAIL' + echo 'DONE' CMEOF sed -i "s|CM_LIB_PATH|$cm_lib|" "$cm_tmp" @@ -2384,6 +2390,114 @@ DDEOF rm -f "$drv" } +# ───────────────────────────────────────────────────────────────── +# Test: global_proxy route rule semantics +# ───────────────────────────────────────────────────────────────── +test_global_proxy() { + header "Global Proxy Route Semantics" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local cm_lib="${NETSHIFT_LIB_DIR}/sing_box_config_manager.sh" + local constants_lib="${NETSHIFT_LIB_DIR}/constants.sh" + local jq_helpers="${NETSHIFT_LIB_DIR}/helpers.jq" + if [ ! -r "$cm_lib" ] || [ ! -r "$constants_lib" ] || [ ! -r "$jq_helpers" ]; then + skip "config manager / constants / helpers.jq not found" + return + fi + + # sing_box_cm_patch_route_rule imports helpers.jq from the runtime path. + mkdir -p /usr/lib/netshift + ln -sf "$jq_helpers" /usr/lib/netshift/helpers.jq + + local cfg tmp + tmp="/tmp/netshift-global-proxy-$$.json" + + . "$constants_lib" + . "$cm_lib" + + local global_out ruleset_tag ipv6_excluded_rule_tag + global_out="global-out" + ruleset_tag="global-user-domains" + ipv6_excluded_rule_tag="global-ipv6-excluded" + + cfg=$(jq -n \ + --arg direct "$SB_DIRECT_OUTBOUND_TAG" \ + --arg global "$global_out" \ + --arg tproxy "$SB_TPROXY_INBOUND_TAG" \ + --arg listen "$SB_TPROXY_INBOUND_ADDRESS" \ + --argjson port "$SB_TPROXY_INBOUND_PORT" \ + --arg ruleset "$ruleset_tag" \ + '{ + log: { disabled: false, level: "warn", timestamp: true }, + dns: { servers: [], rules: [], final: $direct, strategy: "prefer_ipv4", independent_cache: true }, + ntp: {}, + inbounds: [ + { type: "tproxy", tag: $tproxy, listen: $listen, listen_port: $port } + ], + outbounds: [ + { type: "direct", tag: $direct }, + { type: "direct", tag: $global } + ], + route: { + rules: [], + rule_set: [{ type: "inline", tag: $ruleset, rules: [{ domain_suffix: ["example.com"] }] }], + final: $global, + auto_detect_interface: true + } + }') + + cfg=$(sing_box_cm_add_route_rule "$cfg" "$SB_EXCLUSION_RULE_TAG" "$SB_TPROXY_INBOUND_TAG" "$SB_DIRECT_OUTBOUND_TAG") + cfg=$(sing_box_cm_patch_route_rule "$cfg" "$SB_EXCLUSION_RULE_TAG" "rule_set" "$ruleset_tag") + cfg=$(sing_box_cm_add_route_rule "$cfg" "$ipv6_excluded_rule_tag" "$SB_TPROXY_INBOUND_TAG" "$SB_DIRECT_OUTBOUND_TAG") + cfg=$(sing_box_cm_patch_route_rule "$cfg" "$ipv6_excluded_rule_tag" "source_ip_cidr" "fd00:ec3a::123/128") + + if echo "$cfg" | jq -e --arg global "$global_out" '.route.final == $global' > /dev/null 2>&1; then + pass "global_proxy route.final points to global-out" + else + fail "global_proxy route.final is not global-out" "$(echo "$cfg" | jq -r '.route.final // "missing"' 2>/dev/null)" + fi + + if echo "$cfg" | jq -e --arg tag "$SB_EXCLUSION_RULE_TAG" \ + '[.route.rules[] | select(.__service_tag == $tag and (has("rule_set") | not))] | length == 0' \ + > /dev/null 2>&1; then + pass "global_proxy exclusion route rule is constrained by rule_set" + else + fail "global_proxy exclusion route rule lacks rule_set" "$(echo "$cfg" | jq -c '.route.rules' 2>/dev/null)" + fi + + if echo "$cfg" | jq -e --arg tag "$SB_EXCLUSION_RULE_TAG" --arg direct "$SB_DIRECT_OUTBOUND_TAG" --arg ruleset "$ruleset_tag" \ + '[.route.rules[] | select(.__service_tag == $tag and .outbound == $direct and .rule_set == $ruleset)] | length == 1' \ + > /dev/null 2>&1; then + pass "global_proxy exclusion rule routes global-user-domains direct-out" + else + fail "global_proxy exclusion direct rule shape wrong" "$(echo "$cfg" | jq -c '.route.rules' 2>/dev/null)" + fi + + if echo "$cfg" | jq -e --arg tag "$ipv6_excluded_rule_tag" --arg direct "$SB_DIRECT_OUTBOUND_TAG" \ + '[.route.rules[] | select(.__service_tag == $tag and .outbound == $direct and .source_ip_cidr == "fd00:ec3a::123/128")] | length == 1' \ + > /dev/null 2>&1; then + pass "global_proxy routing_excluded_ips supports IPv6 source_ip_cidr" + else + fail "global_proxy IPv6 source_ip_cidr rule shape wrong" "$(echo "$cfg" | jq -c '.route.rules' 2>/dev/null)" + fi + + if command -v sing-box > /dev/null 2>&1; then + sing_box_cm_save_config_to_file "$cfg" "$tmp" + if sing-box -c "$tmp" check > /dev/null 2>&1; then + pass "sing-box validates global_proxy route config" + else + fail "sing-box rejects global_proxy route config" "$(sing-box -c "$tmp" check 2>&1)" + fi + rm -f "$tmp" + else + skip "sing-box not installed — skipping global_proxy config check" + fi +} + # ───────────────────────────────────────────────────────────────── # Main # ───────────────────────────────────────────────────────────────── @@ -2412,6 +2526,7 @@ main() { test_jobstate test_selfheal test_dns_via_outbound + test_global_proxy ;; deps) test_deps ;; syntax) test_syntax ;; @@ -2424,12 +2539,13 @@ main() { jobstate) test_jobstate ;; selfheal) test_selfheal ;; dnsdetour) test_dns_via_outbound ;; + globalproxy) test_global_proxy ;; jq) test_jq_helpers ;; cm) test_config_manager ;; sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription rejected jobstate selfheal dnsdetour" + echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription rejected jobstate selfheal dnsdetour globalproxy" exit 1 ;; esac From d391e32f4f51177090fb6603cd11f9aaf8f249ac Mon Sep 17 00:00:00 2001 From: "spgsroot, yandexru45" <> Date: Sat, 6 Jun 2026 21:24:37 +0300 Subject: [PATCH 54/75] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=D1=8B=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=B3=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 87 ++++++++ .../memory/luci-frontend-developer.md | 39 ++++ .../memory/packaging-ci-engineer.md | 32 +++ .../memory/shell-backend-developer.md | 78 ++++++++ fe-app-netshift/locales/calls.json | 189 ++++++++++++------ fe-app-netshift/locales/netshift.pot | 165 +++++++++------ fe-app-netshift/locales/netshift.ru.po | 34 +++- .../src/validators/tests/validateDns.test.js | 2 + .../src/validators/tests/validateIp.test.js | 10 + .../validators/tests/validateSubnet.test.js | 2 + fe-app-netshift/src/validators/validateIp.ts | 88 +++++++- .../resources/view/netshift/main.js | 59 +++++- .../resources/view/netshift/section.js | 15 +- .../resources/view/netshift/settings.js | 24 ++- luci-app-netshift/po/ru/netshift.po | 34 +++- luci-app-netshift/po/templates/netshift.pot | 165 +++++++++------ netshift/files/usr/bin/netshift | 189 +++++++----------- netshift/files/usr/lib/constants.sh | 9 - netshift/files/usr/lib/helpers.sh | 24 --- tests/docker-compose.yml | 4 +- tests/entrypoint.sh | 104 +++++++++- 21 files changed, 987 insertions(+), 366 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 74f0268b..817f29db 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -266,3 +266,90 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> - UI: two `form.DynamicList` in `section.js` after `subscription_group_by_countries`, rmempty=true, NO validator (keep emoji/space verbatim); `string[]?` fields on `ConfigProxySubscriptionSection` in types.ts; ru/en via locale tooling. + +## PR review workflow + PR #11 findings (review-001, 2026-06-06) + +- Reviewing an external PR (no `gh` CLI installed): fetch via API + `curl https://api.github.com/repos/yandexru45/netshift/pulls/N` (meta), + `.../files` (per-file stats), and `-H "Accept: application/vnd.github.v3.diff"` + for the raw diff. Then `git fetch origin pull/N/head:pr-N` to get a local ref + diffable vs `main`. Workspace `.pr-review/` + `*.txt` are gitignored (untracked). +- Decompose review by LAYER (backend / frontend+i18n / tests-packaging) into + separate diff txt files; launch one `explore` subagent per layer IN PARALLEL + (layers don't share files), then consolidate with the formal `code-reviewer`. + Give each subagent an architect "systemic notes" file of HYPOTHESES to verify. +- **nftables landmine (VERIFIED on nft v1.1.3):** `tproxy ip6 to <addr>:<port>` + REQUIRES bracketed `[addr]:port`. The unbracketed form (e.g. `::1:1603`) PASSES + `nft -c` AND `sing-box check`, but nft normalizes it to a BARE address with NO + port (`::1:1603` -> `[::0.1.22.3]`). Only on-device / `unshare -rn nft -f` + + `nft list ruleset` reveals it. IPv4 `addr:port` is fine unbracketed; v6 is not. +- **Local nft verification trick (no root):** `unshare -rn nft -c -f file` / + `unshare -rn sh -c 'nft -f f && nft list ruleset'` gives netlink in a private + netns so you can load+inspect normalized rules. Plain `nft -c` fails with + "cache initialization failed: Operation not permitted" without it. +- PR #11 ("Синхронизация с netshift", spgsroot, +2314/-1364, 23 files) verdict: + **REQUIRES CHANGES**. Doc at `.pr-review/REVIEW-pr-11.md` (canonical copy would + be `docs/tasks/sync-netshift-review-001.md`). Headline = IPv6 + DoH-block + + global_proxy + sing-box health monitor + check_proxy rework. + * BLOCKER B-01: unbracketed v6 tproxy rule (above). + * Majors: nft model shift (mangle now marks ALL interface traffic, split moved + to sing-box route rules) — `mangle_output` lost router-originated @common/ + fakeip marking (regression); `@netshift_subnets`/@common still populated each + `list_update` but matched by NO rule (dead import path); 8x `SUBNETS_*_V6` + dead constants; `start()` spawns `monitor_sing_box` with no pidfile+kill-0 + guard (orphan leak); over-permissive `validateIPV6` regex (accepts `:::`, + `1::2::3`, etc.) shared by subnet+dns validators, no negative tests; 3 new + flag descriptions concat'd inside `_()` -> ship untranslated. + * GOOD: generated `main.js` is a faithful DRIFT-FREE rebuild (CI no-diff should + pass); NO Oniguruma jq; UTF-8 emoji intact; i18n catalogs machine-consistent. + * Coverage gap: the nft model shift has NO smoke test (test_global_proxy only + checks sing-box route-rule SHAPE; test_nft byte-identical to base) — that's + why B-01 slipped. Any nft-rule PR should add an `nft list ruleset` assertion. + +## PR #11 fix-to-perfect cycle (2026-06-06, after operator merged the PR) + +- Operator merged PR #11 to main, then asked to fix everything to perfection. + Decomposed the review-doc issues into 3 task specs (docs/tasks/task-014 backend, + -015 frontend, -016 packaging) + delegated to the 3 dev subagents, ran the + dev<->code-reviewer loop per layer until all APPROVED. NOTE: `docs/tasks/` is + gitignored (line 7 `docs/tasks`), so task specs are session artifacts (like + .pr-review/), not committed — that's by project design (only TEMPLATE-*.md are + force-tracked). +- Operator design decisions for the nft model shift: B-02=A (router-originated + traffic stays DIRECT in the new mark-everything-in-prerouting model; document + only, don't restore mangle_output marking) and B-03/B-04=A (remove the dead + @netshift_subnets populate path + dead SUBNETS_*_V6). Rule: dead-code removal + for a SET requires first PROVING every populated source is carried by a sing-box + rule_set; the dev produced a coverage map (community->$SRS_MAIN_URL/<svc>.srs, + user/local/remote subnets->rule_sets). DISCORD set is RETAINED (it has a + dport-restricted mangle rule `udp dport {19000-20000,50000-65535}` that a + sing-box route rule cannot express). M1/M2 left as non-blocking follow-ups + (orphaned rulesets.sh helpers + unused IPv4 SUBNETS_* under SC2034). +- **dnsmasq "we-own-it" guard landmine (B-08):** a guard that infers netshift + ownership from the PRESENCE of `netshift_*` BACKUP markers is WRONG, because + `backup_dnsmasq_config_option` only writes a marker when the ORIGINAL value was + non-empty. On stock/default dnsmasq (empty server/noresolv/cachesize) NO markers + exist, so on the redundant `dnsmasq_configure force` path (monitor recovery / + double-start) the guard flips false, re-runs backup, and records netshift's OWN + live values (noresolv=1/cachesize=0) as the "backup" -> restore later sets + noresolv=1/cachesize=0 instead of defaults 0/150 -> router DNS broken after stop. + FIX: an explicit unconditional sentinel `netshift_configured=1` set in + dnsmasq_configure, gating the short-circuit, cleared in dnsmasq_restore. +- **nft v6 NEGATIVE-guard test landmine:** the buggy unbracketed `::1:1603` + normalizes DIFFERENTLY per nft build: `[::0.1.22.3]` on nftables v1.1.3 (WSL), + but `[::1:1603]` on OpenWRT 24.10.6's nft (the smoke container). So a negative + grep for `::0` OR even `\[::0` is a DEAD always-passing assertion in the smoke + env. ROBUST pattern: `grep 'tproxy ip6 to \[' | grep -qv '\[::1\]:1603'` (any + bracketed dest that ISN'T the correct one). Always self-prove a regression guard + by temporarily reintroducing the bug and confirming the test FAILS. +- Environment (WSL2 Debian 12): Docker daemon socket-activation can leave a + self-referential symlink (`/var/run/docker.sock -> /run/docker.sock` where + /var/run IS /run); fix = `sudo rm -f /run/docker.sock; sudo systemctl restart + docker.socket docker.service`. shellcheck not installed -> grab the static + binary to ~/.local/bin (koalaman release tar.xz). yarn is classic 1.22.x via + corepack (safe, no yarn.lock migration); deps install clean with --frozen-lockfile. +- FINAL integrated gates after the cycle: shellcheck (error) clean; yarn ci 439 + tests pass (was 395) + main.js idempotent rebuild (two builds byte-identical); + smoke `all` = 84 passed / 0 failed (was 81; +3 nft v6 regression assertions); + whole-chain `unshare -rn` confirms v6 tproxy normalizes to [::1]:1603. All 3 + layers code-reviewer APPROVED. Ready for human commit (agents never auto-commit). diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 2ac6cab3..53b1befa 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -223,3 +223,42 @@ append findings; keep under ~200 lines. DNS через прокси/VPN"; "DNS outbound section"→"Секция outbound для DNS"; "Main DNS via outbound"→"Основной DNS через outbound"; long descriptions translated equivalently. + +## validateIPV6 rewrite + concat-_() i18n (task-015 PR#11 fixes) + +- The OLD `validateIPV6` (two loose regexes + `colons>=2&&<=7` guard) WRONGLY + accepted `:::`, `1:2:::3`, `1::2::3` (multiple `::`), `1:2:3:4:5:6:7` + (incomplete 7-group). Replaced with a small functional checker (no `any`): + count `::` via `stripped.split('::').length-1` (reject >1); split into + head/tail, `countGroups(side)` splits on `:` and validates each as + `/^[0-9a-fA-F]{1,4}$/` hextet; group-count rule = exactly 8 when no `::`, + ≤7 when one `::` (it stands for ≥1 zero group). `''` side → 0 groups (so `::` + unspecified and `::1` work). Helpers `isHextet`/`countGroups`/`isEmbeddedIPv4` + are module-private (NOT in barrel) → no `main.*` export leak; verified diff + has no new bare export lines. +- F-04 DECISION: ACCEPT IPv4-embedded IPv6 (`::ffff:192.168.1.1`, + `2001:db8::192.168.1.1`) — valid per RFC 4291. `countGroups` treats a LAST + group containing `.` as an embedded IPv4 (validated via `validateIPV4`) that + occupies TWO 16-bit groups (so it adds +1 to the group count). Tested both + positive and the malformed negatives. +- main.js diff for this is EXACTLY the validateIPV6 function body + 3 private + helpers (52+/7- lines); second build idempotent. Expected runtime-code diff. +- I18N CONCAT GOTCHA (F-02): `_('foo ' + 'bar')` is NOT extracted (gettext sees + only literal args; the `+` makes it an expression). The established repo + convention (see section.js community_lists ~415) is + `_('foo') + ' ' + _('bar')` — the SPACE lives OUTSIDE `_()`, and each literal + has NO trailing/leading space. `extract-calls.js` line 55 does `arg.value.trim()` + so a trailing space INSIDE `_('foo ')` is trimmed in the catalog → runtime + lookup of `'foo '` MISSES. So ALWAYS put separators outside `_()`. Fixed + global_proxy (section.js ~310), block_doh + enable_ipv6 (settings.js ~463/481). +- locales regen: ran `node {extract-calls,generate-pot,generate-po ru, + distribute-locales}.js` (NOT yarn). `generate-pot.js` calls + `git config user.name`/`user.email` and CRASHES if unset → set them locally + (`git config --local user.name "..."`, email may be empty string which returns + rc=0). POT header churns (POT-Creation-Date timezone + `<>` from empty email) + — cosmetic, accepted; msgid-level diff was PURELY ADDITIVE (10 added, 0 + removed). 10 new ru msgstrs filled in SOURCE locales/netshift.ru.po then + distributed to po/ru + po/templates (both end up byte-identical to source). +- The 10 new ru fragments are the split sentences of the 3 flag descriptions + (global proxy / block DoH / enable IPv6) — translated formally/technically + matching neighbours. All catalogs LF, no empty non-header msgstr remained. diff --git a/docs/agent-rules/memory/packaging-ci-engineer.md b/docs/agent-rules/memory/packaging-ci-engineer.md index 3f2462bf..60b2e18e 100644 --- a/docs/agent-rules/memory/packaging-ci-engineer.md +++ b/docs/agent-rules/memory/packaging-ci-engineer.md @@ -52,6 +52,38 @@ artifacts out of the container -> **ipk underscore->dash rename** - Add a test: `test_xyz()` (header/pass/fail/skip), add to `main()` `all)` list, add `case` alias, update usage line + docker-compose comment. Keep the two compose invocations (build.yml smoke vs openwrt-smoke-tests.yml) in sync. +- Smoke baselines drift as tests get added: 81 passed (pre task-016) -> 84 + passed after adding `test_nft_ipv6` (3 v6 assertions). Re-confirm the baseline + from the actual run, don't trust a stale number in a task spec. +- `test_nft_ipv6` (alias `nftv6`, task-016): real-nft regression guard for the + B-01 IPv6 tproxy blocker. Builds the v6 tproxy rule from constants + (`SB_TPROXY_INBOUND_ADDRESS_V6`/`_PORT_V6`) in a throwaway `inet` table, lists + it back, and asserts it normalizes to bracketed `[::1]:1603` (positive) and + that no portless bare form (`tproxy ip6 to ::0`) appears (negative guard). The + unbracketed bug (`::1:1603`) is normalized by nft to a portless bare addr + (`[::1:1603]` / `[::0.1.22.3]` depending on nft version) — either way the + `\[::1\]:1603` grep fails, so the guard fires. Capability-gated: an + ip6-tproxy "not supported"/"operation not supported" kernel `skip`s; a + successful-but-wrong load `fail`s. SELF-PROVEN: temp scratch with unbracketed + rule -> 1 failed (guard caught it), reverted. +- Smoke test capability gating pattern: capture `add_err="$(nft add ... 2>&1)"` + inside the `if`; on success run asserts, on failure `case "$add_err"` for + *not supported* substrings -> `skip`, else `fail`. Avoids false-fails on + kernels lacking a feature while still catching real bugs. +- jq `index()` truthiness nit: `index("x") and index("y")` works (jq treats 0 as + truthy) but prefer `(index("x") != null) and (index("y") != null)` for intent + + 0-index robustness. Was at entrypoint.sh:688. +- WSL2 kernel (6.6.x-microsoft-standard-WSL2) DOES support ip6 tproxy in the + smoke container, so the v6 assertions run (not skip) locally. +- nft v6 buggy-form normalization is VERSION-DEPENDENT: the PROOF doc saw + `[::0.1.22.3]` (nftables v1.1.3), but the OpenWRT 24.10.6 smoke container + re-prints unbracketed `::1:1603` as `[::1:1603]` (no `]:` port sep). A + negative guard that greps a single literal (`\[::0`) is therefore a DEAD + assertion on the smoke env. ROBUST pattern: flag any `tproxy ip6 to [...]` + line that is NOT the correct `[::1]:1603` -> + `grep 'tproxy ip6 to \[' | grep -qv '\[::1\]:1603'`. Catches both + normalizations + future variants. Self-proved: unbracketed scratch -> BOTH + positive (`bracketed`) and negative (`no-bare`) guards FAIL (2 failed). ## CI gates by path diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index ed0a29c6..d4cc185e 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -400,3 +400,81 @@ findings; keep under ~200 lines. passed / 0 failed (suite total unchanged because the per-line `pass` runs in a piped `while` subshell — same counter quirk as test_subscription; the per-test ✓ marks are the source of truth, here 15 green for dnsdetour). + +## task-014 (PR#11 backend fixes): nft v6 bracket + dead-code removal + +- **nft IPv6 `tproxy ... to` MUST bracket the address** — `tproxy ip6 to + "$ADDR_V6:$PORT_V6"` expands to `::1:1603`, which nftables v1.1.3 parses as a + BARE IPv6 address (`[::0.1.22.3]`, port 1603 read as 0x1603 hextet) with NO + port. `nft -c` PASSES and `sing-box check` is unrelated — neither gate catches + it; only on-device IPv6 breaks. Fix: `tproxy ip6 to "[$ADDR_V6]:$PORT_V6"`. + Verify with the no-root trick: write the rule to /tmp/t.nft and + `unshare -rn sh -c 'nft -f /tmp/t.nft && nft list ruleset' | grep tproxy` — + bracketed form normalizes to `tproxy ip6 to [::1]:1603` (correct). The IPv4 + `tproxy ip to "$ADDR:$PORT"` is fine (IPv4 has no `:` ambiguity). sing-box + inbounds (`sing_box_cm_add_*_inbound` address+port as SEPARATE jq args -> + JSON `listen`/`listen_port`) have NO bracket defect — don't "fix" them. +- **Router-originated traffic is DIRECT by design** (operator decision A). The + PR's model marks only LAN/forwarded traffic in `mangle` (prerouting) and + splits proxy/direct in sing-box; `mangle_output` only carries local/loopback + daddr returns + the `NFT_OUTBOUND_MARK` return (so sing-box-originated packets + don't loop back into tproxy). Documented with a comment; no behavior change. +- **The `@netshift_subnets` (`NFT_COMMON_SET_NAME`) nft set was fully dead** — + created + populated at 6 sites but matched by NO nft rule after PR#11. SAFE to + remove because every subnet source is independently carried into a sing-box + rule_set: user_subnets -> `patch_source_ruleset_rules ip_cidr` + local source + ruleset; local_subnet_lists -> `import_plain_subnet_list_to_local_source_ruleset_chunked`; + community_lists -> `configure_community_list_handler` (`$SRS_MAIN_URL/<svc>.srs` + remote ruleset); remote json/srs subnets -> `configure_remote_domain_or_subnet_list_handler` + (`sing_box_cm_add_remote_ruleset`); remote plain -> `prepare_source_ruleset` + + plain import. DISCORD is the ONE exception that still needs an nft set + (`NFT_DISCORD_SET_NAME`) — it has a live dport-restricted mangle rule + (`@netshift_discord_subnets udp dport {19000-20000,50000-65535}`) that a + sing-box route rule can't express. Removed: set creation (~972), all 6 + `nft_add_set_elements*` populate calls, the now-orphaned + `import_subnets_from_remote_json_file`/`_srs_file` (json/srs now log + "sing-box manages updates" like the domains path), `netshift_subnets` from the + diagnostics `sets` list, and the `NFT_COMMON_SET_NAME` constant. Left the 9 + IPv4 `SUBNETS_*` constants (only `SUBNETS_DISCORD` used) in place — constants.sh + is `# shellcheck disable=SC2034` so unused-looking vars don't fail lint, and + trimming them was out of declared scope. +- **8 `SUBNETS_*_V6` constants had zero consumers** (`git grep` only matched + definitions + a memory doc) — removed. +- **B-09 dead predicates**: `is_ip`/`is_ipv6_cidr`/`is_ipv6` in helpers.sh were + all unused (`is_ipv6` only called by the other two; tests use only `is_ipv4`/ + `url_is_ipv6_literal`/`is_ipv4_ip_or_ipv4_cidr`). Removed all three. +- **Monitor spawn guard (B-05)**: extracted `start_sing_box_monitor` mirroring + the `start_subscription_startup_retry_worker` pidfile-guard — if + `/var/run/netshift_monitor.pid` exists and `kill -0 "$pid"` succeeds, skip the + spawn (else `rm` stale pidfile then spawn). Prevents a procd double-start from + orphaning a monitor that `stop()` can no longer kill. +- **B-08 dnsmasq guard (review-001 FIX — sentinel, not markers)**: my first B-08 + attempt gated `dnsmasq_is_configured_for_netshift` on the presence of a private + backup marker (`netshift_server`/`netshift_noresolv`/`netshift_cachesize`). + That was WRONG and regressed STOCK dnsmasq: on a default box with no original + server/noresolv/cachesize, `dnsmasq_configure` writes NO markers + (`backup_dnsmasq_config_option` only writes when the original value is + non-empty; the server-backup loop is skipped when current servers are empty). + So the guard returned false, and the redundant `dnsmasq_configure force` path + (monitor recovery restart, double-start) re-ran "backup" — but the LIVE values + were now netshift's OWN (noresolv=1, cachesize=0), so it captured those as the + backup -> `dnsmasq_restore` later restored 1/0 instead of the OpenWRT defaults + (0/150) -> router DNS broken after stop/uninstall. + CORRECT fix = an explicit netshift-owned SENTINEL: `dnsmasq_configure` does + `uci_set "dhcp" "@dnsmasq[0]" "netshift_configured" 1` UNCONDITIONALLY right + after applying our config (before the commit); `dnsmasq_is_configured_for_netshift` + short-circuits iff `netshift_configured == 1` (authoritative ownership flag, no + value/marker inference); `dnsmasq_restore` clears it with `uci_remove_quiet` + before its commit so a fresh future configure re-establishes ownership. The + sentinel is a distinct option name (not in the `server` list), so it never + leaks into the server/backup iteration. Verified all 3 scenarios via an + awk-extracted-functions harness (use an EXACT-match UCI stub — `awk -F'\t' + $1==k`, NOT grep/sed, because the literal `@dnsmasq[0]` key contains `[0]` + which a regex reads as a char class and silently mis-reads every lookup): + (A) stock -> sentinel set, no spurious backup, force-again short-circuits, + restore=0/150, sentinel cleared; (B) admin-had-config -> real values backed up + & restored intact; (C) coincidental admin match w/o sentinel -> NOT treated as + owned. shellcheck clean; smoke 81/0. +- shellcheck -S error clean (bin + libs + install.sh); `smoke-tests all` = 81 + passed / 0 failed (unchanged baseline). No new smoke test (separate packaging + task owns nft/v6 coverage per spec). No sacred constant VALUES changed. diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index 5f151960..afff9f31 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -41,6 +41,13 @@ "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:106" ] }, + { + "call": "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers.", + "key": "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:469" + ] + }, { "call": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "key": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", @@ -59,14 +66,14 @@ "call": "At least one valid domain must be specified. Comments-only content is not allowed.", "key": "At least one valid domain must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:559" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:562" ] }, { "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:640" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:643" ] }, { @@ -76,6 +83,13 @@ "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:47" ] }, + { + "call": "Block direct connections to known public DNS-over-HTTPS (DoH) servers.", + "key": "Block direct connections to known public DNS-over-HTTPS (DoH) servers.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:463" + ] + }, { "call": "Block DoH Servers", "key": "Block DoH Servers", @@ -185,7 +199,7 @@ "call": "Community Lists", "key": "Community Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:414" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:417" ] }, { @@ -319,8 +333,8 @@ "call": "Disabled", "key": "Disabled", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:508", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588" ] }, { @@ -341,7 +355,7 @@ "call": "DNS over HTTPS (DoH)", "key": "DNS over HTTPS (DoH)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15" ] }, @@ -349,7 +363,7 @@ "call": "DNS over TLS (DoT)", "key": "DNS over TLS (DoT)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16" ] }, @@ -357,7 +371,7 @@ "call": "DNS Protocol Type", "key": "DNS Protocol Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:379", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12" ] }, @@ -372,7 +386,7 @@ "call": "DNS Server", "key": "DNS Server", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:392", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:395", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24" ] }, @@ -394,7 +408,7 @@ "call": "Domain Resolver", "key": "Domain Resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:372" ] }, { @@ -452,8 +466,8 @@ "call": "Dynamic List", "key": "Dynamic List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:509", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:589" ] }, { @@ -467,28 +481,35 @@ "call": "Enable built-in DNS resolver for domains handled by this section", "key": "Enable built-in DNS resolver for domains handled by this section", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373" ] }, { "call": "Enable DNS resolve to get real IP when routing", "key": "Enable DNS resolve to get real IP when routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:809" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:812" ] }, { "call": "Enable IPv6 Support", "key": "Enable IPv6 Support", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:476" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:483" + ] + }, + { + "call": "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support.", + "key": "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:484" ] }, { "call": "Enable Mixed Proxy", "key": "Enable Mixed Proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:780" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783" ] }, { @@ -502,7 +523,7 @@ "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:781" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784" ] }, { @@ -530,21 +551,21 @@ "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:541" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544" ] }, { "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:518" ] }, { "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:595" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:598" ] }, { @@ -680,7 +701,7 @@ "call": "Fully Routed IPs", "key": "Fully Routed IPs", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:753" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:756" ] }, { @@ -884,7 +905,7 @@ "call": "Invalid IPv6 address", "key": "Invalid IPv6 address", "places": [ - "src/validators/validateIp.ts:28" + "src/validators/validateIp.ts:66" ] }, { @@ -1166,14 +1187,14 @@ "call": "Local Domain Lists", "key": "Local Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:664" ] }, { "call": "Local Subnet Lists", "key": "Local Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:684" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:687" ] }, { @@ -1208,7 +1229,7 @@ "call": "Mixed Proxy Port", "key": "Mixed Proxy Port", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:796" ] }, { @@ -1250,7 +1271,7 @@ "call": "Network Interface", "key": "Network Interface", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326" ] }, { @@ -1287,6 +1308,20 @@ "src/netshift/tabs/diagnostic/diagnostic.store.ts:91" ] }, + { + "call": "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT.", + "key": "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:473" + ] + }, + { + "call": "Only one section can be global at a time.", + "key": "Only one section can be global at a time.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:318" + ] + }, { "call": "Operation timed out", "key": "Operation timed out", @@ -1386,28 +1421,28 @@ "call": "Regional options cannot be used together", "key": "Regional options cannot be used together", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:448" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:451" ] }, { "call": "Remote Domain Lists", "key": "Remote Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:707" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:710" ] }, { "call": "Remote Subnet Lists", "key": "Remote Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:730" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:733" ] }, { "call": "Resolve real IP for routing", "key": "Resolve real IP for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:811" ] }, { @@ -1417,6 +1452,13 @@ "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:53" ] }, + { + "call": "Route all unmatched traffic through this section's outbound.", + "key": "Route all unmatched traffic through this section's outbound.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:310" + ] + }, { "call": "Route main DNS through proxy/VPN", "key": "Route main DNS through proxy/VPN", @@ -1442,7 +1484,7 @@ "call": "Routing Excluded IPs", "key": "Routing Excluded IPs", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:488" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:494" ] }, { @@ -1498,7 +1540,7 @@ "call": "Russia inside restrictions", "key": "Russia inside restrictions", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:470" ] }, { @@ -1519,7 +1561,7 @@ "call": "Select a predefined list for routing", "key": "Select a predefined list for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:415" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418" ] }, { @@ -1554,14 +1596,14 @@ "call": "Select network interface for VPN connection", "key": "Select network interface for VPN connection", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:327" ] }, { "call": "Select or enter DNS server address", "key": "Select or enter DNS server address", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:393", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25" ] }, @@ -1583,21 +1625,21 @@ "call": "Select the DNS protocol type for the domain resolver", "key": "Select the DNS protocol type for the domain resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:380" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383" ] }, { "call": "Select the list type for adding custom domains", "key": "Select the list type for adding custom domains", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:503" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506" ] }, { "call": "Select the list type for adding custom subnets", "key": "Select the list type for adding custom subnets", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:583" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586" ] }, { @@ -1738,36 +1780,36 @@ "call": "Specify a local IP address to be excluded from routing", "key": "Specify a local IP address to be excluded from routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:489" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:495" ] }, { "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:754" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757" ] }, { "call": "Specify remote URLs to download and use domain lists", "key": "Specify remote URLs to download and use domain lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:708" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:711" ] }, { "call": "Specify remote URLs to download and use subnet lists", "key": "Specify remote URLs to download and use subnet lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:731" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734" ] }, { "call": "Specify the path to the list file located on the router filesystem", "key": "Specify the path to the list file located on the router filesystem", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:685" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:688" ] }, { @@ -1851,8 +1893,8 @@ "call": "Text List", "key": "Text List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:507", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:510", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590" ] }, { @@ -1883,6 +1925,13 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:262" ] }, + { + "call": "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS.", + "key": "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:465" + ] + }, { "call": "Time in seconds for DNS record caching (default: 60)", "key": "Time in seconds for DNS record caching (default: 60)", @@ -1929,7 +1978,7 @@ "call": "UDP (Unprotected DNS)", "key": "UDP (Unprotected DNS)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17" ] }, @@ -2017,46 +2066,60 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229" ] }, + { + "call": "Use this only when the router has working IPv6 connectivity.", + "key": "Use this only when the router has working IPv6 connectivity.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:486" + ] + }, + { + "call": "Use with Exclusion sections to route specific domains directly.", + "key": "Use with Exclusion sections to route specific domains directly.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316" + ] + }, { "call": "User Domain List Type", "key": "User Domain List Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:502" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505" ] }, { "call": "User Domains", "key": "User Domains", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:517" ] }, { "call": "User Domains List", "key": "User Domains List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543" ] }, { "call": "User Subnet List Type", "key": "User Subnet List Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:582" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585" ] }, { "call": "User Subnets", "key": "User Subnets", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:597" ] }, { "call": "User Subnets List", "key": "User Subnets List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:620" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:623" ] }, { @@ -2070,7 +2133,8 @@ "src/validators/validateDomain.ts:30", "src/validators/validateHysteriaUrl.ts:120", "src/validators/validateIp.ts:8", - "src/validators/validateIp.ts:24", + "src/validators/validateIp.ts:91", + "src/validators/validateIp.ts:97", "src/validators/validateOutboundJson.ts:7", "src/validators/validatePath.ts:16", "src/validators/validateShadowsocksUrl.ts:95", @@ -2087,8 +2151,8 @@ "call": "Validation errors:", "key": "Validation errors:", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:652" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:655" ] }, { @@ -2119,14 +2183,21 @@ "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:450" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:453" ] }, { "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:469" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472" + ] + }, + { + "call": "When enabled, traffic not matching any other section's lists will go through this proxy.", + "key": "When enabled, traffic not matching any other section's lists will go through this proxy.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:312" ] }, { @@ -2164,4 +2235,4 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:132" ] } -] +] \ No newline at end of file diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index c70bb9d5..5e6e1464 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# spgsroot, yandexru45, 2026. +# spgsroot, yandexru45 <>, 2026. #, fuzzy msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 07:00+0800\n" -"PO-Revision-Date: 2026-06-06 07:00+0800\n" -"Last-Translator: spgsroot, yandexru45\n" +"POT-Creation-Date: 2026-06-06 15:38+0300\n" +"PO-Revision-Date: 2026-06-06 15:38+0300\n" +"Last-Translator: spgsroot, yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" @@ -40,6 +40,10 @@ msgstr "" msgid "Additional marking rules found" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:469 +msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" @@ -48,11 +52,11 @@ msgstr "" msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:559 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:562 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:640 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:643 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -60,6 +64,10 @@ msgstr "" msgid "Available actions" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:463 +msgid "Block direct connections to known public DNS-over-HTTPS (DoH) servers." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:462 msgid "Block DoH Servers" msgstr "" @@ -123,7 +131,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:414 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:417 msgid "Community Lists" msgstr "" @@ -200,8 +208,8 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:508 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 msgid "Disabled" msgstr "" @@ -213,17 +221,17 @@ msgstr "" msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:379 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12 msgid "DNS Protocol Type" msgstr "" @@ -232,7 +240,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:392 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:395 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24 msgid "DNS Server" msgstr "" @@ -245,7 +253,7 @@ msgstr "" msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:372 msgid "Domain Resolver" msgstr "" @@ -279,8 +287,8 @@ msgstr "" msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:509 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:589 msgid "Dynamic List" msgstr "" @@ -288,19 +296,23 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:809 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:812 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:476 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:483 msgid "Enable IPv6 Support" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:780 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:484 +msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783 msgid "Enable Mixed Proxy" msgstr "" @@ -308,7 +320,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:781 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -324,15 +336,15 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:541 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:518 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:595 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:598 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" @@ -414,7 +426,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:753 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:756 msgid "Fully Routed IPs" msgstr "" @@ -531,7 +543,7 @@ msgstr "" msgid "Invalid IP address" msgstr "" -#: src/validators/validateIp.ts:28 +#: src/validators/validateIp.ts:66 msgid "Invalid IPv6 address" msgstr "" @@ -693,11 +705,11 @@ msgstr "" msgid "List Update Frequency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:664 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:684 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:687 msgid "Local Subnet Lists" msgstr "" @@ -717,7 +729,7 @@ msgstr "" msgid "Memory Usage" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:796 msgid "Mixed Proxy Port" msgstr "" @@ -741,7 +753,7 @@ msgstr "" msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326 msgid "Network Interface" msgstr "" @@ -767,6 +779,14 @@ msgstr "" msgid "Not running" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:473 +msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:318 +msgid "Only one section can be global at a time." +msgstr "" + #: src/helpers/withTimeout.ts:7 msgid "Operation timed out" msgstr "" @@ -823,19 +843,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:448 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:451 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:707 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:710 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:730 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:733 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:811 msgid "Resolve real IP for routing" msgstr "" @@ -843,6 +863,10 @@ msgstr "" msgid "Restart NetShift" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:310 +msgid "Route all unmatched traffic through this section's outbound." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:68 msgid "Route main DNS through proxy/VPN" msgstr "" @@ -855,7 +879,7 @@ msgstr "" msgid "Router DNS is routed through sing-box" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:488 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:494 msgid "Routing Excluded IPs" msgstr "" @@ -887,7 +911,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:470 msgid "Russia inside restrictions" msgstr "" @@ -899,7 +923,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:415 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418 msgid "Select a predefined list for routing" msgstr "" @@ -919,11 +943,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:327 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:393 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25 msgid "Select or enter DNS server address" msgstr "" @@ -936,15 +960,15 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:380 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:503 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506 msgid "Select the list type for adding custom domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:583 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586 msgid "Select the list type for adding custom subnets" msgstr "" @@ -1025,24 +1049,24 @@ msgstr "" msgid "Source Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:489 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:495 msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:754 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:708 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:711 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:731 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:685 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:688 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1090,8 +1114,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:507 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:510 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590 msgid "Text List" msgstr "" @@ -1111,6 +1135,10 @@ msgstr "" msgid "The URL used to test server connectivity" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:465 +msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:114 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" @@ -1135,7 +1163,7 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" @@ -1191,27 +1219,35 @@ msgstr "" msgid "URLTest Tolerance" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:502 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:486 +msgid "Use this only when the router has working IPv6 connectivity." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316 +msgid "Use with Exclusion sections to route specific domains directly." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505 msgid "User Domain List Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:517 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:582 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585 msgid "User Subnet List Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:597 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:620 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:623 msgid "User Subnets List" msgstr "" @@ -1222,7 +1258,8 @@ msgstr "" #: src/validators/validateDomain.ts:30 #: src/validators/validateHysteriaUrl.ts:120 #: src/validators/validateIp.ts:8 -#: src/validators/validateIp.ts:24 +#: src/validators/validateIp.ts:91 +#: src/validators/validateIp.ts:97 #: src/validators/validateOutboundJson.ts:7 #: src/validators/validatePath.ts:16 #: src/validators/validateShadowsocksUrl.ts:95 @@ -1236,8 +1273,8 @@ msgstr "" msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:652 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:655 msgid "Validation errors:" msgstr "" @@ -1256,14 +1293,18 @@ msgstr "" msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:450 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:453 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:469 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:312 +msgid "When enabled, traffic not matching any other section's lists will go through this proxy." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80 msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 68d0c4eb..6d00cfb3 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 15:00+0800\n" -"PO-Revision-Date: 2026-06-06 15:00+0800\n" +"POT-Creation-Date: 2026-06-06 18:38+0300\n" +"PO-Revision-Date: 2026-06-06 18:38+0300\n" "Last-Translator: spgsroot, yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -35,6 +35,9 @@ msgstr "Активные соединения" msgid "Additional marking rules found" msgstr "Найдены дополнительные правила маркировки" +msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." +msgstr "Затрагивает публичные DoH-серверы Cloudflare, Google, Quad9, OpenDNS, AdGuard и Yandex." + msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт." @@ -50,6 +53,9 @@ msgstr "Необходимо указать хотя бы одну действ msgid "Available actions" msgstr "Доступные действия" +msgid "Block direct connections to known public DNS-over-HTTPS (DoH) servers." +msgstr "Блокирует прямые подключения к известным публичным серверам DNS-over-HTTPS (DoH)." + msgid "Block DoH Servers" msgstr "Блокировать DoH-серверы" @@ -218,6 +224,9 @@ msgstr "Разрешать домены в реальные IP-адреса пе msgid "Enable IPv6 Support" msgstr "Включить поддержку IPv6" +msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." +msgstr "Включить маршрутизацию TProxy по IPv6, входящий DNS по IPv6 и поддержку FakeIP для IPv6." + msgid "Enable Mixed Proxy" msgstr "Включить смешанный прокси" @@ -554,6 +563,12 @@ msgstr "Не отвечает" msgid "Not running" msgstr "Не запущено" +msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." +msgstr "Примечание: если тип вышестоящего DNS установлен в «DoH», включайте это только после переключения на UDP или DoT." + +msgid "Only one section can be global at a time." +msgstr "Только одна секция может быть глобальной одновременно." + msgid "Operation timed out" msgstr "Время ожидания истекло" @@ -608,6 +623,9 @@ msgstr "Разрешение реальных IP-адресов" msgid "Restart NetShift" msgstr "Перезапустить NetShift" +msgid "Route all unmatched traffic through this section's outbound." +msgstr "Направлять весь несовпавший трафик через outbound этой секции." + msgid "Route main DNS through proxy/VPN" msgstr "Основной DNS через прокси/VPN" @@ -806,6 +824,9 @@ msgstr "Максимально допустимая разница во врем msgid "The URL used to test server connectivity" msgstr "URL-адрес, используемый для проверки подключения к серверу" +msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." +msgstr "Это не позволяет приложениям обходить DNS-фильтрацию роутера за счёт использования собственного шифрованного DNS." + msgid "Time in seconds for DNS record caching (default: 60)" msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)" @@ -860,6 +881,12 @@ msgstr "URLTest ссылка для проверки" msgid "URLTest Tolerance" msgstr "URLTest допустимое отклонение" +msgid "Use this only when the router has working IPv6 connectivity." +msgstr "Используйте это только если на роутере есть рабочее подключение по IPv6." + +msgid "Use with Exclusion sections to route specific domains directly." +msgstr "Используйте вместе с секциями исключений для прямой маршрутизации определённых доменов." + msgid "User Domain List Type" msgstr "Тип пользовательского списка доменов" @@ -899,6 +926,9 @@ msgstr "Предупреждение: %s нельзя использовать msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных." +msgid "When enabled, traffic not matching any other section's lists will go through this proxy." +msgstr "Когда включено, трафик, не совпадающий со списками других секций, будет идти через этот прокси." + msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "Какая секция прокси/VPN обслуживает DNS. Оставьте пустым, чтобы использовать первый настроенный outbound." diff --git a/fe-app-netshift/src/validators/tests/validateDns.test.js b/fe-app-netshift/src/validators/tests/validateDns.test.js index c2bac53a..9fa72c9c 100644 --- a/fe-app-netshift/src/validators/tests/validateDns.test.js +++ b/fe-app-netshift/src/validators/tests/validateDns.test.js @@ -22,6 +22,8 @@ export const additionalValidDns = [ export const additionalInvalidDns = [ ['IPv6 invalid hex', '2001:db8::zzzz'], ['IPv6 group too long', '12345::1'], + ['IPv6 triple colon only', ':::'], + ['IPv6 multiple compressions', '1::2::3'], ]; const validDns = [...validIPs, ...validDomains, ...additionalValidDns]; diff --git a/fe-app-netshift/src/validators/tests/validateIp.test.js b/fe-app-netshift/src/validators/tests/validateIp.test.js index 9fd63fe2..d44913a9 100644 --- a/fe-app-netshift/src/validators/tests/validateIp.test.js +++ b/fe-app-netshift/src/validators/tests/validateIp.test.js @@ -26,12 +26,22 @@ export const validIPv6 = [ ['Compressed', '2001:db8::1'], ['Full form', '2001:0db8:85a3:0000:0000:8a2e:0370:7334'], ['Bracketed', '[2001:db8::1]'], + ['Unspecified', '::'], + ['Compressed middle', '2001:db8:0:0:1::1'], + ['IPv4-mapped', '::ffff:192.168.1.1'], + ['IPv4-embedded', '2001:db8::192.168.1.1'], ]; export const invalidIPv6 = [ ['Invalid hex', '2001:db8::zzzz'], ['Group too long', '12345::1'], ['Too many groups', '2001:db8:85a3:0:0:8a2e:370:7334:1234'], + ['Triple colon only', ':::'], + ['Colon run inside', '1:2:::3'], + ['Multiple compressions', '1::2::3'], + ['Incomplete (7 groups, no ::)', '1:2:3:4:5:6:7'], + ['Bad hex group', 'gggg::1'], + ['Too many groups (9)', '1:2:3:4:5:6:7:8:9'], ]; describe('validateIPV4', () => { diff --git a/fe-app-netshift/src/validators/tests/validateSubnet.test.js b/fe-app-netshift/src/validators/tests/validateSubnet.test.js index 9415e9b7..0ad92ade 100644 --- a/fe-app-netshift/src/validators/tests/validateSubnet.test.js +++ b/fe-app-netshift/src/validators/tests/validateSubnet.test.js @@ -30,6 +30,8 @@ export const invalidSubnets = [ ['IPv6 CIDR too high', '2001:db8::1/129'], ['IPv6 CIDR negative', '2001:db8::1/-1'], ['IPv6 CIDR not number', '2001:db8::1/abc'], + ['IPv6 triple colon with CIDR', ':::/64'], + ['IPv6 multiple compressions with CIDR', '1::2::3/64'], ]; describe('validateSubnet', () => { diff --git a/fe-app-netshift/src/validators/validateIp.ts b/fe-app-netshift/src/validators/validateIp.ts index fab5b44b..d5fae88e 100644 --- a/fe-app-netshift/src/validators/validateIp.ts +++ b/fe-app-netshift/src/validators/validateIp.ts @@ -11,21 +11,93 @@ export function validateIPV4(ip: string): ValidationResult { return { valid: false, message: _('Invalid IP address') }; } +const HEXTET_REGEX = /^[0-9a-fA-F]{1,4}$/; + +function isHextet(group: string): boolean { + return HEXTET_REGEX.test(group); +} + +function isEmbeddedIPv4(group: string): boolean { + return validateIPV4(group).valid; +} + +// Validates one side (the part before or after "::") as a list of hextets. +// The trailing group may be a dotted IPv4 (embedded/IPv4-mapped IPv6), which +// counts as TWO 16-bit groups. Returns the 16-bit group count, or null on any +// invalid group. +function countGroups(side: string, allowEmbeddedIPv4: boolean): number | null { + if (side === '') { + return 0; + } + + const groups = side.split(':'); + + for (let i = 0; i < groups.length; i++) { + const group = groups[i]; + const isLast = i === groups.length - 1; + + if (allowEmbeddedIPv4 && isLast && group.includes('.')) { + if (!isEmbeddedIPv4(group)) { + return null; + } + + continue; + } + + if (!isHextet(group)) { + return null; + } + } + + // An embedded IPv4 tail occupies two 16-bit groups instead of one. + const lastGroup = groups[groups.length - 1]; + const embeddedExtra = + allowEmbeddedIPv4 && lastGroup.includes('.') && isEmbeddedIPv4(lastGroup) + ? 1 + : 0; + + return groups.length + embeddedExtra; +} + export function validateIPV6(ip: string): ValidationResult { const stripped = ip.replace(/^\[/, '').replace(/\]$/, ''); - const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; - const ipv6CompressedRegex = - /^([0-9a-fA-F]{0,4}:)*:([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/; + const invalid: ValidationResult = { + valid: false, + message: _('Invalid IPv6 address'), + }; + + // At most one "::" compression is allowed. + const doubleColonCount = stripped.split('::').length - 1; + if (doubleColonCount > 1) { + return invalid; + } + + if (doubleColonCount === 1) { + const [head, tail] = stripped.split('::'); - if (ipv6Regex.test(stripped) || ipv6CompressedRegex.test(stripped)) { - const colons = (stripped.match(/:/g) || []).length; + const headGroups = countGroups(head, true); + const tailGroups = countGroups(tail, true); - if (colons >= 2 && colons <= 7) { - return { valid: true, message: _('Valid') }; + if (headGroups === null || tailGroups === null) { + return invalid; } + + // "::" must replace at least one group, so the explicit groups can total + // at most 7 (it stands in for one or more zero groups). + if (headGroups + tailGroups > 7) { + return invalid; + } + + return { valid: true, message: _('Valid') }; + } + + // No "::" → must be exactly 8 groups, all explicit. + const totalGroups = countGroups(stripped, true); + if (totalGroups === 8) { + return { valid: true, message: _('Valid') }; } - return { valid: false, message: _('Invalid IPv6 address') }; + return invalid; } export function validateIP(ip: string): ValidationResult { diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 9a6e32e4..4d7013be 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -13,17 +13,62 @@ function validateIPV4(ip) { } return { valid: false, message: _("Invalid IP address") }; } +var HEXTET_REGEX = /^[0-9a-fA-F]{1,4}$/; +function isHextet(group) { + return HEXTET_REGEX.test(group); +} +function isEmbeddedIPv4(group) { + return validateIPV4(group).valid; +} +function countGroups(side, allowEmbeddedIPv4) { + if (side === "") { + return 0; + } + const groups = side.split(":"); + for (let i = 0; i < groups.length; i++) { + const group = groups[i]; + const isLast = i === groups.length - 1; + if (allowEmbeddedIPv4 && isLast && group.includes(".")) { + if (!isEmbeddedIPv4(group)) { + return null; + } + continue; + } + if (!isHextet(group)) { + return null; + } + } + const lastGroup = groups[groups.length - 1]; + const embeddedExtra = allowEmbeddedIPv4 && lastGroup.includes(".") && isEmbeddedIPv4(lastGroup) ? 1 : 0; + return groups.length + embeddedExtra; +} function validateIPV6(ip) { const stripped = ip.replace(/^\[/, "").replace(/\]$/, ""); - const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; - const ipv6CompressedRegex = /^([0-9a-fA-F]{0,4}:)*:([0-9a-fA-F]{0,4}:)*[0-9a-fA-F]{0,4}$/; - if (ipv6Regex.test(stripped) || ipv6CompressedRegex.test(stripped)) { - const colons = (stripped.match(/:/g) || []).length; - if (colons >= 2 && colons <= 7) { - return { valid: true, message: _("Valid") }; + const invalid = { + valid: false, + message: _("Invalid IPv6 address") + }; + const doubleColonCount = stripped.split("::").length - 1; + if (doubleColonCount > 1) { + return invalid; + } + if (doubleColonCount === 1) { + const [head, tail] = stripped.split("::"); + const headGroups = countGroups(head, true); + const tailGroups = countGroups(tail, true); + if (headGroups === null || tailGroups === null) { + return invalid; + } + if (headGroups + tailGroups > 7) { + return invalid; } + return { valid: true, message: _("Valid") }; + } + const totalGroups = countGroups(stripped, true); + if (totalGroups === 8) { + return { valid: true, message: _("Valid") }; } - return { valid: false, message: _("Invalid IPv6 address") }; + return invalid; } function validateIP(ip) { const ipv4 = validateIPV4(ip); diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index 7076070b..1600d909 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -307,12 +307,15 @@ function createSectionContent(section) { form.Flag, "global_proxy", _("Global Proxy"), - _( - "Route all unmatched traffic through this section's outbound. " + - "When enabled, traffic not matching any other section's lists will go through this proxy. " + - "Use with Exclusion sections to route specific domains directly. " + - "Only one section can be global at a time.", - ), + _("Route all unmatched traffic through this section's outbound.") + + " " + + _( + "When enabled, traffic not matching any other section's lists will go through this proxy.", + ) + + " " + + _("Use with Exclusion sections to route specific domains directly.") + + " " + + _("Only one section can be global at a time."), ); o.default = "0"; o.rmempty = false; diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js index 97a32453..4a262874 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js @@ -460,12 +460,19 @@ function createSettingsContent(section) { form.Flag, "block_doh", _("Block DoH Servers"), - _( - "Block direct connections to known public DNS-over-HTTPS (DoH) servers. " + - "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS. " + - "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers. " + + _("Block direct connections to known public DNS-over-HTTPS (DoH) servers.") + + " " + + _( + "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS.", + ) + + " " + + _( + "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers.", + ) + + " " + + _( "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT.", - ), + ), ); o.default = "0"; o.rmempty = false; @@ -474,10 +481,9 @@ function createSettingsContent(section) { form.Flag, "enable_ipv6", _("Enable IPv6 Support"), - _( - "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support. " + - "Use this only when the router has working IPv6 connectivity.", - ), + _("Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support.") + + " " + + _("Use this only when the router has working IPv6 connectivity."), ); o.default = "0"; o.rmempty = false; diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 68d0c4eb..6d00cfb3 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 15:00+0800\n" -"PO-Revision-Date: 2026-06-06 15:00+0800\n" +"POT-Creation-Date: 2026-06-06 18:38+0300\n" +"PO-Revision-Date: 2026-06-06 18:38+0300\n" "Last-Translator: spgsroot, yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -35,6 +35,9 @@ msgstr "Активные соединения" msgid "Additional marking rules found" msgstr "Найдены дополнительные правила маркировки" +msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." +msgstr "Затрагивает публичные DoH-серверы Cloudflare, Google, Quad9, OpenDNS, AdGuard и Yandex." + msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт." @@ -50,6 +53,9 @@ msgstr "Необходимо указать хотя бы одну действ msgid "Available actions" msgstr "Доступные действия" +msgid "Block direct connections to known public DNS-over-HTTPS (DoH) servers." +msgstr "Блокирует прямые подключения к известным публичным серверам DNS-over-HTTPS (DoH)." + msgid "Block DoH Servers" msgstr "Блокировать DoH-серверы" @@ -218,6 +224,9 @@ msgstr "Разрешать домены в реальные IP-адреса пе msgid "Enable IPv6 Support" msgstr "Включить поддержку IPv6" +msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." +msgstr "Включить маршрутизацию TProxy по IPv6, входящий DNS по IPv6 и поддержку FakeIP для IPv6." + msgid "Enable Mixed Proxy" msgstr "Включить смешанный прокси" @@ -554,6 +563,12 @@ msgstr "Не отвечает" msgid "Not running" msgstr "Не запущено" +msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." +msgstr "Примечание: если тип вышестоящего DNS установлен в «DoH», включайте это только после переключения на UDP или DoT." + +msgid "Only one section can be global at a time." +msgstr "Только одна секция может быть глобальной одновременно." + msgid "Operation timed out" msgstr "Время ожидания истекло" @@ -608,6 +623,9 @@ msgstr "Разрешение реальных IP-адресов" msgid "Restart NetShift" msgstr "Перезапустить NetShift" +msgid "Route all unmatched traffic through this section's outbound." +msgstr "Направлять весь несовпавший трафик через outbound этой секции." + msgid "Route main DNS through proxy/VPN" msgstr "Основной DNS через прокси/VPN" @@ -806,6 +824,9 @@ msgstr "Максимально допустимая разница во врем msgid "The URL used to test server connectivity" msgstr "URL-адрес, используемый для проверки подключения к серверу" +msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." +msgstr "Это не позволяет приложениям обходить DNS-фильтрацию роутера за счёт использования собственного шифрованного DNS." + msgid "Time in seconds for DNS record caching (default: 60)" msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)" @@ -860,6 +881,12 @@ msgstr "URLTest ссылка для проверки" msgid "URLTest Tolerance" msgstr "URLTest допустимое отклонение" +msgid "Use this only when the router has working IPv6 connectivity." +msgstr "Используйте это только если на роутере есть рабочее подключение по IPv6." + +msgid "Use with Exclusion sections to route specific domains directly." +msgstr "Используйте вместе с секциями исключений для прямой маршрутизации определённых доменов." + msgid "User Domain List Type" msgstr "Тип пользовательского списка доменов" @@ -899,6 +926,9 @@ msgstr "Предупреждение: %s нельзя использовать msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных." +msgid "When enabled, traffic not matching any other section's lists will go through this proxy." +msgstr "Когда включено, трафик, не совпадающий со списками других секций, будет идти через этот прокси." + msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "Какая секция прокси/VPN обслуживает DNS. Оставьте пустым, чтобы использовать первый настроенный outbound." diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index c70bb9d5..5e6e1464 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# spgsroot, yandexru45, 2026. +# spgsroot, yandexru45 <>, 2026. #, fuzzy msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 07:00+0800\n" -"PO-Revision-Date: 2026-06-06 07:00+0800\n" -"Last-Translator: spgsroot, yandexru45\n" +"POT-Creation-Date: 2026-06-06 15:38+0300\n" +"PO-Revision-Date: 2026-06-06 15:38+0300\n" +"Last-Translator: spgsroot, yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" @@ -40,6 +40,10 @@ msgstr "" msgid "Additional marking rules found" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:469 +msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" @@ -48,11 +52,11 @@ msgstr "" msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:559 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:562 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:640 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:643 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -60,6 +64,10 @@ msgstr "" msgid "Available actions" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:463 +msgid "Block direct connections to known public DNS-over-HTTPS (DoH) servers." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:462 msgid "Block DoH Servers" msgstr "" @@ -123,7 +131,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:414 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:417 msgid "Community Lists" msgstr "" @@ -200,8 +208,8 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:508 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 msgid "Disabled" msgstr "" @@ -213,17 +221,17 @@ msgstr "" msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:379 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12 msgid "DNS Protocol Type" msgstr "" @@ -232,7 +240,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:392 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:395 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24 msgid "DNS Server" msgstr "" @@ -245,7 +253,7 @@ msgstr "" msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:372 msgid "Domain Resolver" msgstr "" @@ -279,8 +287,8 @@ msgstr "" msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:509 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:589 msgid "Dynamic List" msgstr "" @@ -288,19 +296,23 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:809 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:812 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:476 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:483 msgid "Enable IPv6 Support" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:780 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:484 +msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783 msgid "Enable Mixed Proxy" msgstr "" @@ -308,7 +320,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:781 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -324,15 +336,15 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:541 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:518 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:595 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:598 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" @@ -414,7 +426,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:753 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:756 msgid "Fully Routed IPs" msgstr "" @@ -531,7 +543,7 @@ msgstr "" msgid "Invalid IP address" msgstr "" -#: src/validators/validateIp.ts:28 +#: src/validators/validateIp.ts:66 msgid "Invalid IPv6 address" msgstr "" @@ -693,11 +705,11 @@ msgstr "" msgid "List Update Frequency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:664 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:684 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:687 msgid "Local Subnet Lists" msgstr "" @@ -717,7 +729,7 @@ msgstr "" msgid "Memory Usage" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:796 msgid "Mixed Proxy Port" msgstr "" @@ -741,7 +753,7 @@ msgstr "" msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326 msgid "Network Interface" msgstr "" @@ -767,6 +779,14 @@ msgstr "" msgid "Not running" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:473 +msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:318 +msgid "Only one section can be global at a time." +msgstr "" + #: src/helpers/withTimeout.ts:7 msgid "Operation timed out" msgstr "" @@ -823,19 +843,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:448 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:451 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:707 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:710 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:730 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:733 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:811 msgid "Resolve real IP for routing" msgstr "" @@ -843,6 +863,10 @@ msgstr "" msgid "Restart NetShift" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:310 +msgid "Route all unmatched traffic through this section's outbound." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:68 msgid "Route main DNS through proxy/VPN" msgstr "" @@ -855,7 +879,7 @@ msgstr "" msgid "Router DNS is routed through sing-box" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:488 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:494 msgid "Routing Excluded IPs" msgstr "" @@ -887,7 +911,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:470 msgid "Russia inside restrictions" msgstr "" @@ -899,7 +923,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:415 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418 msgid "Select a predefined list for routing" msgstr "" @@ -919,11 +943,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:327 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:393 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25 msgid "Select or enter DNS server address" msgstr "" @@ -936,15 +960,15 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:380 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:503 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506 msgid "Select the list type for adding custom domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:583 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586 msgid "Select the list type for adding custom subnets" msgstr "" @@ -1025,24 +1049,24 @@ msgstr "" msgid "Source Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:489 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:495 msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:754 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:708 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:711 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:731 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:685 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:688 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1090,8 +1114,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:507 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:510 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590 msgid "Text List" msgstr "" @@ -1111,6 +1135,10 @@ msgstr "" msgid "The URL used to test server connectivity" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:465 +msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:114 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" @@ -1135,7 +1163,7 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" @@ -1191,27 +1219,35 @@ msgstr "" msgid "URLTest Tolerance" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:502 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:486 +msgid "Use this only when the router has working IPv6 connectivity." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316 +msgid "Use with Exclusion sections to route specific domains directly." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505 msgid "User Domain List Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:517 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:540 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:582 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585 msgid "User Subnet List Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:597 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:620 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:623 msgid "User Subnets List" msgstr "" @@ -1222,7 +1258,8 @@ msgstr "" #: src/validators/validateDomain.ts:30 #: src/validators/validateHysteriaUrl.ts:120 #: src/validators/validateIp.ts:8 -#: src/validators/validateIp.ts:24 +#: src/validators/validateIp.ts:91 +#: src/validators/validateIp.ts:97 #: src/validators/validateOutboundJson.ts:7 #: src/validators/validatePath.ts:16 #: src/validators/validateShadowsocksUrl.ts:95 @@ -1236,8 +1273,8 @@ msgstr "" msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:652 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:655 msgid "Validation errors:" msgstr "" @@ -1256,14 +1293,18 @@ msgstr "" msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:450 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:453 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:469 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:312 +msgid "When enabled, traffic not matching any other section's lists will go through this proxy." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80 msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "" diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index cd3e4643..2396a689 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -732,6 +732,7 @@ stop_main() { } start() { + local start_rc dont_touch_dhcp start_main start_rc=$? @@ -751,8 +752,28 @@ start() { uci_set "netshift" "settings" "shutdown_correctly" 0 uci commit "netshift" && config_load "$NETSHIFT_CONFIG" + start_sing_box_monitor +} + +start_sing_box_monitor() { + local pidfile="/var/run/netshift_monitor.pid" + local pid + + # Guard against double-start / procd re-trigger: if a monitor is already + # alive, do not spawn a second one (the old pid would be lost, orphaning a + # monitor that stop() can no longer kill). Mirror the pidfile-guard idiom + # used by start_subscription_startup_retry_worker. + if [ -f "$pidfile" ]; then + pid="$(cat "$pidfile" 2> /dev/null)" + if [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null; then + log "sing-box health monitor is already running with PID $pid" "debug" + return 0 + fi + rm -f "$pidfile" + fi + monitor_sing_box & - echo $! > /var/run/netshift_monitor.pid + echo $! > "$pidfile" log "Started sing-box health monitor with PID $!" "info" } @@ -782,6 +803,7 @@ stop() { monitor_sing_box() { local crash_count=0 local backoff=0 + local dont_touch_dhcp while true; do sleep "$MONITOR_CHECK_INTERVAL" @@ -824,8 +846,7 @@ monitor_sing_box() { log "Attempting sing-box recovery restart" "warn" stop_main - start_main - if [ $? -eq 0 ]; then + if start_main; then config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 if [ "$dont_touch_dhcp" -eq 0 ]; then dnsmasq_configure force @@ -968,9 +989,6 @@ create_nft_rules() { }' fi - log "Create common set" - nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" - log "Create interface set" nft_init_interfaces_set @@ -990,10 +1008,19 @@ create_nft_rules() { nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto tcp tproxy ip to "$SB_TPROXY_INBOUND_ADDRESS:$SB_TPROXY_INBOUND_PORT" counter nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto udp tproxy ip to "$SB_TPROXY_INBOUND_ADDRESS:$SB_TPROXY_INBOUND_PORT" counter if netshift_ipv6_enabled; then - nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto tcp tproxy ip6 to "$SB_TPROXY_INBOUND_ADDRESS_V6:$SB_TPROXY_INBOUND_PORT_V6" counter - nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto udp tproxy ip6 to "$SB_TPROXY_INBOUND_ADDRESS_V6:$SB_TPROXY_INBOUND_PORT_V6" counter - fi - + nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto tcp tproxy ip6 to "[$SB_TPROXY_INBOUND_ADDRESS_V6]:$SB_TPROXY_INBOUND_PORT_V6" counter + nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto udp tproxy ip6 to "[$SB_TPROXY_INBOUND_ADDRESS_V6]:$SB_TPROXY_INBOUND_PORT_V6" counter + fi + + # Router-originated (locally generated) traffic is left DIRECT on purpose. + # The model marks only LAN/forwarded traffic in `mangle` (prerouting) above + # and delegates the proxy/direct split to sing-box route rules; the router's + # own outgoing traffic is NOT proxied. These mangle_output rules only handle + # the returns needed to keep that traffic direct: local/loopback daddr returns + # and the outbound-mark return (sing-box-originated packets carrying + # NFT_OUTBOUND_MARK must not be re-marked into the tproxy path -> avoids a + # routing loop). Proxying router-originated traffic (the old mangle_output + # @common/FakeIP daddr marking) was dropped deliberately to avoid such loops. nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_LOCALV4_SET_NAME" return if netshift_ipv6_enabled; then nft add rule inet "$NFT_TABLE_NAME" mangle_output ip6 daddr "@$NFT_LOCALV6_SET_NAME" return @@ -1028,20 +1055,22 @@ uci_remove_quiet() { } dnsmasq_is_configured_for_netshift() { - local servers server noresolv cachesize has_netshift_server=0 - - servers="$(uci_get "dhcp" "@dnsmasq[0]" "server")" - for server in $servers; do - if [ "$server" = "$SB_DNS_INBOUND_ADDRESS" ]; then - has_netshift_server=1 - break - fi - done - - noresolv="$(uci_get "dhcp" "@dnsmasq[0]" "noresolv")" - cachesize="$(uci_get "dhcp" "@dnsmasq[0]" "cachesize")" - - [ "$has_netshift_server" -eq 1 ] && [ "$noresolv" = "1" ] && [ "$cachesize" = "0" ] + local configured + + # The authoritative "netshift owns this dnsmasq config" flag is the + # netshift_configured sentinel that dnsmasq_configure sets unconditionally + # AFTER applying our config, and dnsmasq_restore clears on teardown. We do + # NOT infer ownership from the live server/noresolv/cachesize values: on a + # stock dnsmasq with no original server/noresolv/cachesize, dnsmasq_configure + # writes no backup markers, and after it runs the live values ARE netshift's + # own (noresolv=1, cachesize=0) — so a value-based or marker-based check + # could not distinguish "we configured it" from "an admin coincidentally set + # the same values", and the redundant `dnsmasq_configure force` path (monitor + # recovery / double-start) would re-run "backup" and capture netshift's own + # values as the backup, corrupting the later restore. The sentinel is the + # single source of truth. + configured="$(uci_get "dhcp" "@dnsmasq[0]" "netshift_configured")" + [ "$configured" = "1" ] } dnsmasq_configure() { @@ -1076,6 +1105,10 @@ dnsmasq_configure() { 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 + # Authoritative ownership sentinel (see dnsmasq_is_configured_for_netshift): + # marks that netshift, not the admin, applied this dnsmasq config. Set + # unconditionally after our config is applied; cleared in dnsmasq_restore. + uci_set "dhcp" "@dnsmasq[0]" "netshift_configured" 1 uci_commit "dhcp" /etc/init.d/dnsmasq restart @@ -1130,6 +1163,11 @@ dnsmasq_restore() { log "Backup DNS servers and default resolvfile not found, possible resolving issues" "warn" fi + # Clear the ownership sentinel so a fresh future dnsmasq_configure + # re-establishes ownership cleanly (and dnsmasq_is_configured_for_netshift + # no longer short-circuits once we have torn down). + uci_remove_quiet "dhcp" "@dnsmasq[0]" "netshift_configured" + uci_commit "dhcp" /etc/init.d/dnsmasq restart @@ -2377,7 +2415,6 @@ configure_user_subnet_list() { items="$(parse_domain_or_subnet_string_to_commas_string "$items" "subnets")" json_array="$(comma_string_to_json_array "$items")" patch_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array" - nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items" } configure_local_domain_lists() { @@ -2420,7 +2457,6 @@ import_local_subnets_list_handler() { fi import_plain_subnet_list_to_local_source_ruleset_chunked "$local_subnet_list_filepath" "$ruleset_filepath" - nft_add_set_elements_from_file_chunked "$local_subnet_list_filepath" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" } configure_remote_domain_or_subnet_list_handler() { @@ -2617,31 +2653,14 @@ import_community_subnet_lists() { import_community_service_subnet_list_handler() { local service="$1" + # Routing for every community service is carried by a sing-box remote rule + # set (see configure_community_list_handler -> $SRS_MAIN_URL/<service>.srs). + # Only discord still needs an nft path here: it has a live dport-restricted + # mangle rule (@netshift_discord_subnets udp dport {...}) that a sing-box + # route rule cannot express, so its subnets must populate that nft set. + # The other services formerly populated @netshift_subnets, but that set was + # matched by NO nft rule (dead path) and is no longer created/populated. case "$service" in - "twitter") - URL=$SUBNETS_TWITTER - ;; - "meta") - URL=$SUBNETS_META - ;; - "telegram") - URL=$SUBNETS_TELERAM - ;; - "cloudflare") - URL=$SUBNETS_CLOUDFLARE - ;; - "hetzner") - URL=$SUBNETS_HETZNER - ;; - "ovh") - URL=$SUBNETS_OVH - ;; - "digitalocean") - URL=$SUBNETS_DIGITALOCEAN - ;; - "cloudfront") - URL=$SUBNETS_CLOUDFRONT - ;; "discord") URL=$SUBNETS_DISCORD if ! nft list set inet "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" > /dev/null 2>&1; then @@ -2653,9 +2672,6 @@ import_community_service_subnet_list_handler() { "@$NFT_DISCORD_SET_NAME" udp dport '{ 19000-20000, 50000-65535 }' meta mark set "$NFT_FAKEIP_MARK" counter fi ;; - "roblox") - URL=$SUBNETS_ROBLOX - ;; *) return 0 ;; esac @@ -2670,11 +2686,7 @@ import_community_service_subnet_list_handler() { return 1 fi - if [ "$service" = "discord" ]; then - nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" - else - nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" - fi + nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" rm -f "$tmpfile" } @@ -2752,13 +2764,12 @@ import_subnets_from_remote_subnet_list_handler() { file_extension="$(url_get_file_extension "$url")" log "Detected file extension: '$file_extension'" "debug" case "$file_extension" in - json) - log "Import subnets from a remote JSON list" "info" - import_subnets_from_remote_json_file "$url" - ;; - srs) - log "Import subnets from a remote SRS list" "info" - import_subnets_from_remote_srs_file "$url" + json | srs) + # JSON/SRS remote subnet lists are routed via a sing-box remote rule set + # (configure_remote_domain_or_subnet_list_handler -> sing_box_cm_add_remote_ruleset), + # and sing-box manages their updates automatically. The previous import + # here only fed the dead nft @netshift_subnets set, so it is dropped. + log "No subnet import needed for $file_extension list - sing-box manages updates automatically." ;; *) log "Import subnets from a remote plain-text list" "info" @@ -2767,51 +2778,6 @@ import_subnets_from_remote_subnet_list_handler() { esac } -import_subnets_from_remote_json_file() { - local url="$1" - local json_tmpfile subnets_tmpfile http_proxy_address - json_tmpfile="$(mktemp)" - subnets_tmpfile="$(mktemp)" - http_proxy_address="$(get_service_proxy_address)" - - download_to_file "$url" "$json_tmpfile" "$http_proxy_address" - - if [ $? -ne 0 ] || [ ! -s "$json_tmpfile" ]; then - log "Download $url list failed" "error" - return 1 - fi - - extract_ip_cidr_from_json_ruleset_to_file "$json_tmpfile" "$subnets_tmpfile" - nft_add_set_elements_from_file_chunked "$subnets_tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" - rm -f "$json_tmpfile" "$subnets_tmpfile" -} - -import_subnets_from_remote_srs_file() { - local url="$1" - - local binary_tmpfile json_tmpfile subnets_tmpfile http_proxy_address - binary_tmpfile="$(mktemp)" - json_tmpfile="$(mktemp)" - subnets_tmpfile="$(mktemp)" - http_proxy_address="$(get_service_proxy_address)" - - download_to_file "$url" "$binary_tmpfile" "$http_proxy_address" - - if [ $? -ne 0 ] || [ ! -s "$binary_tmpfile" ]; then - log "Download $url list failed" "error" - return 1 - fi - - if ! decompile_binary_ruleset "$binary_tmpfile" "$json_tmpfile"; then - log "Failed to decompile binary rule set file" "error" - return 1 - fi - - extract_ip_cidr_from_json_ruleset_to_file "$json_tmpfile" "$subnets_tmpfile" - nft_add_set_elements_from_file_chunked "$subnets_tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" - rm -f "$binary_tmpfile" "$json_tmpfile" "$subnets_tmpfile" -} - import_subnets_from_remote_plain_file() { local url="$1" local section="$2" @@ -2832,7 +2798,6 @@ import_subnets_from_remote_plain_file() { ruleset_tag=$(get_ruleset_tag "$section" "remote" "subnets") ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json" import_plain_subnet_list_to_local_source_ruleset_chunked "$tmpfile" "$ruleset_filepath" - nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" rm -f "$tmpfile" } @@ -3241,7 +3206,7 @@ check_nft() { if [ "$found_hetzner" -eq 1 ] || [ "$found_ovh" -eq 1 ]; then - local sets="netshift_subnets netshift_domains interfaces netshift_discord_subnets localv4" + local sets="netshift_domains interfaces netshift_discord_subnets localv4" nolog "Sets statistics:" for set_name in $sets; do diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 2aae11d1..651c8844 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -31,7 +31,6 @@ RT_TABLE_NAME="netshift" NFT_TABLE_NAME="NetShiftTable" NFT_LOCALV4_SET_NAME="localv4" NFT_LOCALV6_SET_NAME="localv6" -NFT_COMMON_SET_NAME="netshift_subnets" NFT_DISCORD_SET_NAME="netshift_discord_subnets" NFT_INTERFACE_SET_NAME="interfaces" NFT_FAKEIP_MARK="0x00100000" @@ -107,12 +106,4 @@ SUBNETS_HETZNER="${GITHUB_RAW_URL}/Subnets/IPv4/hetzner.lst" SUBNETS_OVH="${GITHUB_RAW_URL}/Subnets/IPv4/ovh.lst" SUBNETS_DIGITALOCEAN="${GITHUB_RAW_URL}/Subnets/IPv4/digitalocean.lst" SUBNETS_CLOUDFRONT="${GITHUB_RAW_URL}/Subnets/IPv4/cloudfront.lst" -SUBNETS_TWITTER_V6="${GITHUB_RAW_URL}/Subnets/IPv6/twitter.lst" -SUBNETS_META_V6="${GITHUB_RAW_URL}/Subnets/IPv6/meta.lst" -SUBNETS_DISCORD_V6="${GITHUB_RAW_URL}/Subnets/IPv6/discord.lst" -SUBNETS_CLOUDFLARE_V6="${GITHUB_RAW_URL}/Subnets/IPv6/cloudflare.lst" -SUBNETS_HETZNER_V6="${GITHUB_RAW_URL}/Subnets/IPv6/hetzner.lst" -SUBNETS_OVH_V6="${GITHUB_RAW_URL}/Subnets/IPv6/ovh.lst" -SUBNETS_DIGITALOCEAN_V6="${GITHUB_RAW_URL}/Subnets/IPv6/digitalocean.lst" -SUBNETS_CLOUDFRONT_V6="${GITHUB_RAW_URL}/Subnets/IPv6/cloudfront.lst" COMMUNITY_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube hdrezka tiktok google_ai google_play hodca discord meta twitter cloudflare cloudfront digitalocean hetzner ovh telegram roblox" diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index 30123b6c..e940502f 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -13,30 +13,6 @@ is_ipv4_cidr() { echo "$ip" | grep -Eq "$regex" } -is_ipv6() { - local ip="$1" - local regex='^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$' - echo "$ip" | grep -Eq "$regex" -} - -is_ipv6_cidr() { - local ip="$1" - local addr mask - addr="${ip%/*}" - mask="${ip#*/}" - - case "$ip" in - */*) ;; - *) return 1 ;; - esac - - is_ipv6 "$addr" && [ "$mask" -ge 0 ] 2>/dev/null && [ "$mask" -le 128 ] 2>/dev/null -} - -is_ip() { - is_ipv4 "$1" || is_ipv6 "$1" -} - is_ipv4_ip_or_ipv4_cidr() { is_ipv4 "$1" || is_ipv4_cidr "$1" } diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index a59020a2..5c29708f 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -9,8 +9,8 @@ # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, -# diagnostics, subscription, rejected, jobstate, selfheal, -# dnsdetour +# nftv6, diagnostics, subscription, rejected, jobstate, +# selfheal, dnsdetour, globalproxy # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 1fa815c5..4e14bd84 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -328,6 +328,104 @@ test_nft() { fi } +# ───────────────────────────────────────────────────────────────── +# Test: NFT IPv6 TProxy regression (B-01 blocker guard) +# +# The PR #11 IPv6 tproxy rule was emitted UNBRACKETED +# (`tproxy ip6 to ::1:1603`), which nft silently normalizes to a +# portless bare address (observed forms: `[::0.1.22.3]` on nftables +# v1.1.3 where 1603 == 0x1603, and `[::1:1603]` on OpenWRT 24.10.6). +# Either way the port is lost. The backend fix emits the BRACKETED +# form `[::1]:1603`. This test pins that +# contract at the real nft level so any revert fails the suite. +# Constants drive rule construction; the expected normalized +# `[::1]:1603` literal is the contract we deliberately hardcode. +# ───────────────────────────────────────────────────────────────── +test_nft_ipv6() { + header "NFT IPv6 TProxy Regression" + + if ! command -v nft > /dev/null 2>&1; then + skip "nft not available" + return + fi + + local constants="${NETSHIFT_LIB_DIR}/constants.sh" + if [ ! -f "$constants" ]; then + fail "constants.sh not found at $constants" + return + fi + + # Source the real runtime contract values (v6 tproxy addr/port). + # shellcheck disable=SC1090 + . "$constants" + + if [ -z "$SB_TPROXY_INBOUND_ADDRESS_V6" ] || [ -z "$SB_TPROXY_INBOUND_PORT_V6" ]; then + fail "v6 tproxy constants missing (SB_TPROXY_INBOUND_ADDRESS_V6/_PORT_V6)" + return + fi + + local test_table="netshift_v6_test_$$" + + # ── ipv6_addr interval set + v6 element (mirrors the ipv4 set test) ── + if nft add table inet "$test_table" 2>/dev/null && \ + nft add set inet "$test_table" testset6 '{ type ipv6_addr; flags interval; auto-merge; }' 2>/dev/null && \ + nft add element inet "$test_table" testset6 '{ fc00::/7 }' 2>/dev/null; then + pass "nft-v6-set-element:OK (ipv6_addr interval set + fc00::/7)" + else + fail "nft-v6-set-element:FAIL (ipv6_addr interval set / element insert failed)" + fi + nft delete table inet "$test_table" 2>/dev/null + + # ── v6 tproxy rule: build EXACTLY as the backend emits, list back ── + # Capability-gate: if adding the rule errors for a kernel/capability + # reason (no ip6 tproxy support), skip instead of false-failing. + local add_err="" + nft add table inet "$test_table" 2>/dev/null + nft add chain inet "$test_table" proxy \ + '{ type filter hook prerouting priority -100; policy accept; }' 2>/dev/null + # A v6 daddr return rule (exclusion) coexisting with the tproxy rule. + nft add rule inet "$test_table" proxy ip6 daddr fc00::/7 counter return 2>/dev/null + + if add_err="$(nft add rule inet "$test_table" proxy meta l4proto tcp \ + tproxy ip6 to "[$SB_TPROXY_INBOUND_ADDRESS_V6]:$SB_TPROXY_INBOUND_PORT_V6" counter 2>&1)"; then + local listed="" + listed="$(nft list chain inet "$test_table" proxy 2>/dev/null)" + + # Positive: MUST normalize to the bracketed [::1]:1603 form. + if echo "$listed" | grep -q 'tproxy ip6 to \[::1\]:1603'; then + pass "nft-v6-tproxy-bracketed:OK (normalizes to [::1]:1603)" + else + fail "nft-v6-tproxy-bracketed:FAIL (expected [::1]:1603)" \ + "$(echo "$listed" | grep -i 'tproxy ip6' || echo "$listed")" + fi + + # Negative guard: a buggy/portless bare v6 dest must NOT appear. + # The unbracketed `::1:1603` is parsed as a bare address (no port); + # nft re-prints it bracketed but mangled. Observed normalizations: + # nftables v1.1.3: tproxy ip6 to [::0.1.22.3] (1603 -> 0x1603) + # OpenWRT 24.10.6: tproxy ip6 to [::1:1603] (no `]:` port sep) + # So the robust marker is: a `tproxy ip6 to [...]` line that is NOT + # the correct `[::1]:1603`. Such a line is the bug; its absence is OK. + if echo "$listed" | grep 'tproxy ip6 to \[' | grep -qv '\[::1\]:1603'; then + fail "nft-v6-tproxy-no-bare:FAIL (buggy portless bare form present)" \ + "$(echo "$listed" | grep -i 'tproxy ip6')" + else + pass "nft-v6-tproxy-no-bare:OK (no portless bare ip6 form)" + fi + else + case "$add_err" in + *[Nn]ot\ supported*|*[Oo]peration\ not\ supported*|*[Nn]o\ such\ file*) + skip "nft-v6-tproxy: kernel lacks ip6 tproxy support ($add_err)" + ;; + *) + fail "nft-v6-tproxy:FAIL (rule add failed unexpectedly)" "$add_err" + ;; + esac + fi + + nft delete table inet "$test_table" 2>/dev/null +} + # ───────────────────────────────────────────────────────────────── # Test: sing-box Config Generation # ───────────────────────────────────────────────────────────────── @@ -685,7 +783,7 @@ echo "$out2" | jq -e '.outbounds[0].alter_id == 64' >/dev/null 2>&1 && echo 'cm- doh_cfg='{"route":{"rules":[],"rule_set":[]}}' doh_out=$(sing_box_cm_add_doh_block_route_rule "$doh_cfg" "doh-block" "tproxy-in" \ "1.1.1.1/32 8.8.8.8/32" "2606:4700:4700::1111/128 2001:4860:4860::8888/128") -echo "$doh_out" | jq -e '.route.rule_set[0].rules[0].ip_cidr | index("1.1.1.1/32") and index("2606:4700:4700::1111/128")' >/dev/null 2>&1 && echo 'cm-doh-cidrs-v4-v6:OK' || echo 'cm-doh-cidrs-v4-v6:FAIL' +echo "$doh_out" | jq -e '.route.rule_set[0].rules[0].ip_cidr | (index("1.1.1.1/32") != null) and (index("2606:4700:4700::1111/128") != null)' >/dev/null 2>&1 && echo 'cm-doh-cidrs-v4-v6:OK' || echo 'cm-doh-cidrs-v4-v6:FAIL' echo "$doh_out" | jq -e '.route.rules[0].action == "reject" and .route.rules[0].rule_set == "doh-block-ruleset" and .route.rules[0].inbound == "tproxy-in"' >/dev/null 2>&1 && echo 'cm-doh-route-rule:OK' || echo 'cm-doh-route-rule:FAIL' echo 'DONE' @@ -2520,6 +2618,7 @@ main() { test_config_manager test_sing_box_config test_nft + test_nft_ipv6 test_diagnostics test_subscription test_rejected_hash @@ -2533,6 +2632,7 @@ main() { config) test_config ;; helpers) test_helpers ;; nft) test_nft ;; + nftv6) test_nft_ipv6 ;; diagnostics) test_diagnostics ;; subscription) test_subscription ;; rejected) test_rejected_hash ;; @@ -2545,7 +2645,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft diagnostics subscription rejected jobstate selfheal dnsdetour globalproxy" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription rejected jobstate selfheal dnsdetour globalproxy" exit 1 ;; esac From f92408b1365aa38cd851cc358fb15197c16674ca Mon Sep 17 00:00:00 2001 From: "spgsroot, yandexru45" <> Date: Sat, 6 Jun 2026 22:44:52 +0300 Subject: [PATCH 55/75] =?UTF-8?q?=D0=94=D0=BE=D0=BF=20=D0=B2=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=B0=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 63 ++ .../memory/luci-frontend-developer.md | 91 +++ .../memory/shell-backend-developer.md | 69 ++ fe-app-netshift/locales/calls.json | 200 ++++- fe-app-netshift/locales/netshift.pot | 155 +++- fe-app-netshift/locales/netshift.ru.po | 57 +- .../src/netshift/methods/shell/index.ts | 83 ++ .../shell/parseComponentCheckUpdate.ts | 58 ++ .../tests/parseComponentCheckUpdate.test.js | 54 ++ .../src/netshift/services/store.service.ts | 23 +- .../tabs/diagnostic/diagnostic.store.ts | 3 - .../tabs/diagnostic/initController.ts | 53 -- .../partials/renderAvailableActions.ts | 13 - fe-app-netshift/src/netshift/tabs/index.ts | 1 + .../src/netshift/tabs/manager/cards.ts | 262 ++++++ .../src/netshift/tabs/manager/index.ts | 9 + .../netshift/tabs/manager/initController.ts | 434 ++++++++++ .../netshift/tabs/manager/manager.store.ts | 20 + .../src/netshift/tabs/manager/render.ts | 8 + .../src/netshift/tabs/manager/styles.ts | 109 +++ .../netshift/tabs/manager/tests/cards.test.js | 207 +++++ fe-app-netshift/src/netshift/types.ts | 19 + fe-app-netshift/src/styles.ts | 3 +- .../resources/view/netshift/main.js | 767 ++++++++++++++++-- .../resources/view/netshift/manager.js | 22 + .../resources/view/netshift/netshift.js | 18 + luci-app-netshift/po/ru/netshift.po | 57 +- luci-app-netshift/po/templates/netshift.pot | 155 +++- netshift/files/usr/lib/constants.sh | 15 + netshift/files/usr/lib/updater.sh | 332 ++++++++ tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 416 +++++++++- 32 files changed, 3522 insertions(+), 256 deletions(-) create mode 100644 fe-app-netshift/src/netshift/methods/shell/parseComponentCheckUpdate.ts create mode 100644 fe-app-netshift/src/netshift/methods/shell/tests/parseComponentCheckUpdate.test.js create mode 100644 fe-app-netshift/src/netshift/tabs/manager/cards.ts create mode 100644 fe-app-netshift/src/netshift/tabs/manager/index.ts create mode 100644 fe-app-netshift/src/netshift/tabs/manager/initController.ts create mode 100644 fe-app-netshift/src/netshift/tabs/manager/manager.store.ts create mode 100644 fe-app-netshift/src/netshift/tabs/manager/render.ts create mode 100644 fe-app-netshift/src/netshift/tabs/manager/styles.ts create mode 100644 fe-app-netshift/src/netshift/tabs/manager/tests/cards.test.js create mode 100644 luci-app-netshift/htdocs/luci-static/resources/view/netshift/manager.js diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 817f29db..fa6973ad 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -353,3 +353,66 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> smoke `all` = 84 passed / 0 failed (was 81; +3 nft v6 regression assertions); whole-chain `unshare -rn` confirms v6 tproxy normalizes to [::1]:1603. All 3 layers code-reviewer APPROVED. Ready for human commit (agents never auto-commit). + +## Component Manager feature (task-017 backend + task-018 frontend, 2026-06-06) + +- New LuCI tab "Component Manager" (RU "Менеджер компонентов"): 3 cards + (NetShift / sing-box stock / sing-box extended) with installed version shown + immediately + on-demand "Check update" + status badges + update/core-switch/ + self-update actions. Core-switch MOVED out of Diagnostics into here. +- Reference impl = `podkop-plus/` (operator clones it to repo root when needed; + it is UNTRACKED and NOT gitignored — must NOT be added to a commit). Paths: + `podkop-plus/fe-app-podkop/src/podkop/tabs/updates/{index,render,initController, + styles}.ts` + `podkop-plus/luci-app-podkop-plus/htdocs/.../view/podkop/updates.js` + (note `luci-app-podkop-plus` dir + `view.podkop_plus.main`; OUR view requires + `view.netshift.main`). The card/styles pattern is the theme-CSS-vars-with- + fallback approach (var(--success-color-medium, green) etc) — safe for custom + LuCI themes. +- Backend (task-017, updater.sh): two NEW component_action sub-cases (the + dispatcher is component_action() :1272, a `case "$comp:$action"`; that is the + ONLY extension point — component_action_async/_status are component-agnostic, + no dispatcher change for new actions). Added `sing_box:check_update_stable` + (sync) + `netshift:self_update` (async via component_action_async). Self-update + = Variant A: targeted pkg upgrade (download release .ipk/.apk + pkg_install), + NOT install.sh (interactive `read`). MUST mirror the updates_install_sing_box_ + extended epilogue (:878-903): reset UPDATES_HEAL_* -> ensure_connectivity + "extended" -> _core to /tmp file + rc -> ALWAYS updates_restore_after_swap -> + re-emit JSON -> return rc. NEVER exit on recoverable fail (echo failure JSON + + return nonzero). Minimal /etc/config/netshift backup. RU i18n only if installed. +- SELF-REPLACEMENT (critical, verified safe): the netshift pkg replaces the very + /usr/bin/netshift running the worker. The async fork runs `"$0" component_action + netshift self_update` in `( trap '' HUP; ... ) &`; busybox ash holds the whole + script in memory, and updates_write_finished_job_state runs in the SAME subshell + AFTER the worker returns — both complete from memory despite the on-disk swap. + RULE: the self_update worker must contain NO exec / NO "$0" / NO re-invoke of + /usr/bin/netshift / NO updates_restart_netshift after pkg_install. (Only + /etc/init.d/netshift start via restore, AFTER install, as a fresh process — ok.) +- updater.sh does NOT source install.sh -> re-implement the tiny pkg helpers + locally with the `updates_` prefix (updates_pkg_is_apk/_install_file/ + _is_installed/_candidate_version). pkg output parsed with cut/awk/grep (no + Oniguruma). Stock candidate via opkg info/list or apk list; >= compare via + is_min_package_version (sort -V) on leading semver ${v%%-*}. +- STABLE cross-layer contract: check_update_stable -> {success,current_version, + latest_version,status:"latest"|"outdated"|"not_installed"}; self_update finished + -> {success,version,message}; versions from get_system_info (netshift_version, + netshift_latest_version, sing_box_version "not installed" when absent, + sing_box_extended 0|1). ACL already allows fs.exec /usr/bin/netshift -> no ACL + change for component_action. +- FRONTEND landmine caught by review (C1): NetShift's "Check update" has NO + backend check action (there is no netshift:check_update). NetShift latest comes + ONLY from get_system_info.netshift_latest_version. A card whose "latest" comes + from a DIFFERENT source than a sibling MUST use a DISTINCT action kind + (`check_netshift`, no backendAction) that refreshes systemInfo — never route it + through the sing-box check method or write a sing-box result into its check + slice. Generalize: when mirroring a multi-card update pattern, verify EACH + card's check actually targets ITS OWN backend source. +- Lenient mid-job polling for self_update: the poll's fetchStatus swallows exec/ + parse errors and returns synthetic {running:true} (NOT null) so the mid-job + binary swap isn't misreported as failure; scoped strictly AFTER a job_id is + obtained (a failed START still surfaces), bounded by MAX_POLLS; success -> + warning toast + window.location.reload(). +- FINAL gates: shellcheck clean; yarn ci 465 tests; main.js idempotent (two builds + byte-identical) + no yarn pollution + i18n catalogs byte-identical (fe<->luci); + smoke all = 101 passed / 0 failed (84 -> +17 new: stablecheck x4 + selfupdate + x13). Both layers code-reviewer APPROVED (backend 1st pass; frontend after a + C1/S1 fix round). Ready for human commit. diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 53b1befa..6e4ae473 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -262,3 +262,94 @@ append findings; keep under ~200 lines. - The 10 new ru fragments are the split sentences of the 3 flag descriptions (global proxy / block DoH / enable IPv6) — translated formally/technically matching neighbours. All catalogs LF, no empty non-header msgstr remained. + +## Component Manager tab (task-018) + +- NEW TAB = 5-file pattern mirroring `tabs/diagnostic`: `manager/{index,render, + initController,styles}.ts` + a hand-written `view/netshift/manager.js` + (`form.DummyValue _mount_node`, `o.rawhtml=true`, `cfgvalue` → + `main.ManagerTab.initController()` + `return main.ManagerTab.render()`). + Register in `netshift.js`: add `"require view.netshift.manager as manager"` + + a `form.TypedSection` block (`anonymous=true`, `addremove=false`, + `cfgsections=()=>["manager"]`). NB the tab UCI section name (`manager`, + `diagnostic`, `dashboard`) need NOT exist in `/etc/config/netshift` — LuCI + renders the virtual TypedSection anyway (diagnostic/dashboard prove it). +- Barrel: add `export * from './manager';` to `tabs/index.ts` → `ManagerTab` + reaches `main.ManagerTab` (verified in the export block at the bottom of + main.js). Wire `ManagerTab.styles` into `src/styles.ts` `GlobalStyles` next to + Dashboard/Diagnostic. Styles use theme vars WITH fallbacks; CBI selector is + `#cbi-netshift-manager-_mount_node > div` + hide `#cbi-netshift-manager > h3`. +- LIFECYCLE: mirror diagnostic exactly but guard re-init with module-level + `*Registered`/`*Initialized`/`*Mounted` booleans (podkop-plus style) since the + lazy-mount listener can fire repeatedly. `onMount('manager-status')` → + `registerLifecycleListeners()` (subscribe on `tabService.current==='manager'`) + → `onPageMount` subscribes store + renders + fetches systemInfo; + `onPageUnmount` resets `['managerActions','managerChecks']`. +- INSTALLED-NOW / LATEST-ON-DEMAND: installed versions come from + `diagnosticsSystemInfo` (reuse the diagnostic `getSystemInfo()`→store slice); + latest is fetched ONLY on a "Check update" click. Pure card builder + `cards.ts:getComponentCards(systemInfo, checks)` + `getCheckTag(status)` + + `isSingBoxInstalled` are DOM/store-free (import only `normalizeCompiledVersion` + leaf + `NetShift` types) → unit-testable WITHOUT the MutationObserver collect + crash. Controller maps descriptors→DOM+click handlers. 3 cards: netshift, + sing_box_stock, sing_box_extended; inactive core → "Not installed" + switch. +- BACKEND CONTRACT (task-017, STABLE): both SING-BOX checks (`check_update` + extended / `check_update_stable` stock) return `{success,current_version, + latest_version,status:"latest"|"outdated"|"dev"|"not_installed"}`. The + PRE-EXISTING `singBoxComponentAction('check_update')` only parsed `{success, + version,message}` (DROPPED status/latest) — do NOT route the manager check + through it. Added ONE `singBoxCheckUpdate(action)` method parsing the FULL + contract via a pure `parseComponentCheckUpdate.ts` (types-only import, status + whitelisted). Install/switch reuse the existing async + `singBoxComponentAction('install_*')` + `pollSingBoxComponentAction`. +- C1 (review fix) — THERE IS NO `netshift:check_update` BACKEND ACTION. NetShift + latest comes ONLY from `get_system_info.netshift_latest_version`. So the + NetShift card's "Check update" must NOT route through `singBoxCheckUpdate` (that + would run the sing-box EXTENDED check and write its status into + `managerChecks.netshift` → wrong). Fix: give the NetShift check a DISTINCT + `kind:'check_netshift'` (no `backendAction`) so `handleManagerAction` routes it + to `runNetshiftCheck`, which RE-FETCHES systemInfo + `resetCheckResult + ('netshift')` and derives the badge/toast from installed-vs-latest. NetShift + status is derived PURELY from systemInfo (`netshiftStatus(systemInfo)` no longer + reads `managerChecks.netshift`). Regression guards in cards.test.js: NetShift + action kind is `check_netshift`, has NO `backendAction`, and status ignores a + bogus `managerChecks.netshift`. LESSON: when a card's "latest" comes from a + DIFFERENT source than its siblings, give it its own action kind so the shared + dispatcher can't misroute it to the wrong backend method. +- S1 (review fix) — keep ONE check method per concern: removed the dead + `singBoxCheckUpdateStable()` (0 callers; controller uses + `singBoxCheckUpdate('check_update_stable')`). No dead exports. +- M2 (review fix) — `ManagerComponentKey` defined ONCE in `tabs/manager/cards.ts` + (the pure module) and `export type`-re-exported from `store.service.ts` (which + `import type`s it) so store consumers keep their path; safe because cards.ts + imports NO store (no cycle). M1 (self-update timeout reusing 'Core switch + timed out' wording) left as-is — non-blocking, and the shared + `pollSingBoxComponentAction` wording is approved for the core-switch path. +- SELF-UPDATE lenient polling: `netshiftSelfUpdate()` starts + `component_action_async netshift self_update`; once a `job_id` is returned, the + poll `fetchStatus` callback SWALLOWS exec/parse errors and returns a synthetic + `{running:true}` (instead of `null`, which the pure poll treats as terminal + failure). This prevents the mid-job `/usr/bin/netshift` binary swap from + misreporting success as failure; `MAX_POLLS` still bounds it. On success: a + warning-style toast then `window.location.reload()` after 1200ms. +- MOVED core-switch OUT of Diagnostics: removed `handleInstallSingBox` + + `singBoxInstall`/`singBoxExtended` from `diagnostic/initController.ts` and the + `singBoxInstall` block + props from `renderAvailableActions.ts`, and the + `singBoxInstall` slice from BOTH `StoreType` and `diagnostic.store.ts`. Net + i18n effect: msgids "Install stable"/"Install extended" were DROPPED (now-dead) + and replaced by manager's "Switch to stable"/"Switch to extended"/"Install %s" + — so the ru.po diff is NOT purely additive this time (2 removed, ~16 added); + that is correct. `renderRotateCcwIcon24` stays imported (still used by Restart). +- i18n: 16 new ru msgstrs filled in SOURCE `locales/netshift.ru.po`, then + `node distribute-locales.js`. "%s" placeholder strings ("Install %s") are + single literals (no concat); progress/result toasts concatenate the version + OUTSIDE `_()` (`` `${_('NetShift updated, version:')} ${v}` ``). Ran the + locales scripts via `node {extract-calls,generate-pot,generate-po ru, + distribute-locales}.js`. yarn here was classic 1.22.22 (NOT corepack) so + `yarn ci` was safe — verified yarn.lock unchanged + no `.yarn/.yarnrc.yml`. +- main.js: +756/-… runtime diff (new tab + methods + core-switch removal), + second build idempotent (byte-identical), banner + `return baseclass.extend` + intact, only `ManagerTab` added to the export block (pure helpers imported by + direct path → no leak). `tsc --noEmit` flags ONE pre-existing error in + `getNetshiftVersionRow.test.ts` (sing_box_extended optionality) — NOT in CI + (yarn ci = format/lint/vitest/build, no tsc), pre-existing, ignore. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index d4cc185e..fb31c967 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -478,3 +478,72 @@ findings; keep under ~200 lines. - shellcheck -S error clean (bin + libs + install.sh); `smoke-tests all` = 81 passed / 0 failed (unchanged baseline). No new smoke test (separate packaging task owns nft/v6 coverage per spec). No sacred constant VALUES changed. + +## task-017: Component Manager backend (stock latest, NetShift self-update) + +- **Two new component_action() cases** (`updater.sh` ~:1612): `sing_box: + check_update_stable) updates_check_sing_box_stable` (SYNC) and + `netshift:self_update) updates_self_update_netshift` (async via the existing + `component_action_async`). NO dispatcher (bin/netshift) change — both are + sub-cases of the already-routed `component_action`; `component_action_async`/ + `_status` are component-agnostic. NO ACL change. +- **pkg-manager abstraction re-implemented locally** (updater.sh does NOT source + install.sh): `updates_pkg_is_apk` (`command -v apk`), `updates_pkg_install_file` + (apk add --allow-untrusted / opkg install, `</dev/null` non-interactive), + `updates_pkg_is_installed` (apk/opkg list grep), `updates_pkg_candidate_version` + (FEED version). Candidate parse, busybox-safe, NO Oniguruma: opkg `list <pkg>` + → `"<name> - <ver>"`, `awk -F' - ' '{print $2}'`; apk `list <pkg>` → first + token `<name>-<ver>`, strip `"<pkg>-"` prefix via `${line#"$pkg"-}`. +- **Stock check `updates_check_sing_box_stable`**: mirrors the extended-check JSON + shape. Runs `opkg/apk update` best-effort first (`|| true`). status: candidate + empty → `success:false` (feed unreachable, return 1); sing-box absent + (`command -v`) → `not_installed`; else compare on LEADING semver `${v%%-*}` + (drops `-r1`/`-extended-…`) via `is_min_package_version` (sort -V) → + `latest`/`outdated`. NEVER exits. STABLE JSON: `{success,current_version, + latest_version,status:"latest"|"outdated"|"not_installed"}`. +- **NetShift self-update = Variant A** (targeted pkg upgrade, NOT install.sh). + `updates_self_update_netshift` (public wrapper) COPIES the + `updates_install_sing_box_extended` epilogue EXACTLY: reset UPDATES_HEAL_*, + `updates_ensure_connectivity "extended"` (GitHub dir) else restore+fail JSON, + run `_updates_self_update_netshift_core >"$out"`, capture rc+json, rm, ALWAYS + `updates_restore_after_swap`, re-emit, `return $rc`. Single cleanup path; no + trap. Core is NON-interactive, all `local`, NEVER `exit`: idempotent guard + (`${installed#v}` == `${latest#v}` → "Already up to date"); minimal + `/etc/config/netshift` tmpfs backup; download assets matching pkg-name prefixes + (`netshift`,`luci-app-netshift`, RU i18n ONLY if `updates_pkg_is_installed`) + filtered to `.ipk`/`.apk` by pkg-mgr via `grep -o 'https://[^"[:space:]]*\.ext'` + (mirrors install.sh:269-274, busybox-safe); install core→luci→ru; core-install + fail is fatal-to-the-op (success:false + restore config), luci/ru fail is + non-critical (warn+continue); defensive config restore if live file empty. +- **Self-replacement CONFIRMED safe**: the `netshift` pkg overwrites + `/usr/bin/netshift` (this very script). busybox ash reads the whole script into + memory before pkg_install; the async fork (`( trap '' HUP; "$0" component_action + netshift self_update >out; updates_write_finished_job_state ... )`) + the + finished-state write complete from memory. The self-update core has ZERO live + re-exec after install: NO `updates_restart_netshift`, NO `"$0"`, NO `exec`, NO + direct `/usr/bin/netshift` or `/etc/init.d/netshift` call. (Only path that runs + the init script is `updates_restore_after_swap`'s `/etc/init.d/netshift start`, + which fires ONLY if the heal tore the redirect down, AFTER install completes, + as a fresh subprocess that safely loads the on-disk binary.) UI (task-018) + reloads the page after success. Verified via `grep` of the core's line range. +- **New constants** (constants.sh, NO ports/marks): `NETSHIFT_RELEASE_API_URL` + (= install.sh REPO / get_system_info :3347 endpoint), + `UPDATES_NETSHIFT_DOWNLOAD_DIR=/tmp/netshift/selfupdate`, + `UPDATES_NETSHIFT_CONFIG_BACKUP=/tmp/netshift/config.bak`, + `UPDATES_NETSHIFT_PKG_CORE/LUCI/I18N_RU`. `get_system_info` UNCHANGED (UI gets + versions there; stock check is a separate action — no missing field). +- **Subshell-piped `while read url` loop can't set parent vars** (task-007 + variant): `_updates_self_update_download_assets` re-checks the dir + (`ls "$dir/netshift"*`) AFTER the loop to decide success, not a flag set inside. +- **Smoke tests `test_check_update_stable` (alias `stablecheck`, 4 cases) + + `test_self_update_netshift` (alias `selfupdate`, 13 assertions)**. Both source + the REAL updater.sh + helpers.sh, re-pin paths/constants, stub via markers. + `test_check_update_stable` KEY GOTCHA: `command -v sing-box` finds the real + `/usr/bin/sing-box`; to test `not_installed` I built an ISOLATED PATH dir of + symlinks to just the needed coreutils (NO /usr/bin in PATH) and linked the fake + sing-box in/out per scenario. `test_self_update_netshift` overrides + `updates_http_get_once` (GitHub JSON) + `updates_download_to_file` in the driver + + stubs opkg `install`/`list-installed` (logs installs) + fake `/etc/init.d/ + netshift` (absolute write+restore). Registered all 5 points (all)/case/usage/ + compose). Used task-009 `... || true` set -e guard. shellcheck -S error clean; + `smoke-tests all` = 101 passed / 0 failed (was 84 baseline; +17 new). diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index afff9f31..38403127 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -80,7 +80,7 @@ "call": "Available actions", "key": "Available actions", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:47" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:43" ] }, { @@ -149,6 +149,15 @@ "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:25" ] }, + { + "call": "Check update", + "key": "Check update", + "places": [ + "src/netshift/tabs/manager/cards.ts:143", + "src/netshift/tabs/manager/cards.ts:179", + "src/netshift/tabs/manager/cards.ts:225" + ] + }, { "call": "Checking, please wait", "key": "Checking, please wait", @@ -202,6 +211,13 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:417" ] }, + { + "call": "Component Manager", + "key": "Component Manager", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:83" + ] + }, { "call": "Config File Path", "key": "Config File Path", @@ -213,7 +229,7 @@ "call": "Configuration for NetShift service", "key": "Configuration for NetShift service", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:27" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:30" ] }, { @@ -248,7 +264,7 @@ "call": "Core switch failed", "key": "Core switch failed", "places": [ - "src/netshift/methods/shell/index.ts:157", + "src/netshift/methods/shell/index.ts:159", "src/netshift/methods/shell/pollSingBoxComponentAction.ts:65" ] }, @@ -270,7 +286,7 @@ "call": "Dashboard", "key": "Dashboard", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:80" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:98" ] }, { @@ -294,6 +310,13 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:272" ] }, + { + "call": "Dev", + "key": "Dev", + "places": [ + "src/netshift/tabs/manager/cards.ts:101" + ] + }, { "call": "DHCP has DNS server", "key": "DHCP has DNS server", @@ -305,14 +328,14 @@ "call": "Diagnostics", "key": "Diagnostics", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:65" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:68" ] }, { "call": "Disable autostart", "key": "Disable autostart", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:83" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:79" ] }, { @@ -474,7 +497,7 @@ "call": "Enable autostart", "key": "Enable autostart", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:93" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:89" ] }, { @@ -683,8 +706,13 @@ "src/netshift/tabs/diagnostic/initController.ts:267", "src/netshift/tabs/diagnostic/initController.ts:304", "src/netshift/tabs/diagnostic/initController.ts:308", - "src/netshift/tabs/diagnostic/initController.ts:347", - "src/netshift/tabs/diagnostic/initController.ts:351" + "src/netshift/tabs/manager/initController.ts:122", + "src/netshift/tabs/manager/initController.ts:132", + "src/netshift/tabs/manager/initController.ts:166", + "src/netshift/tabs/manager/initController.ts:197", + "src/netshift/tabs/manager/initController.ts:201", + "src/netshift/tabs/manager/initController.ts:234", + "src/netshift/tabs/manager/initController.ts:238" ] }, { @@ -708,7 +736,7 @@ "call": "Get global check", "key": "Get global check", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:102" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:98" ] }, { @@ -747,17 +775,19 @@ ] }, { - "call": "Install extended", - "key": "Install extended", + "call": "Install %s", + "key": "Install %s", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129" + "src/netshift/tabs/manager/cards.ts:135", + "src/netshift/tabs/manager/cards.ts:172", + "src/netshift/tabs/manager/cards.ts:218" ] }, { - "call": "Install stable", - "key": "Install stable", + "call": "Installed version is newer than release", + "key": "Installed version is newer than release", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129" + "src/netshift/tabs/manager/initController.ts:96" ] }, { @@ -1173,9 +1203,24 @@ "call": "Latest", "key": "Latest", "places": [ + "src/netshift/tabs/manager/cards.ts:90", "src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:48" ] }, + { + "call": "Latest version is installed", + "key": "Latest version is installed", + "places": [ + "src/netshift/tabs/manager/initController.ts:103" + ] + }, + { + "call": "Latest version is unknown", + "key": "Latest version is unknown", + "places": [ + "src/netshift/tabs/manager/initController.ts:155" + ] + }, { "call": "List Update Frequency", "key": "List Update Frequency", @@ -1257,7 +1302,14 @@ "call": "NetShift Settings", "key": "NetShift Settings", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:26" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:29" + ] + }, + { + "call": "NetShift updated, version:", + "key": "NetShift updated, version:", + "places": [ + "src/netshift/tabs/manager/initController.ts:226" ] }, { @@ -1288,6 +1340,16 @@ "src/netshift/tabs/diagnostic/partials/renderCheckSection.ts:189" ] }, + { + "call": "Not installed", + "key": "Not installed", + "places": [ + "src/netshift/tabs/manager/cards.ts:98", + "src/netshift/tabs/manager/cards.ts:196", + "src/netshift/tabs/manager/cards.ts:241", + "src/netshift/tabs/manager/initController.ts:100" + ] + }, { "call": "Not responding", "key": "Not responding", @@ -1301,11 +1363,11 @@ "call": "Not running", "key": "Not running", "places": [ - "src/netshift/tabs/diagnostic/diagnostic.store.ts:59", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:67", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:75", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:83", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:91" + "src/netshift/tabs/diagnostic/diagnostic.store.ts:56", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:64", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:72", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:80", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:88" ] }, { @@ -1347,6 +1409,7 @@ "call": "Outdated", "key": "Outdated", "places": [ + "src/netshift/tabs/manager/cards.ts:94", "src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:38" ] }, @@ -1389,11 +1452,11 @@ "call": "Pending", "key": "Pending", "places": [ - "src/netshift/tabs/diagnostic/diagnostic.store.ts:107", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:115", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:123", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:131", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:139" + "src/netshift/tabs/diagnostic/diagnostic.store.ts:104", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:112", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:120", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:128", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:136" ] }, { @@ -1449,7 +1512,7 @@ "call": "Restart NetShift", "key": "Restart NetShift", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:53" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:49" ] }, { @@ -1554,7 +1617,7 @@ "call": "Sections", "key": "Sections", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:36" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:39" ] }, { @@ -1684,6 +1747,13 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165" ] }, + { + "call": "Self-update failed", + "key": "Self-update failed", + "places": [ + "src/netshift/methods/shell/index.ts:230" + ] + }, { "call": "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct.", "key": "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct.", @@ -1702,7 +1772,7 @@ "call": "Settings", "key": "Settings", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:49" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:52" ] }, { @@ -1710,7 +1780,7 @@ "key": "Show sing-box config", "places": [ "src/netshift/tabs/diagnostic/initController.ts:292", - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:120" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:116" ] }, { @@ -1731,7 +1801,7 @@ "call": "Sing-box core changed, version:", "key": "Sing-box core changed, version:", "places": [ - "src/netshift/tabs/diagnostic/initController.ts:342" + "src/netshift/tabs/manager/initController.ts:190" ] }, { @@ -1816,14 +1886,14 @@ "call": "Start NetShift", "key": "Start NetShift", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:73" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:69" ] }, { "call": "Stop NetShift", "key": "Stop NetShift", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:63" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:59" ] }, { @@ -1854,11 +1924,25 @@ "src/helpers/copyToClipboard.ts:10" ] }, + { + "call": "Switch to extended", + "key": "Switch to extended", + "places": [ + "src/netshift/tabs/manager/cards.ts:233" + ] + }, + { + "call": "Switch to stable", + "key": "Switch to stable", + "places": [ + "src/netshift/tabs/manager/cards.ts:188" + ] + }, { "call": "Switching sing-box core, this may take a few minutes…", "key": "Switching sing-box core, this may take a few minutes…", "places": [ - "src/netshift/tabs/diagnostic/initController.ts:331" + "src/netshift/tabs/manager/initController.ts:178" ] }, { @@ -1999,6 +2083,14 @@ "src/netshift/tabs/diagnostic/initController.ts:42", "src/netshift/tabs/diagnostic/initController.ts:43", "src/netshift/tabs/diagnostic/initController.ts:44", + "src/netshift/tabs/manager/cards.ts:113", + "src/netshift/tabs/manager/initController.ts:37", + "src/netshift/tabs/manager/initController.ts:38", + "src/netshift/tabs/manager/initController.ts:39", + "src/netshift/tabs/manager/initController.ts:40", + "src/netshift/tabs/manager/initController.ts:41", + "src/netshift/tabs/manager/initController.ts:42", + "src/netshift/tabs/manager/initController.ts:154", "src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7" ] }, @@ -2009,6 +2101,35 @@ "src/netshift/api.ts:40" ] }, + { + "call": "Update", + "key": "Update", + "places": [ + "src/netshift/tabs/manager/cards.ts:172", + "src/netshift/tabs/manager/cards.ts:218" + ] + }, + { + "call": "Update is available", + "key": "Update is available", + "places": [ + "src/netshift/tabs/manager/initController.ts:92" + ] + }, + { + "call": "Update NetShift", + "key": "Update NetShift", + "places": [ + "src/netshift/tabs/manager/cards.ts:136" + ] + }, + { + "call": "Updating NetShift, this may take a few minutes; the page will reload…", + "key": "Updating NetShift, this may take a few minutes; the page will reload…", + "places": [ + "src/netshift/tabs/manager/initController.ts:217" + ] + }, { "call": "Uplink", "key": "Uplink", @@ -2155,12 +2276,19 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:655" ] }, + { + "call": "Version", + "key": "Version", + "places": [ + "src/netshift/tabs/manager/initController.ts:315" + ] + }, { "call": "View logs", "key": "View logs", "places": [ "src/netshift/tabs/diagnostic/initController.ts:258", - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:111" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:107" ] }, { diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index 5e6e1464..f4032464 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 15:38+0300\n" -"PO-Revision-Date: 2026-06-06 15:38+0300\n" +"POT-Creation-Date: 2026-06-06 19:26+0300\n" +"PO-Revision-Date: 2026-06-06 19:26+0300\n" "Last-Translator: spgsroot, yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -60,7 +60,7 @@ msgstr "" msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:47 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:43 msgid "Available actions" msgstr "" @@ -103,6 +103,12 @@ msgstr "" msgid "Cannot receive checks result" msgstr "" +#: src/netshift/tabs/manager/cards.ts:143 +#: src/netshift/tabs/manager/cards.ts:179 +#: src/netshift/tabs/manager/cards.ts:225 +msgid "Check update" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:15 #: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:15 #: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:13 @@ -135,11 +141,15 @@ msgstr "" msgid "Community Lists" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:83 +msgid "Component Manager" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:386 msgid "Config File Path" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:27 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:30 msgid "Configuration for NetShift service" msgstr "" @@ -159,7 +169,7 @@ msgstr "" msgid "Copy" msgstr "" -#: src/netshift/methods/shell/index.ts:157 +#: src/netshift/methods/shell/index.ts:159 #: src/netshift/methods/shell/pollSingBoxComponentAction.ts:65 msgid "Core switch failed" msgstr "" @@ -172,7 +182,7 @@ msgstr "" msgid "Currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:80 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:98 msgid "Dashboard" msgstr "" @@ -188,15 +198,19 @@ msgstr "" msgid "Delay value cannot be empty" msgstr "" +#: src/netshift/tabs/manager/cards.ts:101 +msgid "Dev" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:93 msgid "DHCP has DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:65 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:68 msgid "Diagnostics" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:83 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:79 msgid "Disable autostart" msgstr "" @@ -292,7 +306,7 @@ msgstr "" msgid "Dynamic List" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:93 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:89 msgid "Enable autostart" msgstr "" @@ -414,8 +428,13 @@ msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:267 #: src/netshift/tabs/diagnostic/initController.ts:304 #: src/netshift/tabs/diagnostic/initController.ts:308 -#: src/netshift/tabs/diagnostic/initController.ts:347 -#: src/netshift/tabs/diagnostic/initController.ts:351 +#: src/netshift/tabs/manager/initController.ts:122 +#: src/netshift/tabs/manager/initController.ts:132 +#: src/netshift/tabs/manager/initController.ts:166 +#: src/netshift/tabs/manager/initController.ts:197 +#: src/netshift/tabs/manager/initController.ts:201 +#: src/netshift/tabs/manager/initController.ts:234 +#: src/netshift/tabs/manager/initController.ts:238 msgid "Failed to execute!" msgstr "" @@ -430,7 +449,7 @@ msgstr "" msgid "Fully Routed IPs" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:102 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:98 msgid "Get global check" msgstr "" @@ -454,12 +473,14 @@ msgstr "" msgid "Include servers by keyword" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129 -msgid "Install extended" +#: src/netshift/tabs/manager/cards.ts:135 +#: src/netshift/tabs/manager/cards.ts:172 +#: src/netshift/tabs/manager/cards.ts:218 +msgid "Install %s" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129 -msgid "Install stable" +#: src/netshift/tabs/manager/initController.ts:96 +msgid "Installed version is newer than release" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:232 @@ -697,10 +718,19 @@ msgstr "" msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" +#: src/netshift/tabs/manager/cards.ts:90 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:48 msgid "Latest" msgstr "" +#: src/netshift/tabs/manager/initController.ts:103 +msgid "Latest version is installed" +msgstr "" + +#: src/netshift/tabs/manager/initController.ts:155 +msgid "Latest version is unknown" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:323 msgid "List Update Frequency" msgstr "" @@ -745,10 +775,14 @@ msgstr "" msgid "NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:26 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:29 msgid "NetShift Settings" msgstr "" +#: src/netshift/tabs/manager/initController.ts:226 +msgid "NetShift updated, version:" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:378 msgid "NetShift will not modify your DHCP configuration" msgstr "" @@ -765,17 +799,24 @@ msgstr "" msgid "Not implement yet" msgstr "" +#: src/netshift/tabs/manager/cards.ts:98 +#: src/netshift/tabs/manager/cards.ts:196 +#: src/netshift/tabs/manager/cards.ts:241 +#: src/netshift/tabs/manager/initController.ts:100 +msgid "Not installed" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:74 #: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:80 #: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:99 msgid "Not responding" msgstr "" -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:59 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:67 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:75 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:83 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:91 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:56 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:64 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:72 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:80 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:88 msgid "Not running" msgstr "" @@ -799,6 +840,7 @@ msgstr "" msgid "Outbound Configuration" msgstr "" +#: src/netshift/tabs/manager/cards.ts:94 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:38 msgid "Outdated" msgstr "" @@ -823,11 +865,11 @@ msgstr "" msgid "Path must end with cache.db" msgstr "" -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:107 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:115 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:123 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:131 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:139 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:104 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:112 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:120 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:128 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:136 msgid "Pending" msgstr "" @@ -859,7 +901,7 @@ msgstr "" msgid "Resolve real IP for routing" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:53 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:49 msgid "Restart NetShift" msgstr "" @@ -919,7 +961,7 @@ msgstr "" msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:36 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:39 msgid "Sections" msgstr "" @@ -996,6 +1038,10 @@ msgstr "" msgid "Selector Proxy Links" msgstr "" +#: src/netshift/methods/shell/index.ts:230 +msgid "Self-update failed" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:69 msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." msgstr "" @@ -1004,12 +1050,12 @@ msgstr "" msgid "Services info" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:49 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:52 msgid "Settings" msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:292 -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:120 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:116 msgid "Show sing-box config" msgstr "" @@ -1021,7 +1067,7 @@ msgstr "" msgid "Sing-box autostart disabled" msgstr "" -#: src/netshift/tabs/diagnostic/initController.ts:342 +#: src/netshift/tabs/manager/initController.ts:190 msgid "Sing-box core changed, version:" msgstr "" @@ -1070,11 +1116,11 @@ msgstr "" msgid "Specify the path to the list file located on the router filesystem" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:73 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:69 msgid "Start NetShift" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:63 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:59 msgid "Stop NetShift" msgstr "" @@ -1094,7 +1140,15 @@ msgstr "" msgid "Successfully copied!" msgstr "" -#: src/netshift/tabs/diagnostic/initController.ts:331 +#: src/netshift/tabs/manager/cards.ts:233 +msgid "Switch to extended" +msgstr "" + +#: src/netshift/tabs/manager/cards.ts:188 +msgid "Switch to stable" +msgstr "" + +#: src/netshift/tabs/manager/initController.ts:178 msgid "Switching sing-box core, this may take a few minutes…" msgstr "" @@ -1178,6 +1232,14 @@ msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:42 #: src/netshift/tabs/diagnostic/initController.ts:43 #: src/netshift/tabs/diagnostic/initController.ts:44 +#: src/netshift/tabs/manager/cards.ts:113 +#: src/netshift/tabs/manager/initController.ts:37 +#: src/netshift/tabs/manager/initController.ts:38 +#: src/netshift/tabs/manager/initController.ts:39 +#: src/netshift/tabs/manager/initController.ts:40 +#: src/netshift/tabs/manager/initController.ts:41 +#: src/netshift/tabs/manager/initController.ts:42 +#: src/netshift/tabs/manager/initController.ts:154 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7 msgid "unknown" msgstr "" @@ -1186,6 +1248,23 @@ msgstr "" msgid "Unknown error" msgstr "" +#: src/netshift/tabs/manager/cards.ts:172 +#: src/netshift/tabs/manager/cards.ts:218 +msgid "Update" +msgstr "" + +#: src/netshift/tabs/manager/initController.ts:92 +msgid "Update is available" +msgstr "" + +#: src/netshift/tabs/manager/cards.ts:136 +msgid "Update NetShift" +msgstr "" + +#: src/netshift/tabs/manager/initController.ts:217 +msgid "Updating NetShift, this may take a few minutes; the page will reload…" +msgstr "" + #: src/netshift/tabs/dashboard/initController.ts:240 #: src/netshift/tabs/dashboard/initController.ts:271 msgid "Uplink" @@ -1278,8 +1357,12 @@ msgstr "" msgid "Validation errors:" msgstr "" +#: src/netshift/tabs/manager/initController.ts:315 +msgid "Version" +msgstr "" + #: src/netshift/tabs/diagnostic/initController.ts:258 -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:111 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:107 msgid "View logs" msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 6d00cfb3..4ded1a6e 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 18:38+0300\n" -"PO-Revision-Date: 2026-06-06 18:38+0300\n" +"POT-Creation-Date: 2026-06-06 22:26+0300\n" +"PO-Revision-Date: 2026-06-06 22:26+0300\n" "Last-Translator: spgsroot, yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -80,6 +80,9 @@ msgstr "Путь к файлу кэша не может быть пустым" msgid "Cannot receive checks result" msgstr "Не удалось получить результаты проверки" +msgid "Check update" +msgstr "Проверить обновление" + msgid "Checking, please wait" msgstr "Проверяем, пожалуйста подождите" @@ -101,6 +104,9 @@ msgstr "Закрыть" msgid "Community Lists" msgstr "Списки сообщества" +msgid "Component Manager" +msgstr "Менеджер компонентов" + msgid "Config File Path" msgstr "Путь к файлу конфигурации" @@ -140,6 +146,9 @@ msgstr "Задержка в миллисекундах перед перезаг msgid "Delay value cannot be empty" msgstr "Значение задержки не может быть пустым" +msgid "Dev" +msgstr "Dev" + msgid "DHCP has DNS server" msgstr "DHCP содержит DNS сервер" @@ -326,11 +335,11 @@ msgstr "Ошибка HTTP" msgid "Include servers by keyword" msgstr "Включать серверы по ключевому слову" -msgid "Install extended" -msgstr "Установить extended" +msgid "Install %s" +msgstr "Установить %s" -msgid "Install stable" -msgstr "Установить stable" +msgid "Installed version is newer than release" +msgstr "Установленная версия новее релиза" msgid "Interface Monitoring" msgstr "Мониторинг интерфейса" @@ -509,6 +518,12 @@ msgstr "Оставлять только серверы подписки, имя msgid "Latest" msgstr "Последняя" +msgid "Latest version is installed" +msgstr "Установлена последняя версия" + +msgid "Latest version is unknown" +msgstr "Последняя версия неизвестна" + msgid "List Update Frequency" msgstr "Частота обновления списков" @@ -545,6 +560,9 @@ msgstr "NetShift" msgid "NetShift Settings" msgstr "Настройки NetShift" +msgid "NetShift updated, version:" +msgstr "NetShift обновлён, версия:" + msgid "NetShift will not modify your DHCP configuration" msgstr "NetShift не будет изменять вашу конфигурацию DHCP" @@ -557,6 +575,9 @@ msgstr "Другие правила маркировки не найдены" msgid "Not implement yet" msgstr "Ещё не реализовано" +msgid "Not installed" +msgstr "Не установлено" + msgid "Not responding" msgstr "Не отвечает" @@ -722,6 +743,9 @@ msgstr "Selector" msgid "Selector Proxy Links" msgstr "Ссылки прокси для Selector" +msgid "Self-update failed" +msgstr "Не удалось обновить" + msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." msgstr "Отправлять запросы к основному DNS через outbound прокси/VPN вместо прямого подключения. Bootstrap DNS всегда остаётся прямым." @@ -794,6 +818,12 @@ msgstr "URL подписки" msgid "Successfully copied!" msgstr "Успешно скопировано!" +msgid "Switch to extended" +msgstr "Переключить на extended" + +msgid "Switch to stable" +msgstr "Переключить на stable" + msgid "Switching sing-box core, this may take a few minutes…" msgstr "Переключение ядра sing-box, это может занять несколько минут…" @@ -857,6 +887,18 @@ msgstr "неизвестно" msgid "Unknown error" msgstr "Неизвестная ошибка" +msgid "Update" +msgstr "Обновить" + +msgid "Update is available" +msgstr "Доступно обновление" + +msgid "Update NetShift" +msgstr "Обновить NetShift" + +msgid "Updating NetShift, this may take a few minutes; the page will reload…" +msgstr "Обновление NetShift, это может занять несколько минут; страница перезагрузится…" + msgid "Uplink" msgstr "Исходящий" @@ -911,6 +953,9 @@ msgstr "Валидно" msgid "Validation errors:" msgstr "Ошибки валидации:" +msgid "Version" +msgstr "Версия" + msgid "View logs" msgstr "Посмотреть логи" diff --git a/fe-app-netshift/src/netshift/methods/shell/index.ts b/fe-app-netshift/src/netshift/methods/shell/index.ts index 63dc14da..88a8242f 100644 --- a/fe-app-netshift/src/netshift/methods/shell/index.ts +++ b/fe-app-netshift/src/netshift/methods/shell/index.ts @@ -3,10 +3,12 @@ import { ClashAPI, NetShift } from '../../types'; import { executeShellCommand } from '../../../helpers'; import { ComponentActionStartResponse, + ComponentActionStatus, SingBoxComponentActionResult, parseComponentActionStatus, pollSingBoxComponentAction, } from './pollSingBoxComponentAction'; +import { parseComponentCheckUpdate } from './parseComponentCheckUpdate'; export const NetShiftShellMethods = { checkDNSAvailable: async () => @@ -173,4 +175,85 @@ export const NetShiftShellMethods = { return parseComponentActionStatus(statusResponse.stdout); }); }, + // Sing-box update checks (sync) — STABLE task-017 contract: + // component_action sing_box check_update (extended) + // component_action sing_box check_update_stable (stock) + // → {success, current_version, latest_version, status}. + singBoxCheckUpdate: async ( + action: 'check_update' | 'check_update_stable', + ): Promise<NetShift.ComponentCheckUpdateResult> => { + const response = await executeShellCommand({ + command: '/usr/bin/netshift', + args: ['component_action', 'sing_box', action], + timeout: 600000, + }); + + if (response.stdout) { + return parseComponentCheckUpdate(response.stdout); + } + + return { + success: false, + message: response.stderr || '', + }; + }, + // NetShift self-update (async) — STABLE task-017 contract: + // component_action_async netshift self_update + component_action_status <job>. + // Reuses the component-agnostic poll. Because the package install swaps + // /usr/bin/netshift mid-job, status polls can transiently fail (rpcd / binary + // swap); once the job has STARTED we treat such failures leniently — keep + // polling (return a synthetic running status) instead of aborting hard, so a + // successful self-update is not misreported as a failure. The UI reloads the + // page on success. + netshiftSelfUpdate: async (): Promise<SingBoxComponentActionResult> => { + const startResponse = await executeShellCommand({ + command: '/usr/bin/netshift', + args: ['component_action_async', 'netshift', 'self_update'], + }); + + let start: ComponentActionStartResponse | null = null; + + if (startResponse.stdout) { + try { + start = JSON.parse( + startResponse.stdout, + ) as ComponentActionStartResponse; + } catch (_e) { + start = null; + } + } + + if (!start || start.success !== true || !start.job_id) { + return { + success: false, + message: + start?.message || startResponse.stderr || _('Self-update failed'), + }; + } + + const jobId = start.job_id; + + return pollSingBoxComponentAction(async () => { + // Lenient mid-job polling: any exec/parse error AFTER a successful start + // is reported as "still running" so the binary swap doesn't end the loop + // prematurely. The MAX_POLLS backstop still bounds the loop. + try { + const statusResponse = await executeShellCommand({ + command: '/usr/bin/netshift', + args: ['component_action_status', jobId], + }); + + if (!statusResponse.stdout) { + return { running: true } as ComponentActionStatus; + } + + return ( + parseComponentActionStatus(statusResponse.stdout) ?? + ({ running: true } as ComponentActionStatus) + ); + } catch (_e) { + return { running: true } as ComponentActionStatus; + } + }); + }, }; diff --git a/fe-app-netshift/src/netshift/methods/shell/parseComponentCheckUpdate.ts b/fe-app-netshift/src/netshift/methods/shell/parseComponentCheckUpdate.ts new file mode 100644 index 00000000..b7489019 --- /dev/null +++ b/fe-app-netshift/src/netshift/methods/shell/parseComponentCheckUpdate.ts @@ -0,0 +1,58 @@ +import { NetShift } from '../../types'; + +const VALID_STATUSES: NetShift.ComponentUpdateStatus[] = [ + 'latest', + 'outdated', + 'dev', + 'not_installed', +]; + +function normalizeStatus( + status: unknown, +): NetShift.ComponentUpdateStatus | undefined { + if ( + typeof status === 'string' && + (VALID_STATUSES as string[]).includes(status) + ) { + return status as NetShift.ComponentUpdateStatus; + } + + return undefined; +} + +/** + * Parse the STABLE JSON echoed by the sync update-check actions + * (`component_action sing_box check_update` / + * `component_action sing_box check_update_stable`): + * `{success, current_version, latest_version, status}`. + * + * Pure (types-only import) so it is unit-testable without dragging in the + * helpers barrel (which pulls TabService → MutationObserver and crashes the + * node test env at collect time). + */ +export function parseComponentCheckUpdate( + stdout: string, +): NetShift.ComponentCheckUpdateResult { + try { + const parsed = JSON.parse(stdout) as Record<string, unknown>; + + return { + success: Boolean(parsed.success), + current_version: + typeof parsed.current_version === 'string' + ? parsed.current_version + : undefined, + latest_version: + typeof parsed.latest_version === 'string' + ? parsed.latest_version + : undefined, + status: normalizeStatus(parsed.status), + message: typeof parsed.message === 'string' ? parsed.message : undefined, + }; + } catch (_e) { + return { + success: false, + message: stdout, + }; + } +} diff --git a/fe-app-netshift/src/netshift/methods/shell/tests/parseComponentCheckUpdate.test.js b/fe-app-netshift/src/netshift/methods/shell/tests/parseComponentCheckUpdate.test.js new file mode 100644 index 00000000..b82f904c --- /dev/null +++ b/fe-app-netshift/src/netshift/methods/shell/tests/parseComponentCheckUpdate.test.js @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest'; +import { parseComponentCheckUpdate } from '../parseComponentCheckUpdate'; + +describe('parseComponentCheckUpdate', () => { + it('parses the STABLE check-update JSON', () => { + const result = parseComponentCheckUpdate( + JSON.stringify({ + success: true, + current_version: '1.12.0', + latest_version: '1.12.9', + status: 'outdated', + }), + ); + + expect(result).toEqual({ + success: true, + current_version: '1.12.0', + latest_version: '1.12.9', + status: 'outdated', + message: undefined, + }); + }); + + it.each(['latest', 'outdated', 'dev', 'not_installed'])( + 'accepts the valid status %s', + (status) => { + const result = parseComponentCheckUpdate( + JSON.stringify({ success: true, status }), + ); + + expect(result.status).toBe(status); + }, + ); + + it('drops an unknown status to undefined', () => { + const result = parseComponentCheckUpdate( + JSON.stringify({ success: true, status: 'weird' }), + ); + + expect(result.status).toBeUndefined(); + }); + + it('returns success:false with the raw stdout on invalid JSON', () => { + const result = parseComponentCheckUpdate('not json'); + + expect(result).toEqual({ success: false, message: 'not json' }); + }); + + it('coerces a missing success to false', () => { + const result = parseComponentCheckUpdate(JSON.stringify({})); + + expect(result.success).toBe(false); + }); +}); diff --git a/fe-app-netshift/src/netshift/services/store.service.ts b/fe-app-netshift/src/netshift/services/store.service.ts index 255d61e3..d0476554 100644 --- a/fe-app-netshift/src/netshift/services/store.service.ts +++ b/fe-app-netshift/src/netshift/services/store.service.ts @@ -1,5 +1,11 @@ import { NetShift } from '../types'; import { initialDiagnosticStore } from '../tabs/diagnostic/diagnostic.store'; +import { initialManagerStore } from '../tabs/manager/manager.store'; +import type { ManagerComponentKey } from '../tabs/manager/cards'; + +// Single source of truth for the component key union lives in `tabs/manager/cards`; +// re-export it here so store consumers keep their existing import path. +export type { ManagerComponentKey }; function jsonStableStringify<T, V>(obj: T): string { return JSON.stringify(obj, (_, value) => { @@ -180,7 +186,6 @@ export interface StoreType { globalCheck: { loading: boolean }; viewLogs: { loading: boolean }; showSingBoxConfig: { loading: boolean }; - singBoxInstall: { loading: boolean }; }; diagnosticsSystemInfo: { loading: boolean; @@ -192,6 +197,21 @@ export interface StoreType { device_model: string; sing_box_extended: 0 | 1; }; + managerActions: { + netshiftCheck: { loading: boolean }; + netshiftUpdate: { loading: boolean }; + singBoxStockCheck: { loading: boolean }; + singBoxStockAction: { loading: boolean }; + singBoxExtendedCheck: { loading: boolean }; + singBoxExtendedAction: { loading: boolean }; + }; + managerChecks: Record< + ManagerComponentKey, + { + status: NetShift.ComponentUpdateStatus | null; + latest_version: string; + } + >; } const initialStore: StoreType = { @@ -226,6 +246,7 @@ const initialStore: StoreType = { data: [], }, ...initialDiagnosticStore, + ...initialManagerStore, }; export const store = new StoreService<StoreType>(initialStore); diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts index 9000f4da..6cede441 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts @@ -46,9 +46,6 @@ export const initialDiagnosticStore: Pick< showSingBoxConfig: { loading: false, }, - singBoxInstall: { - loading: false, - }, }, diagnosticsRunAction: { loading: false }, diagnosticsChecks: [ diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts index 68d74970..01b994ca 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts @@ -316,50 +316,6 @@ async function handleShowSingBoxConfig() { } } -async function handleInstallSingBox() { - const diagnosticsActions = store.get().diagnosticsActions; - store.set({ - diagnosticsActions: { - ...diagnosticsActions, - singBoxInstall: { loading: true }, - }, - }); - - const isExtended = store.get().diagnosticsSystemInfo.sing_box_extended === 1; - - showToast( - _('Switching sing-box core, this may take a few minutes…'), - 'success', - ); - - try { - const result = await NetShiftShellMethods.singBoxComponentAction( - isExtended ? 'install_stable' : 'install_extended', - ); - - if (result.success) { - showToast( - _('Sing-box core changed, version: ') + (result.version || ''), - 'success', - ); - } else { - logger.error('[DIAGNOSTIC]', 'handleInstallSingBox - e', result); - showToast(result.message || _('Failed to execute!'), 'error'); - } - } catch (e) { - logger.error('[DIAGNOSTIC]', 'handleInstallSingBox - e', e); - showToast(_('Failed to execute!'), 'error'); - } finally { - store.set({ - diagnosticsActions: { - ...diagnosticsActions, - singBoxInstall: { loading: false }, - }, - }); - await fetchSystemInfo(); - } -} - function renderWikiDisclaimerWidget() { const diagnosticsChecks = store.get().diagnosticsChecks; @@ -448,15 +404,6 @@ function renderDiagnosticAvailableActionsWidget() { onClick: handleShowSingBoxConfig, disabled: atLeastOneServiceCommandLoading, }, - singBoxInstall: { - loading: diagnosticsActions.singBoxInstall.loading, - visible: true, - onClick: handleInstallSingBox, - disabled: - atLeastOneServiceCommandLoading || - diagnosticsActions.singBoxInstall.loading, - }, - singBoxExtended: store.get().diagnosticsSystemInfo.sing_box_extended, }); return preserveScrollForPage(() => { diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts index 4de4d208..8abbadce 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts @@ -27,8 +27,6 @@ interface IRenderAvailableActionsProps { globalCheck: ActionProps; viewLogs: ActionProps; showSingBoxConfig: ActionProps; - singBoxInstall: ActionProps; - singBoxExtended: 0 | 1; } export function renderAvailableActions({ @@ -40,8 +38,6 @@ export function renderAvailableActions({ globalCheck, viewLogs, showSingBoxConfig, - singBoxInstall, - singBoxExtended, }: IRenderAvailableActionsProps) { return E('div', { class: 'pdk_diagnostic-page__right-bar__actions' }, [ E('b', {}, _('Available actions')), @@ -122,14 +118,5 @@ export function renderAvailableActions({ disabled: showSingBoxConfig.disabled, }), ]), - ...insertIf(singBoxInstall.visible, [ - renderButton({ - onClick: singBoxInstall.onClick, - icon: renderRotateCcwIcon24, - text: singBoxExtended ? _('Install stable') : _('Install extended'), - loading: singBoxInstall.loading, - disabled: singBoxInstall.disabled, - }), - ]), ]); } diff --git a/fe-app-netshift/src/netshift/tabs/index.ts b/fe-app-netshift/src/netshift/tabs/index.ts index b49ac00d..1a4fdc3b 100644 --- a/fe-app-netshift/src/netshift/tabs/index.ts +++ b/fe-app-netshift/src/netshift/tabs/index.ts @@ -1,2 +1,3 @@ export * from './dashboard'; export * from './diagnostic'; +export * from './manager'; diff --git a/fe-app-netshift/src/netshift/tabs/manager/cards.ts b/fe-app-netshift/src/netshift/tabs/manager/cards.ts new file mode 100644 index 00000000..349a0a15 --- /dev/null +++ b/fe-app-netshift/src/netshift/tabs/manager/cards.ts @@ -0,0 +1,262 @@ +import { NetShift } from '../../types'; +import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVersion'; + +export type ManagerComponentKey = + | 'netshift' + | 'sing_box_stock' + | 'sing_box_extended'; + +// `check` = a sing-box update check (routed to the sing-box check method); +// `check_netshift` = the NetShift card's on-demand check, which is a +// systemInfo REFRESH (the backend has NO netshift:check_update action — the +// NetShift latest version comes only from get_system_info.netshift_latest_version). +// Keeping it a DISTINCT kind guarantees a NetShift check can never be routed to +// the sing-box check method. +export type ManagerActionKind = + | 'check' + | 'check_netshift' + | 'update' + | 'switch' + | 'self_update'; + +export interface ManagerActionDescriptor { + // The store-slice key driving this button's loading flag. + loadingKey: + | 'netshiftCheck' + | 'netshiftUpdate' + | 'singBoxStockCheck' + | 'singBoxStockAction' + | 'singBoxExtendedCheck' + | 'singBoxExtendedAction'; + kind: ManagerActionKind; + text: string; + // For `update`/`switch`: the backend install action; for `self_update`: + // 'self_update'; for `check`: the sing-box check action. The NetShift + // `check_netshift` kind has NO backend action (it just refreshes systemInfo). + backendAction?: + | 'check_update' + | 'check_update_stable' + | 'install_stable' + | 'install_extended' + | 'self_update'; +} + +export interface ManagerCardTag { + label: string; + kind: 'neutral' | 'success' | 'warning'; +} + +export interface ManagerCardDescriptor { + key: ManagerComponentKey; + title: string; + version: string; + installed: boolean; + tag?: ManagerCardTag; + actions: ManagerActionDescriptor[]; +} + +export type ManagerSystemInfo = { + netshift_version: string; + netshift_latest_version: string; + sing_box_version: string; + sing_box_extended: 0 | 1; +}; + +export type ManagerCheckState = { + status: NetShift.ComponentUpdateStatus | null; + latest_version: string; +}; + +const NOT_INSTALLED = 'not installed'; + +export function isSingBoxInstalled(systemInfo: ManagerSystemInfo): boolean { + const version = systemInfo.sing_box_version; + + return Boolean(version) && version !== NOT_INSTALLED; +} + +/** + * Map a check status to a status badge. Pure — same logic as podkop-plus + * `getCheckTag`, extended with `not_installed`. + */ +export function getCheckTag( + status: NetShift.ComponentUpdateStatus | null, +): ManagerCardTag | undefined { + if (!status) { + return undefined; + } + + if (status === 'latest') { + return { label: _('Latest'), kind: 'success' }; + } + + if (status === 'outdated') { + return { label: _('Outdated'), kind: 'warning' }; + } + + if (status === 'not_installed') { + return { label: _('Not installed'), kind: 'neutral' }; + } + + return { label: _('Dev'), kind: 'neutral' }; +} + +// NetShift status is derived PURELY from systemInfo (installed vs latest). +// There is no NetShift check write into managerChecks — the on-demand check is +// a systemInfo refresh, after which this re-derives. +function netshiftStatus( + systemInfo: ManagerSystemInfo, +): NetShift.ComponentUpdateStatus | null { + const installed = normalizeCompiledVersion(systemInfo.netshift_version); + const latest = systemInfo.netshift_latest_version; + + if (!latest || latest === 'loading' || latest === _('unknown')) { + return null; + } + + if (installed === 'dev') { + return null; + } + + return installed === latest ? 'latest' : 'outdated'; +} + +function netshiftCard(systemInfo: ManagerSystemInfo): ManagerCardDescriptor { + const status = netshiftStatus(systemInfo); + const latest = systemInfo.netshift_latest_version; + const actions: ManagerActionDescriptor[] = []; + + if (status === 'outdated') { + actions.push({ + loadingKey: 'netshiftUpdate', + kind: 'self_update', + text: + latest && latest !== 'loading' + ? _('Install %s').replace('%s', latest) + : _('Update NetShift'), + backendAction: 'self_update', + }); + } else { + actions.push({ + loadingKey: 'netshiftCheck', + kind: 'check_netshift', + text: _('Check update'), + }); + } + + return { + key: 'netshift', + title: 'NetShift', + version: normalizeCompiledVersion(systemInfo.netshift_version), + installed: true, + tag: getCheckTag(status), + actions, + }; +} + +function singBoxStockCard( + systemInfo: ManagerSystemInfo, + check: ManagerCheckState, +): ManagerCardDescriptor { + const installed = isSingBoxInstalled(systemInfo); + const isActive = installed && systemInfo.sing_box_extended === 0; + const actions: ManagerActionDescriptor[] = []; + + if (isActive) { + if (check.status === 'outdated') { + const latest = check.latest_version; + + actions.push({ + loadingKey: 'singBoxStockAction', + kind: 'update', + text: latest ? _('Install %s').replace('%s', latest) : _('Update'), + backendAction: 'install_stable', + }); + } else { + actions.push({ + loadingKey: 'singBoxStockCheck', + kind: 'check', + text: _('Check update'), + backendAction: 'check_update_stable', + }); + } + } else { + // Either extended is active, or no sing-box at all → offer switch-to-stable. + actions.push({ + loadingKey: 'singBoxStockAction', + kind: 'switch', + text: _('Switch to stable'), + backendAction: 'install_stable', + }); + } + + return { + key: 'sing_box_stock', + title: 'sing-box (stock)', + version: isActive ? systemInfo.sing_box_version : _('Not installed'), + installed: isActive, + tag: isActive ? getCheckTag(check.status) : getCheckTag('not_installed'), + actions, + }; +} + +function singBoxExtendedCard( + systemInfo: ManagerSystemInfo, + check: ManagerCheckState, +): ManagerCardDescriptor { + const installed = isSingBoxInstalled(systemInfo); + const isActive = installed && systemInfo.sing_box_extended === 1; + const actions: ManagerActionDescriptor[] = []; + + if (isActive) { + if (check.status === 'outdated') { + const latest = check.latest_version; + + actions.push({ + loadingKey: 'singBoxExtendedAction', + kind: 'update', + text: latest ? _('Install %s').replace('%s', latest) : _('Update'), + backendAction: 'install_extended', + }); + } else { + actions.push({ + loadingKey: 'singBoxExtendedCheck', + kind: 'check', + text: _('Check update'), + backendAction: 'check_update', + }); + } + } else { + actions.push({ + loadingKey: 'singBoxExtendedAction', + kind: 'switch', + text: _('Switch to extended'), + backendAction: 'install_extended', + }); + } + + return { + key: 'sing_box_extended', + title: 'sing-box (extended)', + version: isActive ? systemInfo.sing_box_version : _('Not installed'), + installed: isActive, + tag: isActive ? getCheckTag(check.status) : getCheckTag('not_installed'), + actions, + }; +} + +/** + * Build the three Component Manager cards from systemInfo + per-component check + * state. Pure (no DOM, no store) so it is unit-testable; the controller maps + * descriptors to DOM + click handlers. + */ +export function getComponentCards( + systemInfo: ManagerSystemInfo, + checks: Record<ManagerComponentKey, ManagerCheckState>, +): ManagerCardDescriptor[] { + return [ + netshiftCard(systemInfo), + singBoxStockCard(systemInfo, checks.sing_box_stock), + singBoxExtendedCard(systemInfo, checks.sing_box_extended), + ]; +} diff --git a/fe-app-netshift/src/netshift/tabs/manager/index.ts b/fe-app-netshift/src/netshift/tabs/manager/index.ts new file mode 100644 index 00000000..08c7657d --- /dev/null +++ b/fe-app-netshift/src/netshift/tabs/manager/index.ts @@ -0,0 +1,9 @@ +import { render } from './render'; +import { initController } from './initController'; +import { styles } from './styles'; + +export const ManagerTab = { + render, + initController, + styles, +}; diff --git a/fe-app-netshift/src/netshift/tabs/manager/initController.ts b/fe-app-netshift/src/netshift/tabs/manager/initController.ts new file mode 100644 index 00000000..37f1b7ad --- /dev/null +++ b/fe-app-netshift/src/netshift/tabs/manager/initController.ts @@ -0,0 +1,434 @@ +import { onMount, preserveScrollForPage } from '../../../helpers'; +import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVersion'; +import { showToast } from '../../../helpers/showToast'; +import { renderRotateCcwIcon24, renderSearchIcon24 } from '../../../icons'; +import { renderButton } from '../../../partials'; +import { NetShiftShellMethods } from '../../methods'; +import { logger, store, StoreType } from '../../services'; +import { NetShift } from '../../types'; +import { + ManagerActionDescriptor, + ManagerCardDescriptor, + ManagerComponentKey, + getComponentCards, +} from './cards'; + +type ManagerActionKey = keyof StoreType['managerActions']; + +let managerLifecycleRegistered = false; +let managerControllerInitialized = false; +let managerMounted = false; + +async function fetchSystemInfo() { + const systemInfo = await NetShiftShellMethods.getSystemInfo(); + + if (systemInfo.success) { + store.set({ + diagnosticsSystemInfo: { + loading: false, + ...systemInfo.data, + sing_box_extended: systemInfo.data.sing_box_extended === 1 ? 1 : 0, + }, + }); + } else { + store.set({ + diagnosticsSystemInfo: { + loading: false, + netshift_version: _('unknown'), + netshift_latest_version: _('unknown'), + luci_app_version: _('unknown'), + sing_box_version: _('unknown'), + openwrt_version: _('unknown'), + device_model: _('unknown'), + sing_box_extended: 0, + }, + }); + } +} + +function isAnyActionLoading() { + return Object.values(store.get().managerActions).some((item) => item.loading); +} + +function isSystemInfoLoading() { + return store.get().diagnosticsSystemInfo.loading; +} + +function setActionLoading(action: ManagerActionKey, loading: boolean) { + const managerActions = store.get().managerActions; + + store.set({ + managerActions: { + ...managerActions, + [action]: { loading }, + }, + }); +} + +function setCheckResult( + component: ManagerComponentKey, + status: NetShift.ComponentUpdateStatus | null, + latestVersion: string, +) { + const managerChecks = store.get().managerChecks; + + store.set({ + managerChecks: { + ...managerChecks, + [component]: { + status, + latest_version: latestVersion, + }, + }, + }); +} + +function resetCheckResult(component: ManagerComponentKey) { + setCheckResult(component, null, ''); +} + +function getCheckToastMessage(status: NetShift.ComponentUpdateStatus | null) { + if (status === 'outdated') { + return _('Update is available'); + } + + if (status === 'dev') { + return _('Installed version is newer than release'); + } + + if (status === 'not_installed') { + return _('Not installed'); + } + + return _('Latest version is installed'); +} + +// Sing-box check: routes to the sing-box check method (stock or extended) and +// stores the result into the matching managerChecks slice. +async function runSingBoxCheck( + component: ManagerComponentKey, + button: ManagerActionDescriptor, +) { + setActionLoading(button.loadingKey, true); + + try { + const parsed = await NetShiftShellMethods.singBoxCheckUpdate( + button.backendAction === 'check_update_stable' + ? 'check_update_stable' + : 'check_update', + ); + + if (!parsed.success) { + showToast(parsed.message || _('Failed to execute!'), 'error'); + return; + } + + const status = parsed.status ?? null; + + setCheckResult(component, status, parsed.latest_version || ''); + showToast(getCheckToastMessage(status), 'success'); + } catch (error) { + logger.error('[MANAGER]', 'runSingBoxCheck failed', error); + showToast(_('Failed to execute!'), 'error'); + } finally { + setActionLoading(button.loadingKey, false); + } +} + +// NetShift check: the backend has NO netshift:check_update action — NetShift's +// latest version comes only from get_system_info.netshift_latest_version. So an +// on-demand NetShift check is a systemInfo REFRESH; the card then re-derives its +// status from the refreshed installed-vs-latest comparison. We never write a +// sing-box check result into managerChecks.netshift. +async function runNetshiftCheck(button: ManagerActionDescriptor) { + setActionLoading(button.loadingKey, true); + + try { + await fetchSystemInfo(); + resetCheckResult('netshift'); + + const status = store.get().diagnosticsSystemInfo; + const installed = normalizeCompiledVersion(status.netshift_version); + const latest = status.netshift_latest_version; + + if (!latest || latest === 'loading' || latest === _('unknown')) { + showToast(_('Latest version is unknown'), 'success'); + } else if (installed === 'dev') { + showToast(getCheckToastMessage('dev'), 'success'); + } else { + showToast( + getCheckToastMessage(installed === latest ? 'latest' : 'outdated'), + 'success', + ); + } + } catch (error) { + logger.error('[MANAGER]', 'runNetshiftCheck failed', error); + showToast(_('Failed to execute!'), 'error'); + } finally { + setActionLoading(button.loadingKey, false); + } +} + +async function runSingBoxMutation( + component: ManagerComponentKey, + button: ManagerActionDescriptor, +) { + setActionLoading(button.loadingKey, true); + showToast( + _('Switching sing-box core, this may take a few minutes…'), + 'success', + ); + + try { + const result = await NetShiftShellMethods.singBoxComponentAction( + button.backendAction === 'install_stable' + ? 'install_stable' + : 'install_extended', + ); + + if (result.success) { + const changed = _('Sing-box core changed, version:'); + + showToast(`${changed} ${result.version || ''}`.trim(), 'success'); + resetCheckResult(component); + await fetchSystemInfo(); + } else { + logger.error('[MANAGER]', 'runSingBoxMutation failed', result); + showToast(result.message || _('Failed to execute!'), 'error'); + } + } catch (error) { + logger.error('[MANAGER]', 'runSingBoxMutation failed', error); + showToast(_('Failed to execute!'), 'error'); + } finally { + setActionLoading(button.loadingKey, false); + } +} + +function reloadPageAfterSelfUpdate() { + window.setTimeout(() => { + window.location.reload(); + }, 1200); +} + +async function runNetshiftSelfUpdate(button: ManagerActionDescriptor) { + setActionLoading(button.loadingKey, true); + // Warning-style toast: self-update is long and ends in a page reload. + showToast( + _('Updating NetShift, this may take a few minutes; the page will reload…'), + 'success', + 6000, + ); + + try { + const result = await NetShiftShellMethods.netshiftSelfUpdate(); + + if (result.success) { + const updated = _('NetShift updated, version:'); + + showToast(`${updated} ${result.version || ''}`.trim(), 'success', 1200); + reloadPageAfterSelfUpdate(); + return; + } + + logger.error('[MANAGER]', 'runNetshiftSelfUpdate failed', result); + showToast(result.message || _('Failed to execute!'), 'error'); + setActionLoading(button.loadingKey, false); + } catch (error) { + logger.error('[MANAGER]', 'runNetshiftSelfUpdate failed', error); + showToast(_('Failed to execute!'), 'error'); + setActionLoading(button.loadingKey, false); + } +} + +function handleManagerAction( + card: ManagerCardDescriptor, + button: ManagerActionDescriptor, +) { + if (isAnyActionLoading()) { + return; + } + + if (button.kind === 'check_netshift') { + void runNetshiftCheck(button); + return; + } + + if (button.kind === 'check') { + void runSingBoxCheck(card.key, button); + return; + } + + if (button.kind === 'self_update') { + void runNetshiftSelfUpdate(button); + return; + } + + // `update` / `switch` — both drive the async sing-box install contract. + void runSingBoxMutation(card.key, button); +} + +function renderComponentTag(card: ManagerCardDescriptor) { + if (!card.tag) { + return null; + } + + return E( + 'span', + { + class: [ + 'pdk_manager-page__component__tag', + card.tag.kind === 'success' + ? 'pdk_manager-page__component__tag--success' + : '', + card.tag.kind === 'warning' + ? 'pdk_manager-page__component__tag--warning' + : '', + ] + .filter(Boolean) + .join(' '), + }, + card.tag.label, + ); +} + +function renderComponentCard(card: ManagerCardDescriptor) { + const managerActions = store.get().managerActions; + const anyActionLoading = isAnyActionLoading(); + const systemInfoLoading = isSystemInfoLoading(); + const tag = renderComponentTag(card); + const headerChildren: Node[] = [ + E('b', { class: 'pdk_manager-page__component__title' }, card.title), + ]; + + if (tag) { + headerChildren.push( + E('div', { class: 'pdk_manager-page__component__status' }, [tag]), + ); + } + + return E('div', { class: 'pdk_manager-page__component' }, [ + E('div', { class: 'pdk_manager-page__component__header' }, headerChildren), + E('div', { class: 'pdk_manager-page__component__version' }, [ + E( + 'span', + { class: 'pdk_manager-page__component__version__label' }, + _('Version'), + ), + E( + 'span', + { class: 'pdk_manager-page__component__version__value' }, + card.version, + ), + ]), + E( + 'div', + { class: 'pdk_manager-page__component__actions' }, + card.actions.map((action) => { + const loading = managerActions[action.loadingKey].loading; + + return renderButton({ + text: action.text, + icon: + action.kind === 'check' || action.kind === 'check_netshift' + ? renderSearchIcon24 + : renderRotateCcwIcon24, + loading, + disabled: systemInfoLoading || (anyActionLoading && !loading), + onClick: () => handleManagerAction(card, action), + }); + }), + ), + ]); +} + +function renderManagerComponents() { + const container = document.getElementById('pdk_manager-components'); + + if (!container) { + return; + } + + const { diagnosticsSystemInfo, managerChecks } = store.get(); + const renderedComponents = getComponentCards( + { + netshift_version: normalizeCompiledVersion( + diagnosticsSystemInfo.netshift_version, + ), + netshift_latest_version: diagnosticsSystemInfo.netshift_latest_version, + sing_box_version: diagnosticsSystemInfo.sing_box_version, + sing_box_extended: diagnosticsSystemInfo.sing_box_extended, + }, + managerChecks, + ).map(renderComponentCard); + + return preserveScrollForPage(() => { + container.replaceChildren(...renderedComponents); + }); +} + +function onStoreUpdate( + _next: StoreType, + _prev: StoreType, + diff: Partial<StoreType>, +) { + if (diff.diagnosticsSystemInfo || diff.managerActions || diff.managerChecks) { + renderManagerComponents(); + } +} + +function onPageMount() { + onPageUnmount(); + + managerMounted = true; + store.subscribe(onStoreUpdate); + renderManagerComponents(); + void fetchSystemInfo(); +} + +function onPageUnmount() { + managerMounted = false; + store.unsubscribe(onStoreUpdate); + store.reset(['managerActions', 'managerChecks']); +} + +function registerLifecycleListeners() { + if (managerLifecycleRegistered) { + return; + } + + managerLifecycleRegistered = true; + + store.subscribe((next, prev, diff) => { + if ( + diff.tabService && + next.tabService.current !== prev.tabService.current + ) { + const isManagerVisible = next.tabService.current === 'manager'; + + if (isManagerVisible) { + return onPageMount(); + } + + if (managerMounted) { + return onPageUnmount(); + } + } + }); +} + +export async function initController(): Promise<void> { + if (managerControllerInitialized) { + return; + } + + managerControllerInitialized = true; + + onMount('manager-status').then(() => { + logger.debug('[MANAGER]', 'initController', 'onMount'); + registerLifecycleListeners(); + + if (store.get().tabService.current === 'manager') { + onPageMount(); + } + }); +} diff --git a/fe-app-netshift/src/netshift/tabs/manager/manager.store.ts b/fe-app-netshift/src/netshift/tabs/manager/manager.store.ts new file mode 100644 index 00000000..54df338b --- /dev/null +++ b/fe-app-netshift/src/netshift/tabs/manager/manager.store.ts @@ -0,0 +1,20 @@ +import { StoreType } from '../../services'; + +export const initialManagerStore: Pick< + StoreType, + 'managerActions' | 'managerChecks' +> = { + managerActions: { + netshiftCheck: { loading: false }, + netshiftUpdate: { loading: false }, + singBoxStockCheck: { loading: false }, + singBoxStockAction: { loading: false }, + singBoxExtendedCheck: { loading: false }, + singBoxExtendedAction: { loading: false }, + }, + managerChecks: { + netshift: { status: null, latest_version: '' }, + sing_box_stock: { status: null, latest_version: '' }, + sing_box_extended: { status: null, latest_version: '' }, + }, +}; diff --git a/fe-app-netshift/src/netshift/tabs/manager/render.ts b/fe-app-netshift/src/netshift/tabs/manager/render.ts new file mode 100644 index 00000000..5c140930 --- /dev/null +++ b/fe-app-netshift/src/netshift/tabs/manager/render.ts @@ -0,0 +1,8 @@ +export function render() { + return E('div', { id: 'manager-status', class: 'pdk_manager-page' }, [ + E('div', { + id: 'pdk_manager-components', + class: 'pdk_manager-page__components', + }), + ]); +} diff --git a/fe-app-netshift/src/netshift/tabs/manager/styles.ts b/fe-app-netshift/src/netshift/tabs/manager/styles.ts new file mode 100644 index 00000000..8156d9c5 --- /dev/null +++ b/fe-app-netshift/src/netshift/tabs/manager/styles.ts @@ -0,0 +1,109 @@ +// language=CSS +export const styles = ` +#cbi-netshift-manager-_mount_node > div { + width: 100%; +} + +#cbi-netshift-manager > h3 { + display: none; +} + +.pdk_manager-page { + width: 100%; +} + +.pdk_manager-page__components { + display: grid; + grid-template-columns: repeat(2, minmax(240px, 1fr)); + grid-gap: 10px; +} + +@media (max-width: 760px) { + .pdk_manager-page__components { + grid-template-columns: 1fr; + } +} + +.pdk_manager-page__component { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; + min-width: 0; + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} + +.pdk_manager-page__component__header { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, auto); + align-items: start; + gap: 8px; + min-width: 0; +} + +.pdk_manager-page__component__title { + color: var(--text-color-high); + line-height: 1.25; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pdk_manager-page__component__status { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + min-width: 0; + max-width: 180px; + overflow: hidden; +} + +.pdk_manager-page__component__version { + display: grid; + grid-template-columns: auto 1fr; + grid-column-gap: 6px; + align-items: baseline; + min-width: 0; +} + +.pdk_manager-page__component__version__label { + color: var(--text-color-medium); +} + +.pdk_manager-page__component__version__value { + min-width: 0; + overflow-wrap: anywhere; +} + +.pdk_manager-page__component__tag { + flex: 0 0 auto; + padding: 2px 5px; + border: 1px var(--background-color-high, gray) solid; + border-radius: 4px; + color: var(--text-color-medium, gray); + line-height: 1.2; +} + +.pdk_manager-page__component__tag--success { + border-color: var(--success-color-medium, green); + color: var(--success-color-medium, green); +} + +.pdk_manager-page__component__tag--warning { + border-color: var(--warn-color-medium, orange); + color: var(--warn-color-medium, orange); +} + +.pdk_manager-page__component__actions { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.pdk_manager-page__component__actions > .pdk-partial-button { + margin-left: 0; +} +`; diff --git a/fe-app-netshift/src/netshift/tabs/manager/tests/cards.test.js b/fe-app-netshift/src/netshift/tabs/manager/tests/cards.test.js new file mode 100644 index 00000000..870f8a1f --- /dev/null +++ b/fe-app-netshift/src/netshift/tabs/manager/tests/cards.test.js @@ -0,0 +1,207 @@ +import { describe, expect, it } from 'vitest'; +import { getCheckTag, getComponentCards, isSingBoxInstalled } from '../cards'; + +const emptyChecks = { + netshift: { status: null, latest_version: '' }, + sing_box_stock: { status: null, latest_version: '' }, + sing_box_extended: { status: null, latest_version: '' }, +}; + +function makeSystemInfo(patch = {}) { + return { + netshift_version: '1.0.0', + netshift_latest_version: '1.0.0', + sing_box_version: '1.12.0', + sing_box_extended: 0, + ...patch, + }; +} + +describe('getCheckTag', () => { + it.each([ + ['latest', { label: 'Latest', kind: 'success' }], + ['outdated', { label: 'Outdated', kind: 'warning' }], + ['dev', { label: 'Dev', kind: 'neutral' }], + ['not_installed', { label: 'Not installed', kind: 'neutral' }], + ])('maps status %s to the right badge', (status, expected) => { + expect(getCheckTag(status)).toEqual(expected); + }); + + it('returns undefined for a null status', () => { + expect(getCheckTag(null)).toBeUndefined(); + }); +}); + +describe('isSingBoxInstalled', () => { + it.each([ + ['1.12.0', true], + ['not installed', false], + ['', false], + ])('treats %s as installed=%s', (version, expected) => { + expect( + isSingBoxInstalled(makeSystemInfo({ sing_box_version: version })), + ).toBe(expected); + }); +}); + +describe('getComponentCards', () => { + it('always builds exactly three cards in order', () => { + const cards = getComponentCards(makeSystemInfo(), emptyChecks); + + expect(cards.map((c) => c.key)).toEqual([ + 'netshift', + 'sing_box_stock', + 'sing_box_extended', + ]); + }); + + it('shows the stock card as installed/active when sing_box_extended=0', () => { + const cards = getComponentCards( + makeSystemInfo({ sing_box_extended: 0, sing_box_version: '1.12.0' }), + emptyChecks, + ); + const [, stock, extended] = cards; + + expect(stock.installed).toBe(true); + expect(stock.version).toBe('1.12.0'); + // No check yet → no badge for the active card. + expect(stock.tag).toBeUndefined(); + expect(stock.actions[0].kind).toBe('check'); + expect(stock.actions[0].backendAction).toBe('check_update_stable'); + + // Inactive extended card → "Not installed" + switch-to-extended. + expect(extended.installed).toBe(false); + expect(extended.version).toBe('Not installed'); + expect(extended.actions[0].kind).toBe('switch'); + expect(extended.actions[0].backendAction).toBe('install_extended'); + }); + + it('mirrors the layout when sing_box_extended=1', () => { + const cards = getComponentCards( + makeSystemInfo({ sing_box_extended: 1, sing_box_version: '1.12.5' }), + emptyChecks, + ); + const [, stock, extended] = cards; + + expect(extended.installed).toBe(true); + expect(extended.actions[0].backendAction).toBe('check_update'); + + expect(stock.installed).toBe(false); + expect(stock.actions[0].kind).toBe('switch'); + expect(stock.actions[0].backendAction).toBe('install_stable'); + }); + + it('offers switch-to on both cores when sing-box is absent', () => { + const cards = getComponentCards( + makeSystemInfo({ sing_box_version: 'not installed' }), + emptyChecks, + ); + const [, stock, extended] = cards; + + expect(stock.installed).toBe(false); + expect(stock.actions[0].kind).toBe('switch'); + expect(extended.installed).toBe(false); + expect(extended.actions[0].kind).toBe('switch'); + }); + + it('turns an outdated stock check into an Install %s update action', () => { + const cards = getComponentCards( + makeSystemInfo({ sing_box_extended: 0, sing_box_version: '1.12.0' }), + { + ...emptyChecks, + sing_box_stock: { status: 'outdated', latest_version: '1.12.9' }, + }, + ); + const stock = cards[1]; + + expect(stock.tag).toEqual({ label: 'Outdated', kind: 'warning' }); + expect(stock.actions[0].kind).toBe('update'); + expect(stock.actions[0].backendAction).toBe('install_stable'); + expect(stock.actions[0].text).toBe('Install 1.12.9'); + }); + + it('derives an outdated NetShift card from systemInfo latest mismatch', () => { + const cards = getComponentCards( + makeSystemInfo({ + netshift_version: '1.0.0', + netshift_latest_version: '1.1.0', + }), + emptyChecks, + ); + const netshift = cards[0]; + + expect(netshift.tag).toEqual({ label: 'Outdated', kind: 'warning' }); + expect(netshift.actions[0].kind).toBe('self_update'); + expect(netshift.actions[0].backendAction).toBe('self_update'); + expect(netshift.actions[0].text).toBe('Install 1.1.0'); + }); + + it('keeps the NetShift card on Check update when versions match', () => { + const cards = getComponentCards( + makeSystemInfo({ + netshift_version: '1.1.0', + netshift_latest_version: '1.1.0', + }), + emptyChecks, + ); + const netshift = cards[0]; + + expect(netshift.tag).toEqual({ label: 'Latest', kind: 'success' }); + // The NetShift check is a DISTINCT kind so it can never be routed to the + // sing-box check method. + expect(netshift.actions[0].kind).toBe('check_netshift'); + }); + + it('NetShift check action carries NO sing-box backendAction', () => { + // C1 regression guard: the NetShift "Check update" must never be a sing-box + // check (the backend has no netshift:check_update action). Its action has no + // backendAction at all — it triggers a systemInfo refresh in the controller. + const cards = getComponentCards( + makeSystemInfo({ + netshift_version: '1.0.0', + netshift_latest_version: '1.0.0', + }), + emptyChecks, + ); + const netshift = cards[0]; + + expect(netshift.actions[0].kind).toBe('check_netshift'); + expect(netshift.actions[0].backendAction).toBeUndefined(); + expect(['check_update', 'check_update_stable']).not.toContain( + netshift.actions[0].backendAction, + ); + }); + + it('derives NetShift status purely from systemInfo, ignoring managerChecks', () => { + // Even if a (bogus) sing-box-style status leaked into managerChecks.netshift, + // the NetShift card must derive its status from systemInfo versions only. + const cards = getComponentCards( + makeSystemInfo({ + netshift_version: '1.0.0', + netshift_latest_version: '1.0.0', + }), + { + ...emptyChecks, + netshift: { status: 'outdated', latest_version: '9.9.9' }, + }, + ); + const netshift = cards[0]; + + expect(netshift.tag).toEqual({ label: 'Latest', kind: 'success' }); + expect(netshift.actions[0].kind).toBe('check_netshift'); + }); + + it('treats an unknown NetShift latest as no status (Check update, no badge)', () => { + const cards = getComponentCards( + makeSystemInfo({ + netshift_version: '1.0.0', + netshift_latest_version: 'unknown', + }), + emptyChecks, + ); + const netshift = cards[0]; + + expect(netshift.tag).toBeUndefined(); + expect(netshift.actions[0].kind).toBe('check_netshift'); + }); +}); diff --git a/fe-app-netshift/src/netshift/types.ts b/fe-app-netshift/src/netshift/types.ts index 46120573..17572349 100644 --- a/fe-app-netshift/src/netshift/types.ts +++ b/fe-app-netshift/src/netshift/types.ts @@ -230,4 +230,23 @@ export namespace NetShift { } export type GetClashApiGroupLatency = Record<string, number>; + + // Component Manager (task-018) — consumes the STABLE backend contract from + // task-017. Status union returned by the sync update-check actions. + export type ComponentUpdateStatus = + | 'latest' + | 'outdated' + | 'dev' + | 'not_installed'; + + // Shape echoed by the sync update-check actions: + // component_action sing_box check_update (extended) + // component_action sing_box check_update_stable (stock) + export interface ComponentCheckUpdateResult { + success: boolean; + current_version?: string; + latest_version?: string; + status?: ComponentUpdateStatus; + message?: string; + } } diff --git a/fe-app-netshift/src/styles.ts b/fe-app-netshift/src/styles.ts index b553b600..efb12925 100644 --- a/fe-app-netshift/src/styles.ts +++ b/fe-app-netshift/src/styles.ts @@ -1,10 +1,11 @@ // language=CSS -import { DashboardTab, DiagnosticTab } from './netshift'; +import { DashboardTab, DiagnosticTab, ManagerTab } from './netshift'; import { PartialStyles } from './partials'; export const GlobalStyles = ` ${DashboardTab.styles} ${DiagnosticTab.styles} +${ManagerTab.styles} ${PartialStyles} diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 4d7013be..f8b39409 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -834,6 +834,37 @@ async function pollSingBoxComponentAction(fetchStatus, sleepFn = sleep, interval }; } +// src/netshift/methods/shell/parseComponentCheckUpdate.ts +var VALID_STATUSES = [ + "latest", + "outdated", + "dev", + "not_installed" +]; +function normalizeStatus(status) { + if (typeof status === "string" && VALID_STATUSES.includes(status)) { + return status; + } + return void 0; +} +function parseComponentCheckUpdate(stdout) { + try { + const parsed = JSON.parse(stdout); + return { + success: Boolean(parsed.success), + current_version: typeof parsed.current_version === "string" ? parsed.current_version : void 0, + latest_version: typeof parsed.latest_version === "string" ? parsed.latest_version : void 0, + status: normalizeStatus(parsed.status), + message: typeof parsed.message === "string" ? parsed.message : void 0 + }; + } catch (_e) { + return { + success: false, + message: stdout + }; + } +} + // src/netshift/methods/shell/index.ts var NetShiftShellMethods = { checkDNSAvailable: async () => callBaseMethod( @@ -960,6 +991,69 @@ var NetShiftShellMethods = { } return parseComponentActionStatus(statusResponse.stdout); }); + }, + // Sing-box update checks (sync) — STABLE task-017 contract: + // component_action sing_box check_update (extended) + // component_action sing_box check_update_stable (stock) + // → {success, current_version, latest_version, status}. + singBoxCheckUpdate: async (action) => { + const response = await executeShellCommand({ + command: "/usr/bin/netshift", + args: ["component_action", "sing_box", action], + timeout: 6e5 + }); + if (response.stdout) { + return parseComponentCheckUpdate(response.stdout); + } + return { + success: false, + message: response.stderr || "" + }; + }, + // NetShift self-update (async) — STABLE task-017 contract: + // component_action_async netshift self_update + component_action_status <job>. + // Reuses the component-agnostic poll. Because the package install swaps + // /usr/bin/netshift mid-job, status polls can transiently fail (rpcd / binary + // swap); once the job has STARTED we treat such failures leniently — keep + // polling (return a synthetic running status) instead of aborting hard, so a + // successful self-update is not misreported as a failure. The UI reloads the + // page on success. + netshiftSelfUpdate: async () => { + const startResponse = await executeShellCommand({ + command: "/usr/bin/netshift", + args: ["component_action_async", "netshift", "self_update"] + }); + let start = null; + if (startResponse.stdout) { + try { + start = JSON.parse( + startResponse.stdout + ); + } catch (_e) { + start = null; + } + } + if (!start || start.success !== true || !start.job_id) { + return { + success: false, + message: start?.message || startResponse.stderr || _("Self-update failed") + }; + } + const jobId = start.job_id; + return pollSingBoxComponentAction(async () => { + try { + const statusResponse = await executeShellCommand({ + command: "/usr/bin/netshift", + args: ["component_action_status", jobId] + }); + if (!statusResponse.stdout) { + return { running: true }; + } + return parseComponentActionStatus(statusResponse.stdout) ?? { running: true }; + } catch (_e) { + return { running: true }; + } + }); } }; @@ -1517,9 +1611,6 @@ var initialDiagnosticStore = { }, showSingBoxConfig: { loading: false - }, - singBoxInstall: { - loading: false } }, diagnosticsRunAction: { loading: false }, @@ -1611,6 +1702,23 @@ var loadingDiagnosticsChecksStore = { ] }; +// src/netshift/tabs/manager/manager.store.ts +var initialManagerStore = { + managerActions: { + netshiftCheck: { loading: false }, + netshiftUpdate: { loading: false }, + singBoxStockCheck: { loading: false }, + singBoxStockAction: { loading: false }, + singBoxExtendedCheck: { loading: false }, + singBoxExtendedAction: { loading: false } + }, + managerChecks: { + netshift: { status: null, latest_version: "" }, + sing_box_stock: { status: null, latest_version: "" }, + sing_box_extended: { status: null, latest_version: "" } + } +}; + // src/netshift/services/store.service.ts function jsonStableStringify(obj) { return JSON.stringify(obj, (_2, value) => { @@ -1732,7 +1840,8 @@ var initialStore = { latencyFetching: false, data: [] }, - ...initialDiagnosticStore + ...initialDiagnosticStore, + ...initialManagerStore }; var store = new StoreService(initialStore); @@ -3858,9 +3967,7 @@ function renderAvailableActions({ disable, globalCheck, viewLogs, - showSingBoxConfig, - singBoxInstall, - singBoxExtended + showSingBoxConfig }) { return E("div", { class: "pdk_diagnostic-page__right-bar__actions" }, [ E("b", {}, _("Available actions")), @@ -3940,15 +4047,6 @@ function renderAvailableActions({ loading: showSingBoxConfig.loading, disabled: showSingBoxConfig.disabled }) - ]), - ...insertIf(singBoxInstall.visible, [ - renderButton({ - onClick: singBoxInstall.onClick, - icon: renderRotateCcwIcon24, - text: singBoxExtended ? _("Install stable") : _("Install extended"), - loading: singBoxInstall.loading, - disabled: singBoxInstall.disabled - }) ]) ]); } @@ -4606,45 +4704,6 @@ async function handleShowSingBoxConfig() { }); } } -async function handleInstallSingBox() { - const diagnosticsActions = store.get().diagnosticsActions; - store.set({ - diagnosticsActions: { - ...diagnosticsActions, - singBoxInstall: { loading: true } - } - }); - const isExtended = store.get().diagnosticsSystemInfo.sing_box_extended === 1; - showToast( - _("Switching sing-box core, this may take a few minutes\u2026"), - "success" - ); - try { - const result = await NetShiftShellMethods.singBoxComponentAction( - isExtended ? "install_stable" : "install_extended" - ); - if (result.success) { - showToast( - _("Sing-box core changed, version: ") + (result.version || ""), - "success" - ); - } else { - logger.error("[DIAGNOSTIC]", "handleInstallSingBox - e", result); - showToast(result.message || _("Failed to execute!"), "error"); - } - } catch (e) { - logger.error("[DIAGNOSTIC]", "handleInstallSingBox - e", e); - showToast(_("Failed to execute!"), "error"); - } finally { - store.set({ - diagnosticsActions: { - ...diagnosticsActions, - singBoxInstall: { loading: false } - } - }); - await fetchSystemInfo(); - } -} function renderWikiDisclaimerWidget() { const diagnosticsChecks = store.get().diagnosticsChecks; function getWikiKind() { @@ -4718,14 +4777,7 @@ function renderDiagnosticAvailableActionsWidget() { visible: true, onClick: handleShowSingBoxConfig, disabled: atLeastOneServiceCommandLoading - }, - singBoxInstall: { - loading: diagnosticsActions.singBoxInstall.loading, - visible: true, - onClick: handleInstallSingBox, - disabled: atLeastOneServiceCommandLoading || diagnosticsActions.singBoxInstall.loading - }, - singBoxExtended: store.get().diagnosticsSystemInfo.sing_box_extended + } }); return preserveScrollForPage(() => { container.replaceChildren(renderedActions); @@ -5046,10 +5098,598 @@ var DiagnosticTab = { styles: styles4 }; +// src/netshift/tabs/manager/render.ts +function render3() { + return E("div", { id: "manager-status", class: "pdk_manager-page" }, [ + E("div", { + id: "pdk_manager-components", + class: "pdk_manager-page__components" + }) + ]); +} + +// src/netshift/tabs/manager/cards.ts +var NOT_INSTALLED = "not installed"; +function isSingBoxInstalled(systemInfo) { + const version = systemInfo.sing_box_version; + return Boolean(version) && version !== NOT_INSTALLED; +} +function getCheckTag(status) { + if (!status) { + return void 0; + } + if (status === "latest") { + return { label: _("Latest"), kind: "success" }; + } + if (status === "outdated") { + return { label: _("Outdated"), kind: "warning" }; + } + if (status === "not_installed") { + return { label: _("Not installed"), kind: "neutral" }; + } + return { label: _("Dev"), kind: "neutral" }; +} +function netshiftStatus(systemInfo) { + const installed = normalizeCompiledVersion(systemInfo.netshift_version); + const latest = systemInfo.netshift_latest_version; + if (!latest || latest === "loading" || latest === _("unknown")) { + return null; + } + if (installed === "dev") { + return null; + } + return installed === latest ? "latest" : "outdated"; +} +function netshiftCard(systemInfo) { + const status = netshiftStatus(systemInfo); + const latest = systemInfo.netshift_latest_version; + const actions = []; + if (status === "outdated") { + actions.push({ + loadingKey: "netshiftUpdate", + kind: "self_update", + text: latest && latest !== "loading" ? _("Install %s").replace("%s", latest) : _("Update NetShift"), + backendAction: "self_update" + }); + } else { + actions.push({ + loadingKey: "netshiftCheck", + kind: "check_netshift", + text: _("Check update") + }); + } + return { + key: "netshift", + title: "NetShift", + version: normalizeCompiledVersion(systemInfo.netshift_version), + installed: true, + tag: getCheckTag(status), + actions + }; +} +function singBoxStockCard(systemInfo, check) { + const installed = isSingBoxInstalled(systemInfo); + const isActive = installed && systemInfo.sing_box_extended === 0; + const actions = []; + if (isActive) { + if (check.status === "outdated") { + const latest = check.latest_version; + actions.push({ + loadingKey: "singBoxStockAction", + kind: "update", + text: latest ? _("Install %s").replace("%s", latest) : _("Update"), + backendAction: "install_stable" + }); + } else { + actions.push({ + loadingKey: "singBoxStockCheck", + kind: "check", + text: _("Check update"), + backendAction: "check_update_stable" + }); + } + } else { + actions.push({ + loadingKey: "singBoxStockAction", + kind: "switch", + text: _("Switch to stable"), + backendAction: "install_stable" + }); + } + return { + key: "sing_box_stock", + title: "sing-box (stock)", + version: isActive ? systemInfo.sing_box_version : _("Not installed"), + installed: isActive, + tag: isActive ? getCheckTag(check.status) : getCheckTag("not_installed"), + actions + }; +} +function singBoxExtendedCard(systemInfo, check) { + const installed = isSingBoxInstalled(systemInfo); + const isActive = installed && systemInfo.sing_box_extended === 1; + const actions = []; + if (isActive) { + if (check.status === "outdated") { + const latest = check.latest_version; + actions.push({ + loadingKey: "singBoxExtendedAction", + kind: "update", + text: latest ? _("Install %s").replace("%s", latest) : _("Update"), + backendAction: "install_extended" + }); + } else { + actions.push({ + loadingKey: "singBoxExtendedCheck", + kind: "check", + text: _("Check update"), + backendAction: "check_update" + }); + } + } else { + actions.push({ + loadingKey: "singBoxExtendedAction", + kind: "switch", + text: _("Switch to extended"), + backendAction: "install_extended" + }); + } + return { + key: "sing_box_extended", + title: "sing-box (extended)", + version: isActive ? systemInfo.sing_box_version : _("Not installed"), + installed: isActive, + tag: isActive ? getCheckTag(check.status) : getCheckTag("not_installed"), + actions + }; +} +function getComponentCards(systemInfo, checks) { + return [ + netshiftCard(systemInfo), + singBoxStockCard(systemInfo, checks.sing_box_stock), + singBoxExtendedCard(systemInfo, checks.sing_box_extended) + ]; +} + +// src/netshift/tabs/manager/initController.ts +var managerLifecycleRegistered = false; +var managerControllerInitialized = false; +var managerMounted = false; +async function fetchSystemInfo2() { + const systemInfo = await NetShiftShellMethods.getSystemInfo(); + if (systemInfo.success) { + store.set({ + diagnosticsSystemInfo: { + loading: false, + ...systemInfo.data, + sing_box_extended: systemInfo.data.sing_box_extended === 1 ? 1 : 0 + } + }); + } else { + store.set({ + diagnosticsSystemInfo: { + loading: false, + netshift_version: _("unknown"), + netshift_latest_version: _("unknown"), + luci_app_version: _("unknown"), + sing_box_version: _("unknown"), + openwrt_version: _("unknown"), + device_model: _("unknown"), + sing_box_extended: 0 + } + }); + } +} +function isAnyActionLoading() { + return Object.values(store.get().managerActions).some((item) => item.loading); +} +function isSystemInfoLoading() { + return store.get().diagnosticsSystemInfo.loading; +} +function setActionLoading(action, loading) { + const managerActions = store.get().managerActions; + store.set({ + managerActions: { + ...managerActions, + [action]: { loading } + } + }); +} +function setCheckResult(component, status, latestVersion) { + const managerChecks = store.get().managerChecks; + store.set({ + managerChecks: { + ...managerChecks, + [component]: { + status, + latest_version: latestVersion + } + } + }); +} +function resetCheckResult(component) { + setCheckResult(component, null, ""); +} +function getCheckToastMessage(status) { + if (status === "outdated") { + return _("Update is available"); + } + if (status === "dev") { + return _("Installed version is newer than release"); + } + if (status === "not_installed") { + return _("Not installed"); + } + return _("Latest version is installed"); +} +async function runSingBoxCheck2(component, button) { + setActionLoading(button.loadingKey, true); + try { + const parsed = await NetShiftShellMethods.singBoxCheckUpdate( + button.backendAction === "check_update_stable" ? "check_update_stable" : "check_update" + ); + if (!parsed.success) { + showToast(parsed.message || _("Failed to execute!"), "error"); + return; + } + const status = parsed.status ?? null; + setCheckResult(component, status, parsed.latest_version || ""); + showToast(getCheckToastMessage(status), "success"); + } catch (error) { + logger.error("[MANAGER]", "runSingBoxCheck failed", error); + showToast(_("Failed to execute!"), "error"); + } finally { + setActionLoading(button.loadingKey, false); + } +} +async function runNetshiftCheck(button) { + setActionLoading(button.loadingKey, true); + try { + await fetchSystemInfo2(); + resetCheckResult("netshift"); + const status = store.get().diagnosticsSystemInfo; + const installed = normalizeCompiledVersion(status.netshift_version); + const latest = status.netshift_latest_version; + if (!latest || latest === "loading" || latest === _("unknown")) { + showToast(_("Latest version is unknown"), "success"); + } else if (installed === "dev") { + showToast(getCheckToastMessage("dev"), "success"); + } else { + showToast( + getCheckToastMessage(installed === latest ? "latest" : "outdated"), + "success" + ); + } + } catch (error) { + logger.error("[MANAGER]", "runNetshiftCheck failed", error); + showToast(_("Failed to execute!"), "error"); + } finally { + setActionLoading(button.loadingKey, false); + } +} +async function runSingBoxMutation(component, button) { + setActionLoading(button.loadingKey, true); + showToast( + _("Switching sing-box core, this may take a few minutes\u2026"), + "success" + ); + try { + const result = await NetShiftShellMethods.singBoxComponentAction( + button.backendAction === "install_stable" ? "install_stable" : "install_extended" + ); + if (result.success) { + const changed = _("Sing-box core changed, version:"); + showToast(`${changed} ${result.version || ""}`.trim(), "success"); + resetCheckResult(component); + await fetchSystemInfo2(); + } else { + logger.error("[MANAGER]", "runSingBoxMutation failed", result); + showToast(result.message || _("Failed to execute!"), "error"); + } + } catch (error) { + logger.error("[MANAGER]", "runSingBoxMutation failed", error); + showToast(_("Failed to execute!"), "error"); + } finally { + setActionLoading(button.loadingKey, false); + } +} +function reloadPageAfterSelfUpdate() { + window.setTimeout(() => { + window.location.reload(); + }, 1200); +} +async function runNetshiftSelfUpdate(button) { + setActionLoading(button.loadingKey, true); + showToast( + _("Updating NetShift, this may take a few minutes; the page will reload\u2026"), + "success", + 6e3 + ); + try { + const result = await NetShiftShellMethods.netshiftSelfUpdate(); + if (result.success) { + const updated = _("NetShift updated, version:"); + showToast(`${updated} ${result.version || ""}`.trim(), "success", 1200); + reloadPageAfterSelfUpdate(); + return; + } + logger.error("[MANAGER]", "runNetshiftSelfUpdate failed", result); + showToast(result.message || _("Failed to execute!"), "error"); + setActionLoading(button.loadingKey, false); + } catch (error) { + logger.error("[MANAGER]", "runNetshiftSelfUpdate failed", error); + showToast(_("Failed to execute!"), "error"); + setActionLoading(button.loadingKey, false); + } +} +function handleManagerAction(card, button) { + if (isAnyActionLoading()) { + return; + } + if (button.kind === "check_netshift") { + void runNetshiftCheck(button); + return; + } + if (button.kind === "check") { + void runSingBoxCheck2(card.key, button); + return; + } + if (button.kind === "self_update") { + void runNetshiftSelfUpdate(button); + return; + } + void runSingBoxMutation(card.key, button); +} +function renderComponentTag(card) { + if (!card.tag) { + return null; + } + return E( + "span", + { + class: [ + "pdk_manager-page__component__tag", + card.tag.kind === "success" ? "pdk_manager-page__component__tag--success" : "", + card.tag.kind === "warning" ? "pdk_manager-page__component__tag--warning" : "" + ].filter(Boolean).join(" ") + }, + card.tag.label + ); +} +function renderComponentCard(card) { + const managerActions = store.get().managerActions; + const anyActionLoading = isAnyActionLoading(); + const systemInfoLoading = isSystemInfoLoading(); + const tag = renderComponentTag(card); + const headerChildren = [ + E("b", { class: "pdk_manager-page__component__title" }, card.title) + ]; + if (tag) { + headerChildren.push( + E("div", { class: "pdk_manager-page__component__status" }, [tag]) + ); + } + return E("div", { class: "pdk_manager-page__component" }, [ + E("div", { class: "pdk_manager-page__component__header" }, headerChildren), + E("div", { class: "pdk_manager-page__component__version" }, [ + E( + "span", + { class: "pdk_manager-page__component__version__label" }, + _("Version") + ), + E( + "span", + { class: "pdk_manager-page__component__version__value" }, + card.version + ) + ]), + E( + "div", + { class: "pdk_manager-page__component__actions" }, + card.actions.map((action) => { + const loading = managerActions[action.loadingKey].loading; + return renderButton({ + text: action.text, + icon: action.kind === "check" || action.kind === "check_netshift" ? renderSearchIcon24 : renderRotateCcwIcon24, + loading, + disabled: systemInfoLoading || anyActionLoading && !loading, + onClick: () => handleManagerAction(card, action) + }); + }) + ) + ]); +} +function renderManagerComponents() { + const container = document.getElementById("pdk_manager-components"); + if (!container) { + return; + } + const { diagnosticsSystemInfo, managerChecks } = store.get(); + const renderedComponents = getComponentCards( + { + netshift_version: normalizeCompiledVersion( + diagnosticsSystemInfo.netshift_version + ), + netshift_latest_version: diagnosticsSystemInfo.netshift_latest_version, + sing_box_version: diagnosticsSystemInfo.sing_box_version, + sing_box_extended: diagnosticsSystemInfo.sing_box_extended + }, + managerChecks + ).map(renderComponentCard); + return preserveScrollForPage(() => { + container.replaceChildren(...renderedComponents); + }); +} +function onStoreUpdate3(_next, _prev, diff) { + if (diff.diagnosticsSystemInfo || diff.managerActions || diff.managerChecks) { + renderManagerComponents(); + } +} +function onPageMount3() { + onPageUnmount3(); + managerMounted = true; + store.subscribe(onStoreUpdate3); + renderManagerComponents(); + void fetchSystemInfo2(); +} +function onPageUnmount3() { + managerMounted = false; + store.unsubscribe(onStoreUpdate3); + store.reset(["managerActions", "managerChecks"]); +} +function registerLifecycleListeners3() { + if (managerLifecycleRegistered) { + return; + } + managerLifecycleRegistered = true; + store.subscribe((next, prev, diff) => { + if (diff.tabService && next.tabService.current !== prev.tabService.current) { + const isManagerVisible = next.tabService.current === "manager"; + if (isManagerVisible) { + return onPageMount3(); + } + if (managerMounted) { + return onPageUnmount3(); + } + } + }); +} +async function initController3() { + if (managerControllerInitialized) { + return; + } + managerControllerInitialized = true; + onMount("manager-status").then(() => { + logger.debug("[MANAGER]", "initController", "onMount"); + registerLifecycleListeners3(); + if (store.get().tabService.current === "manager") { + onPageMount3(); + } + }); +} + +// src/netshift/tabs/manager/styles.ts +var styles5 = ` +#cbi-netshift-manager-_mount_node > div { + width: 100%; +} + +#cbi-netshift-manager > h3 { + display: none; +} + +.pdk_manager-page { + width: 100%; +} + +.pdk_manager-page__components { + display: grid; + grid-template-columns: repeat(2, minmax(240px, 1fr)); + grid-gap: 10px; +} + +@media (max-width: 760px) { + .pdk_manager-page__components { + grid-template-columns: 1fr; + } +} + +.pdk_manager-page__component { + border: 2px var(--background-color-low, lightgray) solid; + border-radius: 4px; + padding: 10px; + min-width: 0; + display: grid; + grid-template-columns: 1fr; + grid-row-gap: 10px; +} + +.pdk_manager-page__component__header { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, auto); + align-items: start; + gap: 8px; + min-width: 0; +} + +.pdk_manager-page__component__title { + color: var(--text-color-high); + line-height: 1.25; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pdk_manager-page__component__status { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + min-width: 0; + max-width: 180px; + overflow: hidden; +} + +.pdk_manager-page__component__version { + display: grid; + grid-template-columns: auto 1fr; + grid-column-gap: 6px; + align-items: baseline; + min-width: 0; +} + +.pdk_manager-page__component__version__label { + color: var(--text-color-medium); +} + +.pdk_manager-page__component__version__value { + min-width: 0; + overflow-wrap: anywhere; +} + +.pdk_manager-page__component__tag { + flex: 0 0 auto; + padding: 2px 5px; + border: 1px var(--background-color-high, gray) solid; + border-radius: 4px; + color: var(--text-color-medium, gray); + line-height: 1.2; +} + +.pdk_manager-page__component__tag--success { + border-color: var(--success-color-medium, green); + color: var(--success-color-medium, green); +} + +.pdk_manager-page__component__tag--warning { + border-color: var(--warn-color-medium, orange); + color: var(--warn-color-medium, orange); +} + +.pdk_manager-page__component__actions { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.pdk_manager-page__component__actions > .pdk-partial-button { + margin-left: 0; +} +`; + +// src/netshift/tabs/manager/index.ts +var ManagerTab = { + render: render3, + initController: initController3, + styles: styles5 +}; + // src/styles.ts var GlobalStyles = ` ${DashboardTab.styles} ${DiagnosticTab.styles} +${ManagerTab.styles} ${PartialStyles} @@ -5314,6 +5954,7 @@ return baseclass.extend({ FETCH_TIMEOUT, IP_CHECK_DOMAIN, Logger, + ManagerTab, NETSHIFT_LUCI_APP_VERSION, NetShiftShellMethods, REGIONAL_OPTIONS, diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/manager.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/manager.js new file mode 100644 index 00000000..5d7617c9 --- /dev/null +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/manager.js @@ -0,0 +1,22 @@ +"use strict"; +"require baseclass"; +"require form"; +"require ui"; +"require uci"; +"require fs"; +"require view.netshift.main as main"; + +function createManagerContent(section) { + const o = section.option(form.DummyValue, "_mount_node"); + o.rawhtml = true; + o.cfgvalue = () => { + main.ManagerTab.initController(); + return main.ManagerTab.render(); + }; +} + +const EntryPoint = { + createManagerContent, +}; + +return baseclass.extend(EntryPoint); diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js index 4893143d..ca791b7f 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js @@ -17,6 +17,9 @@ // Diagnostic content "require view.netshift.diagnostic as diagnostic"; +// Component Manager content +"require view.netshift.manager as manager"; + const EntryPoint = { async render() { main.injectGlobalStyles(); @@ -73,6 +76,21 @@ const EntryPoint = { // Render diagnostic content diagnostic.createDiagnosticContent(diagnosticSection); + // Component Manager tab + const managerSection = netshiftMap.section( + form.TypedSection, + "manager", + _("Component Manager"), + ); + managerSection.anonymous = true; + managerSection.addremove = false; + managerSection.cfgsections = function () { + return ["manager"]; + }; + + // Render Component Manager content + manager.createManagerContent(managerSection); + // Dashboard tab const dashboardSection = netshiftMap.section( form.TypedSection, diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 6d00cfb3..4ded1a6e 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 18:38+0300\n" -"PO-Revision-Date: 2026-06-06 18:38+0300\n" +"POT-Creation-Date: 2026-06-06 22:26+0300\n" +"PO-Revision-Date: 2026-06-06 22:26+0300\n" "Last-Translator: spgsroot, yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -80,6 +80,9 @@ msgstr "Путь к файлу кэша не может быть пустым" msgid "Cannot receive checks result" msgstr "Не удалось получить результаты проверки" +msgid "Check update" +msgstr "Проверить обновление" + msgid "Checking, please wait" msgstr "Проверяем, пожалуйста подождите" @@ -101,6 +104,9 @@ msgstr "Закрыть" msgid "Community Lists" msgstr "Списки сообщества" +msgid "Component Manager" +msgstr "Менеджер компонентов" + msgid "Config File Path" msgstr "Путь к файлу конфигурации" @@ -140,6 +146,9 @@ msgstr "Задержка в миллисекундах перед перезаг msgid "Delay value cannot be empty" msgstr "Значение задержки не может быть пустым" +msgid "Dev" +msgstr "Dev" + msgid "DHCP has DNS server" msgstr "DHCP содержит DNS сервер" @@ -326,11 +335,11 @@ msgstr "Ошибка HTTP" msgid "Include servers by keyword" msgstr "Включать серверы по ключевому слову" -msgid "Install extended" -msgstr "Установить extended" +msgid "Install %s" +msgstr "Установить %s" -msgid "Install stable" -msgstr "Установить stable" +msgid "Installed version is newer than release" +msgstr "Установленная версия новее релиза" msgid "Interface Monitoring" msgstr "Мониторинг интерфейса" @@ -509,6 +518,12 @@ msgstr "Оставлять только серверы подписки, имя msgid "Latest" msgstr "Последняя" +msgid "Latest version is installed" +msgstr "Установлена последняя версия" + +msgid "Latest version is unknown" +msgstr "Последняя версия неизвестна" + msgid "List Update Frequency" msgstr "Частота обновления списков" @@ -545,6 +560,9 @@ msgstr "NetShift" msgid "NetShift Settings" msgstr "Настройки NetShift" +msgid "NetShift updated, version:" +msgstr "NetShift обновлён, версия:" + msgid "NetShift will not modify your DHCP configuration" msgstr "NetShift не будет изменять вашу конфигурацию DHCP" @@ -557,6 +575,9 @@ msgstr "Другие правила маркировки не найдены" msgid "Not implement yet" msgstr "Ещё не реализовано" +msgid "Not installed" +msgstr "Не установлено" + msgid "Not responding" msgstr "Не отвечает" @@ -722,6 +743,9 @@ msgstr "Selector" msgid "Selector Proxy Links" msgstr "Ссылки прокси для Selector" +msgid "Self-update failed" +msgstr "Не удалось обновить" + msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." msgstr "Отправлять запросы к основному DNS через outbound прокси/VPN вместо прямого подключения. Bootstrap DNS всегда остаётся прямым." @@ -794,6 +818,12 @@ msgstr "URL подписки" msgid "Successfully copied!" msgstr "Успешно скопировано!" +msgid "Switch to extended" +msgstr "Переключить на extended" + +msgid "Switch to stable" +msgstr "Переключить на stable" + msgid "Switching sing-box core, this may take a few minutes…" msgstr "Переключение ядра sing-box, это может занять несколько минут…" @@ -857,6 +887,18 @@ msgstr "неизвестно" msgid "Unknown error" msgstr "Неизвестная ошибка" +msgid "Update" +msgstr "Обновить" + +msgid "Update is available" +msgstr "Доступно обновление" + +msgid "Update NetShift" +msgstr "Обновить NetShift" + +msgid "Updating NetShift, this may take a few minutes; the page will reload…" +msgstr "Обновление NetShift, это может занять несколько минут; страница перезагрузится…" + msgid "Uplink" msgstr "Исходящий" @@ -911,6 +953,9 @@ msgstr "Валидно" msgid "Validation errors:" msgstr "Ошибки валидации:" +msgid "Version" +msgstr "Версия" + msgid "View logs" msgstr "Посмотреть логи" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index 5e6e1464..f4032464 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 15:38+0300\n" -"PO-Revision-Date: 2026-06-06 15:38+0300\n" +"POT-Creation-Date: 2026-06-06 19:26+0300\n" +"PO-Revision-Date: 2026-06-06 19:26+0300\n" "Last-Translator: spgsroot, yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -60,7 +60,7 @@ msgstr "" msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:47 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:43 msgid "Available actions" msgstr "" @@ -103,6 +103,12 @@ msgstr "" msgid "Cannot receive checks result" msgstr "" +#: src/netshift/tabs/manager/cards.ts:143 +#: src/netshift/tabs/manager/cards.ts:179 +#: src/netshift/tabs/manager/cards.ts:225 +msgid "Check update" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:15 #: src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:15 #: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:13 @@ -135,11 +141,15 @@ msgstr "" msgid "Community Lists" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:83 +msgid "Component Manager" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:386 msgid "Config File Path" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:27 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:30 msgid "Configuration for NetShift service" msgstr "" @@ -159,7 +169,7 @@ msgstr "" msgid "Copy" msgstr "" -#: src/netshift/methods/shell/index.ts:157 +#: src/netshift/methods/shell/index.ts:159 #: src/netshift/methods/shell/pollSingBoxComponentAction.ts:65 msgid "Core switch failed" msgstr "" @@ -172,7 +182,7 @@ msgstr "" msgid "Currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:80 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:98 msgid "Dashboard" msgstr "" @@ -188,15 +198,19 @@ msgstr "" msgid "Delay value cannot be empty" msgstr "" +#: src/netshift/tabs/manager/cards.ts:101 +msgid "Dev" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:93 msgid "DHCP has DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:65 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:68 msgid "Diagnostics" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:83 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:79 msgid "Disable autostart" msgstr "" @@ -292,7 +306,7 @@ msgstr "" msgid "Dynamic List" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:93 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:89 msgid "Enable autostart" msgstr "" @@ -414,8 +428,13 @@ msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:267 #: src/netshift/tabs/diagnostic/initController.ts:304 #: src/netshift/tabs/diagnostic/initController.ts:308 -#: src/netshift/tabs/diagnostic/initController.ts:347 -#: src/netshift/tabs/diagnostic/initController.ts:351 +#: src/netshift/tabs/manager/initController.ts:122 +#: src/netshift/tabs/manager/initController.ts:132 +#: src/netshift/tabs/manager/initController.ts:166 +#: src/netshift/tabs/manager/initController.ts:197 +#: src/netshift/tabs/manager/initController.ts:201 +#: src/netshift/tabs/manager/initController.ts:234 +#: src/netshift/tabs/manager/initController.ts:238 msgid "Failed to execute!" msgstr "" @@ -430,7 +449,7 @@ msgstr "" msgid "Fully Routed IPs" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:102 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:98 msgid "Get global check" msgstr "" @@ -454,12 +473,14 @@ msgstr "" msgid "Include servers by keyword" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129 -msgid "Install extended" +#: src/netshift/tabs/manager/cards.ts:135 +#: src/netshift/tabs/manager/cards.ts:172 +#: src/netshift/tabs/manager/cards.ts:218 +msgid "Install %s" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:129 -msgid "Install stable" +#: src/netshift/tabs/manager/initController.ts:96 +msgid "Installed version is newer than release" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:232 @@ -697,10 +718,19 @@ msgstr "" msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" +#: src/netshift/tabs/manager/cards.ts:90 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:48 msgid "Latest" msgstr "" +#: src/netshift/tabs/manager/initController.ts:103 +msgid "Latest version is installed" +msgstr "" + +#: src/netshift/tabs/manager/initController.ts:155 +msgid "Latest version is unknown" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:323 msgid "List Update Frequency" msgstr "" @@ -745,10 +775,14 @@ msgstr "" msgid "NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:26 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:29 msgid "NetShift Settings" msgstr "" +#: src/netshift/tabs/manager/initController.ts:226 +msgid "NetShift updated, version:" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:378 msgid "NetShift will not modify your DHCP configuration" msgstr "" @@ -765,17 +799,24 @@ msgstr "" msgid "Not implement yet" msgstr "" +#: src/netshift/tabs/manager/cards.ts:98 +#: src/netshift/tabs/manager/cards.ts:196 +#: src/netshift/tabs/manager/cards.ts:241 +#: src/netshift/tabs/manager/initController.ts:100 +msgid "Not installed" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:74 #: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:80 #: src/netshift/tabs/diagnostic/checks/runSectionsCheck.ts:99 msgid "Not responding" msgstr "" -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:59 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:67 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:75 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:83 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:91 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:56 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:64 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:72 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:80 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:88 msgid "Not running" msgstr "" @@ -799,6 +840,7 @@ msgstr "" msgid "Outbound Configuration" msgstr "" +#: src/netshift/tabs/manager/cards.ts:94 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:38 msgid "Outdated" msgstr "" @@ -823,11 +865,11 @@ msgstr "" msgid "Path must end with cache.db" msgstr "" -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:107 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:115 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:123 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:131 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:139 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:104 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:112 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:120 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:128 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:136 msgid "Pending" msgstr "" @@ -859,7 +901,7 @@ msgstr "" msgid "Resolve real IP for routing" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:53 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:49 msgid "Restart NetShift" msgstr "" @@ -919,7 +961,7 @@ msgstr "" msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:36 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:39 msgid "Sections" msgstr "" @@ -996,6 +1038,10 @@ msgstr "" msgid "Selector Proxy Links" msgstr "" +#: src/netshift/methods/shell/index.ts:230 +msgid "Self-update failed" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:69 msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." msgstr "" @@ -1004,12 +1050,12 @@ msgstr "" msgid "Services info" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:49 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:52 msgid "Settings" msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:292 -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:120 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:116 msgid "Show sing-box config" msgstr "" @@ -1021,7 +1067,7 @@ msgstr "" msgid "Sing-box autostart disabled" msgstr "" -#: src/netshift/tabs/diagnostic/initController.ts:342 +#: src/netshift/tabs/manager/initController.ts:190 msgid "Sing-box core changed, version:" msgstr "" @@ -1070,11 +1116,11 @@ msgstr "" msgid "Specify the path to the list file located on the router filesystem" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:73 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:69 msgid "Start NetShift" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:63 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:59 msgid "Stop NetShift" msgstr "" @@ -1094,7 +1140,15 @@ msgstr "" msgid "Successfully copied!" msgstr "" -#: src/netshift/tabs/diagnostic/initController.ts:331 +#: src/netshift/tabs/manager/cards.ts:233 +msgid "Switch to extended" +msgstr "" + +#: src/netshift/tabs/manager/cards.ts:188 +msgid "Switch to stable" +msgstr "" + +#: src/netshift/tabs/manager/initController.ts:178 msgid "Switching sing-box core, this may take a few minutes…" msgstr "" @@ -1178,6 +1232,14 @@ msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:42 #: src/netshift/tabs/diagnostic/initController.ts:43 #: src/netshift/tabs/diagnostic/initController.ts:44 +#: src/netshift/tabs/manager/cards.ts:113 +#: src/netshift/tabs/manager/initController.ts:37 +#: src/netshift/tabs/manager/initController.ts:38 +#: src/netshift/tabs/manager/initController.ts:39 +#: src/netshift/tabs/manager/initController.ts:40 +#: src/netshift/tabs/manager/initController.ts:41 +#: src/netshift/tabs/manager/initController.ts:42 +#: src/netshift/tabs/manager/initController.ts:154 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7 msgid "unknown" msgstr "" @@ -1186,6 +1248,23 @@ msgstr "" msgid "Unknown error" msgstr "" +#: src/netshift/tabs/manager/cards.ts:172 +#: src/netshift/tabs/manager/cards.ts:218 +msgid "Update" +msgstr "" + +#: src/netshift/tabs/manager/initController.ts:92 +msgid "Update is available" +msgstr "" + +#: src/netshift/tabs/manager/cards.ts:136 +msgid "Update NetShift" +msgstr "" + +#: src/netshift/tabs/manager/initController.ts:217 +msgid "Updating NetShift, this may take a few minutes; the page will reload…" +msgstr "" + #: src/netshift/tabs/dashboard/initController.ts:240 #: src/netshift/tabs/dashboard/initController.ts:271 msgid "Uplink" @@ -1278,8 +1357,12 @@ msgstr "" msgid "Validation errors:" msgstr "" +#: src/netshift/tabs/manager/initController.ts:315 +msgid "Version" +msgstr "" + #: src/netshift/tabs/diagnostic/initController.ts:258 -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:111 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:107 msgid "View logs" msgstr "" diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 651c8844..a1cacb48 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -58,6 +58,21 @@ UPDATES_RESOLV_BACKUP="/tmp/netshift-resolv.conf.bak" # locations; tests override them. UPDATES_SING_BOX_BIN="/usr/bin/sing-box" UPDATES_LIBCRONET_LIB="/usr/lib/libcronet.so" +# Component Manager — NetShift self-update (task-017). The GitHub latest-release +# API for NetShift itself (same endpoint install.sh and get_system_info use); +# the self-update worker downloads the release .ipk/.apk assets from it. +NETSHIFT_RELEASE_API_URL="https://api.github.com/repos/yandexru45/netshift/releases/latest" +# tmpfs scratch dir for the self-update download (release packages) — RAM, never +# the tiny overlay; reaped on success and on reboot. +UPDATES_NETSHIFT_DOWNLOAD_DIR="/tmp/netshift/selfupdate" +# tmpfs backup of /etc/config/netshift taken before the self-update package +# install (conffiles normally preserve it; this is the defensive belt). +UPDATES_NETSHIFT_CONFIG_BACKUP="/tmp/netshift/config.bak" +# NetShift package names handled by the self-update (in install order). The RU +# i18n package is upgraded ONLY if already installed (never newly installed). +UPDATES_NETSHIFT_PKG_CORE="netshift" +UPDATES_NETSHIFT_PKG_LUCI="luci-app-netshift" +UPDATES_NETSHIFT_PKG_I18N_RU="luci-i18n-netshift-ru" # DNS SB_DNS_SERVER_TAG="dns-server" SB_FAKEIP_DNS_SERVER_TAG="fakeip-server" diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 903e1907..66b6afcf 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -1268,6 +1268,332 @@ updates_check_sing_box_extended() { return 0 } +# ── Package-manager abstraction (Component Manager, task-017) ─────── +# +# updater.sh does NOT source install.sh, so these are the tiny `updates_`-prefixed +# equivalents of install.sh's pkg_is_apk / pkg_install / pkg_is_installed. On a +# real device exactly ONE of apk/opkg exists. Package output is parsed with +# cut/awk/grep only — NEVER Oniguruma jq. + +# Returns 0 if the device uses apk (the apk binary is present), non-zero for opkg. +updates_pkg_is_apk() { + command -v apk >/dev/null 2>&1 +} + +# Installs a package FILE (downloaded .ipk/.apk) non-interactively. Returns the +# package manager's exit status. apk needs --allow-untrusted for self-built +# packages; opkg install handles the local file path directly. +updates_pkg_install_file() { + local pkg_file="$1" + + if updates_pkg_is_apk; then + apk add --allow-untrusted "$pkg_file" </dev/null >/dev/null 2>&1 + else + opkg install "$pkg_file" </dev/null >/dev/null 2>&1 + fi +} + +# Returns 0 if a package NAME is currently installed. Mirrors install.sh's +# pkg_is_installed grep-based detection (busybox-safe; no regex needed). +updates_pkg_is_installed() { + local pkg_name="$1" + + if updates_pkg_is_apk; then + apk list --installed 2>/dev/null | grep -q "$pkg_name" + else + opkg list-installed 2>/dev/null | grep -q "$pkg_name" + fi +} + +# Echoes the FEED/candidate version of a package (the version the package +# manager would install), or nothing if unavailable. Parsed with cut/awk only. +# opkg list <pkg> -> "<name> - <version>" (field after " - ") +# apk list <pkg> -> "<name>-<version> <arch> {...} ..." (strip "<name>-") +updates_pkg_candidate_version() { + local pkg_name="$1" + local line version="" + + if updates_pkg_is_apk; then + # First matching list line; the token is "<name>-<version>". Strip the + # leading "<pkg>-" so only the version (e.g. "1.12.22-r1") remains. + line="$(apk list "$pkg_name" 2>/dev/null | grep -v '\[installed\]' | awk '{print $1}' | head -n1)" + [ -n "$line" ] || line="$(apk list "$pkg_name" 2>/dev/null | awk '{print $1}' | head -n1)" + case "$line" in + "$pkg_name"-*) version="${line#"$pkg_name"-}" ;; + esac + else + # opkg list prints "<name> - <version>"; take the field after " - ". + version="$(opkg list "$pkg_name" 2>/dev/null | grep "^${pkg_name} " | head -n1 | awk -F' - ' '{print $2}')" + fi + + printf '%s' "$version" +} + +# Checks whether a newer STOCK (stable) sing-box is available via the system +# package manager. SYNC (quick call → stays on the synchronous component_action +# path). Graceful on an unreachable feed / parse failure: echoes +# {"success":false,"message":"..."} and returns non-zero. NEVER exits. +# +# Output (STABLE, mirrors updates_check_sing_box_extended): +# {"success":true,"current_version":"...","latest_version":"...", +# "status":"latest"|"outdated"|"not_installed"} +updates_check_sing_box_stable() { + local current_version candidate cur_semver cand_semver status + + # Refresh the package index so the candidate version reflects the feed. + # Best-effort: a failure here just means we compare against whatever index + # is cached; the candidate-empty branch below reports the unreachable feed. + if updates_pkg_is_apk; then + apk update </dev/null >/dev/null 2>&1 || true + else + opkg update </dev/null >/dev/null 2>&1 || true + fi + + candidate="$(updates_pkg_candidate_version "sing-box")" + if [ -z "$candidate" ]; then + echo "{\"success\":false,\"message\":\"Could not determine the stock sing-box version from the package feed (feed unreachable or package not found)\"}" + return 1 + fi + + # sing-box absent → not_installed (no running binary to compare). + if ! command -v sing-box >/dev/null 2>&1; then + echo "{\"success\":true,\"current_version\":\"not installed\",\"latest_version\":\"$candidate\",\"status\":\"not_installed\"}" + return 0 + fi + + current_version="$(get_sing_box_version)" + + # Compare on the leading semver only (drop any "-r1"/"-extended-..." suffix) + # so the sort -V based >= test in is_min_package_version is well-defined. + cur_semver="${current_version%%-*}" + cand_semver="${candidate%%-*}" + + if is_min_package_version "$cur_semver" "$cand_semver"; then + status="latest" + else + status="outdated" + fi + + echo "{\"success\":true,\"current_version\":\"$current_version\",\"latest_version\":\"$candidate\",\"status\":\"$status\"}" + return 0 +} + +# ── NetShift self-update (Component Manager, task-017) ────────────── +# +# Variant A: a targeted package upgrade (download the release .ipk/.apk from +# GitHub and pkg_install them) — NOT install.sh (interactive). Runs as the async +# worker `component_action netshift self_update`. +# +# Public wrapper — EXACTLY mirrors the updates_install_sing_box_extended epilogue +# (single cleanup path): reset heal flags → ensure GitHub connectivity (preflight +# + self-heal) → run the private core capturing JSON to a tmpfs file + rc → +# ALWAYS updates_restore_after_swap → re-emit the JSON → return rc. No early +# return skips the restore; no trap needed. +updates_self_update_netshift() { + local rc out json + + UPDATES_HEAL_RESOLV_REPLACED=0 + UPDATES_HEAL_REDIRECT_DOWN=0 + + if ! updates_ensure_connectivity "extended"; then + # Heal failed BEFORE anything was touched: NetShift is left fully intact. + updates_restore_after_swap + updates_log "Aborting NetShift self-update: GitHub unreachable and self-heal failed (NetShift left intact)" "error" + echo '{"success":false,"message":"GitHub unreachable and self-heal failed; self-update aborted (NetShift left intact)"}' + return 1 + fi + + out="/tmp/netshift-selfupdate-result.$$" + _updates_self_update_netshift_core >"$out" 2>/dev/null + rc=$? + json="$(cat "$out" 2>/dev/null)" + rm -f "$out" 2>/dev/null + + updates_restore_after_swap + + [ -n "$json" ] && printf '%s\n' "$json" + return "$rc" +} + +# Echoes the GitHub latest-release tag for NetShift (e.g. "v0.8.1"), or nothing. +# Reuses the same API endpoint as get_system_info / install.sh; parsed with +# grep/cut (the tag is needed only as a display/compare string, no jq array). +updates_netshift_latest_tag() { + local response + + response="$(updates_http_get_once "$NETSHIFT_RELEASE_API_URL" "")" + if [ -z "$response" ]; then + return 1 + fi + printf '%s' "$response" | grep '"tag_name":' | head -n1 | cut -d'"' -f4 +} + +# Downloads the NetShift release assets matching the package-name prefixes for +# the active package manager into $dir. Echoes nothing; returns 0 if at least +# the core "netshift" package was downloaded, non-zero otherwise. The asset URL +# list comes from the same latest-release JSON, filtered to .ipk or .apk by the +# package manager (busybox grep -o, no jq array walk required). +_updates_self_update_download_assets() { + local dir="$1" + local response ext pattern url filename dest attempt got_core=0 + + response="$(updates_http_get_once "$NETSHIFT_RELEASE_API_URL" "")" + if [ -z "$response" ]; then + return 1 + fi + + if updates_pkg_is_apk; then + ext="apk" + else + ext="ipk" + fi + pattern="https://[^\"[:space:]]*\.${ext}" + + # Iterate the matching browser_download_url values. Only keep assets whose + # filename starts with one of the NetShift package-name prefixes; the RU + # i18n package is kept ONLY if already installed. + printf '%s' "$response" | grep -o "$pattern" | while read -r url; do + filename="$(basename "$url")" + case "$filename" in + "$UPDATES_NETSHIFT_PKG_CORE"* | "$UPDATES_NETSHIFT_PKG_LUCI"*) ;; + "$UPDATES_NETSHIFT_PKG_I18N_RU"*) + updates_pkg_is_installed "$UPDATES_NETSHIFT_PKG_I18N_RU" || continue + ;; + *) continue ;; + esac + + dest="$dir/$filename" + attempt=0 + while [ "$attempt" -lt 3 ]; do + if updates_download_to_file "$url" "$dest"; then + break + fi + rm -f "$dest" 2>/dev/null + attempt=$((attempt + 1)) + done + done + + # Verify the core package landed (the subshell-piped loop can't set a parent + # var, so re-check the directory contents here). + if ls "$dir/$UPDATES_NETSHIFT_PKG_CORE"* >/dev/null 2>&1; then + got_core=1 + fi + [ "$got_core" -eq 1 ] +} + +# Private core of the self-update. Runs NON-interactively; every variable local. +# Echoes a single JSON object; NEVER exits (returns non-zero on recoverable +# failure so the wrapper still runs the restore epilogue). +_updates_self_update_netshift_core() { + local installed latest pkg file_path candidate_file + local backup_made=0 + + installed="$NETSHIFT_VERSION" + + latest="$(updates_netshift_latest_tag)" + if [ -z "$latest" ]; then + updates_log "Self-update: could not determine the latest NetShift release tag" "error" + echo '{"success":false,"message":"Could not determine the latest NetShift release (GitHub API unreachable or rate-limited)"}' + return 1 + fi + + # Idempotent defense: compare ignoring a leading "v" so "v0.8.1" vs "0.8.1" + # still match (the UI also gates on the "outdated" status). + if [ "${installed#v}" = "${latest#v}" ]; then + updates_log "Self-update: NetShift already at the latest version ($installed)" + echo "{\"success\":true,\"message\":\"Already up to date\",\"version\":\"$installed\"}" + return 0 + fi + + # Minimal backup: /etc/config/netshift to tmpfs (conffiles normally preserve + # it; this is the defensive belt). + rm -rf "$UPDATES_NETSHIFT_DOWNLOAD_DIR" 2>/dev/null + if ! mkdir -p "$UPDATES_NETSHIFT_DOWNLOAD_DIR"; then + updates_log "Self-update: failed to create download directory" "error" + echo '{"success":false,"message":"Failed to create the self-update download directory"}' + return 1 + fi + mkdir -p "$(dirname "$UPDATES_NETSHIFT_CONFIG_BACKUP")" 2>/dev/null || true + if [ -f "$NETSHIFT_CONFIG" ]; then + if cp -p "$NETSHIFT_CONFIG" "$UPDATES_NETSHIFT_CONFIG_BACKUP" 2>/dev/null; then + backup_made=1 + else + updates_log "Self-update: failed to back up $NETSHIFT_CONFIG (continuing; conffiles preserve it)" "warn" + fi + fi + + # Download the release assets (.ipk/.apk) for this package manager. + updates_log "Self-update: downloading NetShift $latest release packages" + if ! _updates_self_update_download_assets "$UPDATES_NETSHIFT_DOWNLOAD_DIR"; then + rm -rf "$UPDATES_NETSHIFT_DOWNLOAD_DIR" 2>/dev/null + updates_log "Self-update: failed to download the NetShift release packages" "error" + echo '{"success":false,"message":"Failed to download the NetShift release packages (GitHub unreachable or no matching assets)"}' + return 1 + fi + + # Install core, then LuCI app, then RU i18n if applicable (already filtered + # to "installed-only" at download time). NON-interactive. The netshift + # package replaces /usr/bin/netshift (this very script) — busybox ash has + # already read the whole script into memory, so the in-flight worker and the + # subsequent updates_write_finished_job_state complete from memory. We MUST + # NOT re-exec /usr/bin/netshift after this install (no updates_restart that + # re-runs the CLI; only /etc/init.d/netshift restart, which spawns a fresh + # process that may safely load the new binary). + for pkg in "$UPDATES_NETSHIFT_PKG_CORE" "$UPDATES_NETSHIFT_PKG_LUCI" "$UPDATES_NETSHIFT_PKG_I18N_RU"; do + file_path="" + for candidate_file in "$UPDATES_NETSHIFT_DOWNLOAD_DIR/$pkg"*; do + if [ -f "$candidate_file" ]; then + file_path="$candidate_file" + break + fi + done + [ -n "$file_path" ] || continue + + updates_log "Self-update: installing $(basename "$file_path")" + if ! updates_pkg_install_file "$file_path"; then + # The core is the critical package; if it fails, surface the failure. + # conffiles preserve /etc/config/netshift; restore defensively below. + if [ "$pkg" = "$UPDATES_NETSHIFT_PKG_CORE" ]; then + _updates_self_update_restore_config "$backup_made" + rm -rf "$UPDATES_NETSHIFT_DOWNLOAD_DIR" 2>/dev/null + updates_log "Self-update: failed to install the NetShift core package" "error" + echo '{"success":false,"message":"Failed to install the NetShift core package; configuration preserved"}' + return 1 + fi + updates_log "Self-update: failed to install $pkg (non-critical; continuing)" "warn" + fi + done + + # Defensive: if the config got clobbered/emptied, restore from the backup. + _updates_self_update_restore_config "$backup_made" + + # Success cleanup: drop the download dir and the config backup. + rm -rf "$UPDATES_NETSHIFT_DOWNLOAD_DIR" 2>/dev/null + [ "$backup_made" -eq 1 ] && rm -f "$UPDATES_NETSHIFT_CONFIG_BACKUP" 2>/dev/null + + updates_log "Self-update: NetShift updated to $latest" + echo "{\"success\":true,\"version\":\"$latest\",\"message\":\"NetShift updated to $latest\"}" + return 0 +} + +# Restores /etc/config/netshift from the tmpfs backup IF the live file is missing +# or empty (conffiles normally keep it; this is the defensive belt). $1 = 1 when +# a backup was taken. +_updates_self_update_restore_config() { + local backup_made="$1" + + [ "$backup_made" -eq 1 ] || return 0 + [ -f "$UPDATES_NETSHIFT_CONFIG_BACKUP" ] || return 0 + + if [ ! -s "$NETSHIFT_CONFIG" ]; then + if cp -p "$UPDATES_NETSHIFT_CONFIG_BACKUP" "$NETSHIFT_CONFIG" 2>/dev/null; then + updates_log "Self-update: restored $NETSHIFT_CONFIG from backup (live file was missing/empty)" "warn" + else + updates_log "Self-update: FAILED to restore $NETSHIFT_CONFIG from backup" "error" + fi + fi +} + # Dispatcher for component-related actions. component_action() { local component="$1" @@ -1283,6 +1609,12 @@ component_action() { sing_box:check_update) updates_check_sing_box_extended ;; + sing_box:check_update_stable) + updates_check_sing_box_stable + ;; + netshift:self_update) + updates_self_update_netshift + ;; *) echo '{"success":false,"message":"Unknown component action"}' return 1 diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 5c29708f..9506ebe8 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -10,7 +10,7 @@ # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, # nftv6, diagnostics, subscription, rejected, jobstate, -# selfheal, dnsdetour, globalproxy +# selfheal, dnsdetour, globalproxy, stablecheck, selfupdate # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 4e14bd84..2e0e1817 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -2596,6 +2596,416 @@ test_global_proxy() { fi } +# ───────────────────────────────────────────────────────────────── +# Test: Stock sing-box update check (task-017) +# ───────────────────────────────────────────────────────────────── +# Exercises updates_check_sing_box_stable through the real sourced updater.sh +# with a PATH-prepended fake opkg whose candidate version + presence of sing-box +# are driven by env/marker files (the test_selfheal stub-harness pattern). Asserts +# the STABLE JSON `status` for: installed == candidate -> latest; candidate newer +# -> outdated; sing-box absent -> not_installed; feed unreachable -> success:false. +test_check_update_stable() { + header "Stock sing-box Update Check (task-017)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local updater="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$updater" ]; then + skip "updater.sh not found in ${NETSHIFT_LIB_DIR}" + return + fi + + local work="/tmp/netshift-stablecheck-$$" + rm -rf "$work" + mkdir -p "$work/bin" + + # Fake opkg: `update` always ok; `list sing-box` echoes the candidate line + # only when the candidate marker is set; a `sing-box` shim reports the + # running version only when the present marker is set. + cat > "$work/bin/opkg" << 'OPKGEOF' +#!/bin/sh +case "$1" in +update) exit 0 ;; +list) + if [ -n "$STUBCHECK_CANDIDATE" ]; then + printf 'sing-box - %s\n' "$STUBCHECK_CANDIDATE" + fi + exit 0 + ;; +esac +exit 0 +OPKGEOF + cat > "$work/bin/sing-box" << 'SBEOF' +#!/bin/sh +case "$1" in +version) printf 'sing-box version %s\n' "$STUBCHECK_INSTALLED" ;; +esac +exit 0 +SBEOF + chmod 0755 "$work/bin/opkg" "$work/bin/sing-box" + + # Isolated PATH: symlink only the utilities the updater/helpers need into a + # dedicated dir so the real /usr/bin/sing-box is NOT reachable. The fake + # sing-box is linked in conditionally per scenario (present vs absent). + mkdir -p "$work/path" + local _tool _tool_path + for _tool in sh ash cat grep awk cut head sort sed printf basename ls rm mkdir cp mv chmod env jq dirname; do + _tool_path="$(command -v "$_tool" 2>/dev/null)" && ln -sf "$_tool_path" "$work/path/$_tool" 2>/dev/null + done + ln -sf "$work/bin/opkg" "$work/path/opkg" + + # Driver: source updater.sh + helpers.sh (real is_min_package_version / + # get_sing_box_version), silence logging, run the check. + local drv="$work/driver.sh" + cat > "$drv" << 'DRVEOF' +log() { :; } +echolog() { :; } +nolog() { :; } +. "DRV_HELPERS" +. "DRV_UPDATER" +updates_check_sing_box_stable +DRVEOF + sed -i "s|DRV_UPDATER|$updater|g;s|DRV_HELPERS|${NETSHIFT_LIB_DIR}/helpers.sh|g" "$drv" + + local out="$work/out.json" + run_check() { + # Isolated PATH: only $work/path (no real sing-box, apk absent → opkg + # branch). sing-box presence is controlled by linking the fake in/out. + if [ -n "$STUBCHECK_PRESENT" ]; then + ln -sf "$work/bin/sing-box" "$work/path/sing-box" 2>/dev/null + else + rm -f "$work/path/sing-box" 2>/dev/null + fi + PATH="$work/path" ash "$drv" > "$out" 2>/dev/null || true + } + + # ── Case 1: installed == candidate → latest ────────────────────────────── + export STUBCHECK_CANDIDATE="1.12.0-r1" + export STUBCHECK_PRESENT=1 + export STUBCHECK_INSTALLED="1.12.0" + run_check + if jq -e '.success == true and .status == "latest"' "$out" > /dev/null 2>&1; then + pass "stablecheck-installed-eq-candidate-latest:OK" + else + fail "stablecheck-installed-eq-candidate-latest:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 2: candidate newer → outdated ─────────────────────────────────── + export STUBCHECK_CANDIDATE="1.13.5-r1" + export STUBCHECK_PRESENT=1 + export STUBCHECK_INSTALLED="1.12.0" + run_check + if jq -e '.success == true and .status == "outdated"' "$out" > /dev/null 2>&1; then + pass "stablecheck-candidate-newer-outdated:OK" + else + fail "stablecheck-candidate-newer-outdated:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 3: sing-box absent → not_installed ────────────────────────────── + export STUBCHECK_CANDIDATE="1.13.5-r1" + unset STUBCHECK_PRESENT + run_check + if jq -e '.success == true and .status == "not_installed"' "$out" > /dev/null 2>&1; then + pass "stablecheck-absent-not_installed:OK" + else + fail "stablecheck-absent-not_installed:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 4: feed unreachable (empty candidate) → success:false ─────────── + unset STUBCHECK_CANDIDATE + export STUBCHECK_PRESENT=1 + export STUBCHECK_INSTALLED="1.12.0" + run_check + if jq -e '.success == false and (.message | length) > 0' "$out" > /dev/null 2>&1; then + pass "stablecheck-feed-unreachable-successfalse:OK" + else + fail "stablecheck-feed-unreachable-successfalse:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + unset STUBCHECK_CANDIDATE STUBCHECK_PRESENT STUBCHECK_INSTALLED + rm -rf "$work" +} + +# ───────────────────────────────────────────────────────────────── +# Test: NetShift self-update (task-017) +# ───────────────────────────────────────────────────────────────── +# Exercises updates_self_update_netshift (public wrapper + private core) through +# the real sourced updater.sh. Connectivity probes (dig/curl) + the GitHub fetch +# + the asset download + the package install are all stubbed; the heal flags are +# re-pinned to temp paths and a fake /etc/init.d/netshift (absolute path) is +# written+restored. Asserts the anti-brick contract: +# * connectivity-fail -> aborts BEFORE any change, restore ran, success:false +# * download-fail -> success:false, restore ran, config backup intact +# * happy path -> success:true with version, restore ran +# Uses the task-009 `... || true` set -e guard (worker returns non-zero on a +# recoverable failure; assertions read JSON/file-state, not rc). +test_self_update_netshift() { + header "NetShift Self-Update (task-017)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local updater="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$updater" ]; then + skip "updater.sh not found in ${NETSHIFT_LIB_DIR}" + return + fi + + local work="/tmp/netshift-selfupdate-$$" + rm -rf "$work" + mkdir -p "$work/bin" "$work/init" + + # Connectivity probes (dig/curl), keyed off markers like test_selfheal. + cat > "$work/bin/dig" << 'DIGEOF' +#!/bin/sh +[ -f "$SU_DNS_OK" ] && { echo "1.2.3.4"; exit 0; } +exit 1 +DIGEOF + cat > "$work/bin/curl" << 'CURLEOF' +#!/bin/sh +[ -f "$SU_HTTP_OK" ] && exit 0 +exit 1 +CURLEOF + # Fake opkg: `install <file>` succeeds per marker and records the install. + cat > "$work/bin/opkg" << 'OPKGEOF' +#!/bin/sh +case "$1" in +update) exit 0 ;; +list-installed) cat "$SU_INSTALLED_LIST" 2>/dev/null; exit 0 ;; +install) + shift + printf '%s\n' "$1" >> "$SU_INSTALL_LOG" + [ -f "$SU_PKG_OK" ] && exit 0 + exit 1 + ;; +esac +exit 0 +OPKGEOF + chmod 0755 "$work/bin/dig" "$work/bin/curl" "$work/bin/opkg" + + # Fake /etc/init.d/netshift: records stop/start/restart (used by self-heal + # teardown/bring-up). We never re-exec /usr/bin/netshift here. + cat > "$work/init/netshift" << 'INITEOF' +#!/bin/sh +printf '%s\n' "$1" >> "$SU_INIT_LOG" +exit 0 +INITEOF + chmod 0755 "$work/init/netshift" + + # Driver: source updater.sh, re-pin heal/connectivity paths + constants, + # stub the GitHub fetch + download with markers, run the public wrapper. + local drv="$work/driver.sh" + cat > "$drv" << 'DRVEOF' +log() { :; } +echolog() { :; } +nolog() { :; } +updates_log() { :; } +RESOLV_CONF="DRV_RESOLV" +UPDATES_RESOLV_BACKUP="DRV_BACKUP" +UPDATES_FEED_PROBE_HOST="feeds.test" +UPDATES_GITHUB_PROBE_HOST="github.test" +UPDATES_HEAL_RESOLVERS="1.1.1.1 9.9.9.9" +NETSHIFT_VERSION="0.8.0" +NETSHIFT_CONFIG="DRV_CONFIG" +NETSHIFT_RELEASE_API_URL="https://api.test/latest" +UPDATES_NETSHIFT_DOWNLOAD_DIR="DRV_DLDIR" +UPDATES_NETSHIFT_CONFIG_BACKUP="DRV_CFGBAK" +UPDATES_NETSHIFT_PKG_CORE="netshift" +UPDATES_NETSHIFT_PKG_LUCI="luci-app-netshift" +UPDATES_NETSHIFT_PKG_I18N_RU="luci-i18n-netshift-ru" +. "DRV_UPDATER" +# Re-pin after sourcing. +RESOLV_CONF="DRV_RESOLV" +UPDATES_RESOLV_BACKUP="DRV_BACKUP" +UPDATES_FEED_PROBE_HOST="feeds.test" +UPDATES_GITHUB_PROBE_HOST="github.test" +UPDATES_HEAL_RESOLVERS="1.1.1.1 9.9.9.9" +NETSHIFT_VERSION="0.8.0" +NETSHIFT_CONFIG="DRV_CONFIG" +NETSHIFT_RELEASE_API_URL="https://api.test/latest" +UPDATES_NETSHIFT_DOWNLOAD_DIR="DRV_DLDIR" +UPDATES_NETSHIFT_CONFIG_BACKUP="DRV_CFGBAK" +UPDATES_NETSHIFT_PKG_CORE="netshift" +UPDATES_NETSHIFT_PKG_LUCI="luci-app-netshift" +UPDATES_NETSHIFT_PKG_I18N_RU="luci-i18n-netshift-ru" + +# Stub the GitHub latest-release fetch: emit a tiny JSON with a tag and asset +# URLs only when the marker says GitHub is reachable for the fetch. +updates_http_get_once() { + [ -f "$SU_GH_OK" ] || return 1 + cat <<JSON +{"tag_name":"$SU_LATEST_TAG", + "assets":[ + {"browser_download_url":"https://dl.test/netshift-$SU_LATEST_TAG.ipk"}, + {"browser_download_url":"https://dl.test/luci-app-netshift-$SU_LATEST_TAG.ipk"}, + {"browser_download_url":"https://dl.test/luci-i18n-netshift-ru-$SU_LATEST_TAG.ipk"} + ]} +JSON +} +# Stub the asset download: write a non-empty file only when the marker is set. +updates_download_to_file() { + [ -f "$SU_DL_OK" ] || return 1 + printf 'pkg-bytes\n' > "$2" + [ -s "$2" ] +} + +updates_self_update_netshift +DRVEOF + sed -i "s|DRV_UPDATER|$updater|g;s|DRV_RESOLV|$work/resolv.conf|g;s|DRV_BACKUP|$work/resolv.bak|g;s|DRV_CONFIG|$work/etc-config-netshift|g;s|DRV_DLDIR|$work/dl|g;s|DRV_CFGBAK|$work/config.bak|g" "$drv" + + # Install the fake /etc/init.d/netshift (write+restore the real one). + local init_target="/etc/init.d/netshift" + local init_saved="" + if [ -e "$init_target" ]; then + init_saved="$work/netshift.realinit" + cp -p "$init_target" "$init_saved" 2>/dev/null || init_saved="" + fi + mkdir -p /etc/init.d 2>/dev/null || true + cp -p "$work/init/netshift" "$init_target" 2>/dev/null + chmod 0755 "$init_target" 2>/dev/null || true + + export SU_DNS_OK="$work/dns_ok" + export SU_HTTP_OK="$work/http_ok" + export SU_GH_OK="$work/gh_ok" + export SU_DL_OK="$work/dl_ok" + export SU_PKG_OK="$work/pkg_ok" + export SU_INIT_LOG="$work/init.log" + export SU_INSTALL_LOG="$work/install.log" + export SU_INSTALLED_LIST="$work/installed.list" + export SU_LATEST_TAG="0.8.1" + + local out="$work/out.json" + run_scenario() { + rm -f "$work/init.log" "$work/install.log" + PATH="$work/bin:/usr/bin:/bin" ash "$drv" > "$out" 2>/dev/null || true + } + + # RU i18n NOT installed (so it is never downloaded/installed). + : > "$work/installed.list" + + # ── Scenario 1: connectivity fails → abort BEFORE any change ────────────── + rm -f "$SU_DNS_OK" "$SU_HTTP_OK" "$SU_GH_OK" "$SU_DL_OK" "$SU_PKG_OK" + printf 'CONFIG-ORIG\n' > "$work/etc-config-netshift" + printf 'original-resolver\n' > "$work/resolv.conf" + run_scenario + if jq -e '.success == false and (.message | length) > 0' "$out" > /dev/null 2>&1; then + pass "selfupdate-connfail-aborts-successfalse:OK" + else + fail "selfupdate-connfail-aborts-successfalse:FAIL" "$(cat "$out" 2>/dev/null)" + fi + # No install attempted (aborted before the core). + if [ ! -f "$work/install.log" ]; then + pass "selfupdate-connfail-no-install:OK" + else + fail "selfupdate-connfail-no-install:FAIL" "install.log=$(cat "$work/install.log" 2>/dev/null)" + fi + # Epilogue restored the original resolv.conf (heal may have replaced it). + if [ "$(cat "$work/resolv.conf" 2>/dev/null)" = "original-resolver" ]; then + pass "selfupdate-connfail-resolv-restored:OK" + else + fail "selfupdate-connfail-resolv-restored:FAIL" "$(cat "$work/resolv.conf" 2>/dev/null)" + fi + + # ── Scenario 2: download fails → success:false, config backup intact ────── + : > "$SU_DNS_OK"; : > "$SU_HTTP_OK"; : > "$SU_GH_OK" + rm -f "$SU_DL_OK" "$SU_PKG_OK" + printf 'CONFIG-ORIG\n' > "$work/etc-config-netshift" + printf 'original-resolver\n' > "$work/resolv.conf" + run_scenario + if jq -e '.success == false' "$out" > /dev/null 2>&1; then + pass "selfupdate-dlfail-successfalse:OK" + else + fail "selfupdate-dlfail-successfalse:FAIL" "$(cat "$out" 2>/dev/null)" + fi + # No package install ran (download failed first). + if [ ! -f "$work/install.log" ]; then + pass "selfupdate-dlfail-no-install:OK" + else + fail "selfupdate-dlfail-no-install:FAIL" "install.log=$(cat "$work/install.log" 2>/dev/null)" + fi + # /etc/config/netshift untouched (download failed before any install). + if [ "$(cat "$work/etc-config-netshift" 2>/dev/null)" = "CONFIG-ORIG" ]; then + pass "selfupdate-dlfail-config-intact:OK" + else + fail "selfupdate-dlfail-config-intact:FAIL" "$(cat "$work/etc-config-netshift" 2>/dev/null)" + fi + + # ── Scenario 3: happy path → success:true with version, restore ran ─────── + : > "$SU_DNS_OK"; : > "$SU_HTTP_OK"; : > "$SU_GH_OK"; : > "$SU_DL_OK"; : > "$SU_PKG_OK" + printf 'CONFIG-ORIG\n' > "$work/etc-config-netshift" + printf 'original-resolver\n' > "$work/resolv.conf" + run_scenario + if jq -e '.success == true and .version == "0.8.1"' "$out" > /dev/null 2>&1; then + pass "selfupdate-happy-successtrue-version:OK" + else + fail "selfupdate-happy-successtrue-version:FAIL" "$(cat "$out" 2>/dev/null)" + fi + # Core + LuCI installed; RU i18n NOT (not installed) → exactly 2 installs. + if [ -f "$work/install.log" ] && [ "$(grep -c . "$work/install.log" 2>/dev/null)" = "2" ] \ + && ! grep -q 'i18n' "$work/install.log" 2>/dev/null; then + pass "selfupdate-happy-core-luci-installed-no-ru:OK" + else + fail "selfupdate-happy-core-luci-installed-no-ru:FAIL" "install.log=$(cat "$work/install.log" 2>/dev/null)" + fi + # Connectivity was fine → no teardown → resolv.conf untouched original. + if [ "$(cat "$work/resolv.conf" 2>/dev/null)" = "original-resolver" ]; then + pass "selfupdate-happy-resolv-untouched:OK" + else + fail "selfupdate-happy-resolv-untouched:FAIL" "$(cat "$work/resolv.conf" 2>/dev/null)" + fi + # Success cleanup: the download dir is removed. + if [ ! -d "$work/dl" ]; then + pass "selfupdate-happy-download-dir-cleaned:OK" + else + fail "selfupdate-happy-download-dir-cleaned:FAIL" "dl dir remains" + fi + + # ── Scenario 4: already up to date (idempotent) → success:true, no install + : > "$SU_DNS_OK"; : > "$SU_HTTP_OK"; : > "$SU_GH_OK"; : > "$SU_DL_OK"; : > "$SU_PKG_OK" + printf 'CONFIG-ORIG\n' > "$work/etc-config-netshift" + export SU_LATEST_TAG="0.8.0" # equals the pinned NETSHIFT_VERSION + run_scenario + export SU_LATEST_TAG="0.8.1" + if jq -e '.success == true and (.message | contains("up to date"))' "$out" > /dev/null 2>&1; then + pass "selfupdate-already-current-idempotent:OK" + else + fail "selfupdate-already-current-idempotent:FAIL" "$(cat "$out" 2>/dev/null)" + fi + if [ ! -f "$work/install.log" ]; then + pass "selfupdate-already-current-no-install:OK" + else + fail "selfupdate-already-current-no-install:FAIL" "install.log=$(cat "$work/install.log" 2>/dev/null)" + fi + + # ── Scenario 5: RU i18n IS installed → it is upgraded too (3 installs) ───── + : > "$SU_DNS_OK"; : > "$SU_HTTP_OK"; : > "$SU_GH_OK"; : > "$SU_DL_OK"; : > "$SU_PKG_OK" + printf 'CONFIG-ORIG\n' > "$work/etc-config-netshift" + printf 'luci-i18n-netshift-ru - 0.8.0\n' > "$work/installed.list" + run_scenario + : > "$work/installed.list" + if [ -f "$work/install.log" ] && [ "$(grep -c . "$work/install.log" 2>/dev/null)" = "3" ] \ + && grep -q 'i18n' "$work/install.log" 2>/dev/null; then + pass "selfupdate-ru-installed-upgraded:OK" + else + fail "selfupdate-ru-installed-upgraded:FAIL" "install.log=$(cat "$work/install.log" 2>/dev/null)" + fi + + # ── Restore the real init script (if any) and clean up. ────────────────── + if [ -n "$init_saved" ] && [ -e "$init_saved" ]; then + cp -p "$init_saved" "$init_target" 2>/dev/null || true + else + rm -f "$init_target" 2>/dev/null || true + fi + unset SU_DNS_OK SU_HTTP_OK SU_GH_OK SU_DL_OK SU_PKG_OK SU_INIT_LOG \ + SU_INSTALL_LOG SU_INSTALLED_LIST SU_LATEST_TAG + rm -rf "$work" +} + # ───────────────────────────────────────────────────────────────── # Main # ───────────────────────────────────────────────────────────────── @@ -2626,6 +3036,8 @@ main() { test_selfheal test_dns_via_outbound test_global_proxy + test_check_update_stable + test_self_update_netshift ;; deps) test_deps ;; syntax) test_syntax ;; @@ -2640,12 +3052,14 @@ main() { selfheal) test_selfheal ;; dnsdetour) test_dns_via_outbound ;; globalproxy) test_global_proxy ;; + stablecheck) test_check_update_stable ;; + selfupdate) test_self_update_netshift ;; jq) test_jq_helpers ;; cm) test_config_manager ;; sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription rejected jobstate selfheal dnsdetour globalproxy" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription rejected jobstate selfheal dnsdetour globalproxy stablecheck selfupdate" exit 1 ;; esac From cbdc714ab8f5aa637b430dfc3e7cc77b6071e05d Mon Sep 17 00:00:00 2001 From: "spgsroot, yandexru45" <> Date: Sun, 7 Jun 2026 00:15:59 +0300 Subject: [PATCH 56/75] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D0=B8=20extended=20=D1=8F=D0=B4=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 12 +-- .../memory/luci-frontend-developer.md | 2 +- .../memory/shell-backend-developer.md | 40 +++++++- netshift/files/usr/lib/updater.sh | 24 ++++- tests/docker-compose.yml | 3 +- tests/entrypoint.sh | 99 ++++++++++++++++++- 6 files changed, 160 insertions(+), 20 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index fa6973ad..4f42ec05 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -104,8 +104,8 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> `sing-box version` fine WITHOUT LD_LIBRARY_PATH (libcronet only needed at runtime for naive); `chmod 0755` itself works under umask 0077. The code's chmod/validate is correct; it just never gets to run. -- FIX DIRECTION (matches podkop-plus): make core-switch ASYNCHRONOUS — podkop-plus - has `component_action_async` (writes output to a file, forks the work) + +- FIX DIRECTION: make core-switch ASYNCHRONOUS — has `component_action_async` + (writes output to a file, forks the work) + `component_action_status` (UI polls). NetShift's updater is synchronous and has no async/status path. Port that model: fork the install, return immediately, poll status; UI shows progress instead of hitting the 30s rpcd wall. @@ -360,14 +360,6 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> (NetShift / sing-box stock / sing-box extended) with installed version shown immediately + on-demand "Check update" + status badges + update/core-switch/ self-update actions. Core-switch MOVED out of Diagnostics into here. -- Reference impl = `podkop-plus/` (operator clones it to repo root when needed; - it is UNTRACKED and NOT gitignored — must NOT be added to a commit). Paths: - `podkop-plus/fe-app-podkop/src/podkop/tabs/updates/{index,render,initController, - styles}.ts` + `podkop-plus/luci-app-podkop-plus/htdocs/.../view/podkop/updates.js` - (note `luci-app-podkop-plus` dir + `view.podkop_plus.main`; OUR view requires - `view.netshift.main`). The card/styles pattern is the theme-CSS-vars-with- - fallback approach (var(--success-color-medium, green) etc) — safe for custom - LuCI themes. - Backend (task-017, updater.sh): two NEW component_action sub-cases (the dispatcher is component_action() :1272, a `case "$comp:$action"`; that is the ONLY extension point — component_action_async/_status are component-agnostic, diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 6e4ae473..8d369ce5 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -280,7 +280,7 @@ append findings; keep under ~200 lines. Dashboard/Diagnostic. Styles use theme vars WITH fallbacks; CBI selector is `#cbi-netshift-manager-_mount_node > div` + hide `#cbi-netshift-manager > h3`. - LIFECYCLE: mirror diagnostic exactly but guard re-init with module-level - `*Registered`/`*Initialized`/`*Mounted` booleans (podkop-plus style) since the + `*Registered`/`*Initialized`/`*Mounted` booleans since the lazy-mount listener can fire repeatedly. `onMount('manager-status')` → `registerLifecycleListeners()` (subscribe on `tabService.current==='manager'`) → `onPageMount` subscribes store + renders + fetches systemInfo; diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index fb31c967..b2395952 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -137,8 +137,7 @@ findings; keep under ~200 lines. the worker mid-extract (after `tar -O > /usr/bin/sing-box`, before `chmod 0755`). The JS-side `timeout: 600000` does NOT help (server-side limit). Fix = fork the worker detached; return a job_id in <<30s; poll status. -- Job-state machinery lives in `updater.sh` (jq, no ucode — podkop-plus uses - `json_utils_ucode` which we don't have). State dir `/var/run/netshift/ +- Job-state machinery lives in `updater.sh` (jq, no ucode). State dir `/var/run/netshift/ component-actions` (tmpfs). Constants: `UPDATES_JOB_DIR`, `UPDATES_JOB_FINISHED_TTL_MINUTES=60`, `UPDATES_JOB_ORPHAN_OUTPUT_TTL_MINUTES=60`, `UPDATES_JOB_STALE_GRACE_SECONDS=15`. @@ -547,3 +546,40 @@ findings; keep under ~200 lines. netshift` (absolute write+restore). Registered all 5 points (all)/case/usage/ compose). Used task-009 `... || true` set -e guard. shellcheck -S error clean; `smoke-tests all` = 101 passed / 0 failed (was 84 baseline; +17 new). + +## task-019: extended-check false "outdated" — v-prefix mismatch (Variant A) + +- Root cause: `updates_check_sing_box_extended` (updater.sh ~:1245) compared + installed `get_sing_box_version` (`1.13.12-extended-2.3.2`, NO v) against the + GitHub `.tag_name` (`v1.13.12-extended-2.3.2`, WITH v) via `case + "$current_version" in *"$tag"*)`. The `v` prefix means the substring never + matched → fell through to `outdated` for a user ALREADY on the latest. (Stock + check + self-update were already correct: stock candidates have no v; + `_updates_self_update_netshift_core` already does `${installed#v}` == + `${latest#v}`.) +- Fix (Variant A, ONLY this function): strip a single leading v off BOTH sides + (`cur_norm="${current_version#v}"; tag_norm="${tag#v}"`; `${x#v}` removes one + leading v if present, no-op otherwise), then EXACT-compare `[ "$cur_norm" = + "$tag_norm" ]` (the extended version is the full token — exact-after-v-strip is + correct and avoids the partial matches the old `case *"$tag"*` allowed). Emit + BOTH `current_version` and `latest_version` v-stripped so the UI shows a + consistent string. JSON shape/keys/order unchanged (STABLE for task-018); + `success:false` branches (fetch fail, no tag) untouched. New vars `local`, + POSIX ash, never exits. +- CRITICAL isolation: the install/asset path is a SEPARATE function + (`_updates_install_sing_box_extended_core`, ~:942-957) that re-derives its OWN + `tag` from `updates_extended_release_tag` (raw, WITH v) and feeds it to + `updates_extended_release_object` (`.tag_name == $t`) + `updates_extended_asset_url`. + In the check, `tag` (raw) is NO LONGER fed anywhere downstream — only `cur_norm`/ + `tag_norm`. Did NOT touch `_release_tag`/`_release_object`/`_asset_url`/wrappers. +- Smoke: NEW top-level `test_check_update_extended` (alias `extcheck`, 3 cases). + updater.sh is a sourceable lib, so the driver sources updater.sh + helpers.sh, + silences log/echolog/nolog, then OVERRIDES the 3 deps AFTER sourcing + (`get_sing_box_version`, `updates_fetch_sing_box_extended_releases`, + `updates_extended_release_tag`) reading marker env (`STUBEXT_INSTALLED/RELEASES/TAG`) + — simpler than awk-extract since it's not in bin/netshift. Cases: (1) installed + == latest, only tag has v → latest + both v-stripped+equal (THE regression); + (2) installed older → outdated; (3) empty releases → success:false. Registered + all 5 points (all)/case/usage/docker-compose comment). shellcheck -S error clean + (bin+libs+install.sh); `smoke-tests all` = 104 passed / 0 failed (101 baseline + + 3 new). `extcheck` alone = 3/0. diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 66b6afcf..812fa29e 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -1243,7 +1243,7 @@ updates_stable_rollback() { # Checks whether a newer sing-box-extended release is available. # Echoes a JSON status (latest|outdated) on stdout. updates_check_sing_box_extended() { - local current_version releases tag status + local current_version releases tag status cur_norm tag_norm current_version="$(get_sing_box_version)" @@ -1259,12 +1259,26 @@ updates_check_sing_box_extended() { return 1 fi + # Normalize a single leading "v" off BOTH sides before comparing/emitting. + # get_sing_box_version yields "1.13.12-extended-2.3.2" (no v) while the + # GitHub .tag_name is "v1.13.12-extended-2.3.2" (with v), so the old + # substring match never fired and reported a false "outdated". ${x#v} strips + # exactly one leading "v" if present and leaves the string otherwise — safe + # for both forms. NB: `tag` itself (with v) is untouched and is NOT used by + # the install/asset path here; the installer re-derives its own tag. + cur_norm="${current_version#v}" + tag_norm="${tag#v}" + + # EXACT equality after the v-strip: the extended version string is the full + # token (e.g. "1.13.12-extended-2.3.2"), so an exact match is correct and + # avoids the accidental partial matches the old `case *"$tag"*` form allowed. status="outdated" - case "$current_version" in - *"$tag"*) status="latest" ;; - esac + if [ "$cur_norm" = "$tag_norm" ]; then + status="latest" + fi - echo "{\"success\":true,\"current_version\":\"$current_version\",\"latest_version\":\"$tag\",\"status\":\"$status\"}" + # Emit BOTH versions v-stripped so the UI shows a consistent string. + echo "{\"success\":true,\"current_version\":\"$cur_norm\",\"latest_version\":\"$tag_norm\",\"status\":\"$status\"}" return 0 } diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 9506ebe8..135dbe6f 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -10,7 +10,8 @@ # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, # nftv6, diagnostics, subscription, rejected, jobstate, -# selfheal, dnsdetour, globalproxy, stablecheck, selfupdate +# selfheal, dnsdetour, globalproxy, stablecheck, extcheck, +# selfupdate # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 2e0e1817..ac41f9cc 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -2729,6 +2729,101 @@ DRVEOF rm -rf "$work" } +# ───────────────────────────────────────────────────────────────── +# Test: Extended sing-box Update Check — v-prefix regression (task-019) +# ───────────────────────────────────────────────────────────────── +# Sources the REAL updates_check_sing_box_extended from updater.sh and stubs its +# three dependencies (get_sing_box_version, updates_fetch_sing_box_extended_releases, +# updates_extended_release_tag) via markers so the comparison + emitted JSON can +# be driven deterministically. The regression: installed "1.13.12-extended-2.3.2" +# (no v) vs GitHub tag "v1.13.12-extended-2.3.2" (with v) must report +# status:"latest" (NOT "outdated"), with current_version/latest_version both +# emitted v-stripped and equal. +test_check_update_extended() { + header "Extended sing-box Update Check — v-prefix (task-019)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local updater="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$updater" ]; then + skip "updater.sh not found in ${NETSHIFT_LIB_DIR}" + return + fi + + local work="/tmp/netshift-extcheck-$$" + rm -rf "$work" + mkdir -p "$work" + + # Driver: source updater.sh, silence logging, OVERRIDE the three deps after + # sourcing (so the real updates_check_sing_box_extended calls our stubs), run + # the check. STUBEXT_INSTALLED = get_sing_box_version output; + # STUBEXT_RELEASES = the raw releases blob (empty → fetch-failure branch); + # STUBEXT_TAG = the resolved release tag (with the leading v, as GitHub gives). + local drv="$work/driver.sh" + cat > "$drv" << 'DRVEOF' +log() { :; } +echolog() { :; } +nolog() { :; } +. "DRV_HELPERS" +. "DRV_UPDATER" +get_sing_box_version() { printf '%s' "$STUBEXT_INSTALLED"; } +updates_fetch_sing_box_extended_releases() { printf '%s' "$STUBEXT_RELEASES"; } +updates_extended_release_tag() { printf '%s' "$STUBEXT_TAG"; } +updates_check_sing_box_extended +DRVEOF + sed -i "s|DRV_UPDATER|$updater|g;s|DRV_HELPERS|${NETSHIFT_LIB_DIR}/helpers.sh|g" "$drv" + + local out="$work/out.json" + run_extcheck() { + ash "$drv" > "$out" 2>/dev/null || true + } + + # ── Case 1: installed == latest, only the tag carries a leading v → latest ── + # THE regression: must NOT be "outdated"; both versions emitted v-stripped+eq. + export STUBEXT_INSTALLED="1.13.12-extended-2.3.2" + export STUBEXT_RELEASES='[{"tag_name":"v1.13.12-extended-2.3.2"}]' + export STUBEXT_TAG="v1.13.12-extended-2.3.2" + run_extcheck + if jq -e '.success == true and .status == "latest" + and .current_version == "1.13.12-extended-2.3.2" + and .latest_version == "1.13.12-extended-2.3.2" + and .current_version == .latest_version' "$out" > /dev/null 2>&1; then + pass "extcheck-vprefix-installed-eq-latest:OK" + else + fail "extcheck-vprefix-installed-eq-latest:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 2: installed older than the latest tag → outdated ────────────────── + export STUBEXT_INSTALLED="1.13.10-extended-2.3.0" + export STUBEXT_RELEASES='[{"tag_name":"v1.13.12-extended-2.3.2"}]' + export STUBEXT_TAG="v1.13.12-extended-2.3.2" + run_extcheck + if jq -e '.success == true and .status == "outdated" + and .current_version == "1.13.10-extended-2.3.0" + and .latest_version == "1.13.12-extended-2.3.2"' "$out" > /dev/null 2>&1; then + pass "extcheck-older-outdated:OK" + else + fail "extcheck-older-outdated:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 3: releases fetch failure (empty blob) → success:false ───────────── + export STUBEXT_INSTALLED="1.13.12-extended-2.3.2" + export STUBEXT_RELEASES="" + export STUBEXT_TAG="" + run_extcheck + if jq -e '.success == false and (.message | length) > 0' "$out" > /dev/null 2>&1; then + pass "extcheck-fetch-failure-successfalse:OK" + else + fail "extcheck-fetch-failure-successfalse:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + unset STUBEXT_INSTALLED STUBEXT_RELEASES STUBEXT_TAG + rm -rf "$work" +} + # ───────────────────────────────────────────────────────────────── # Test: NetShift self-update (task-017) # ───────────────────────────────────────────────────────────────── @@ -3037,6 +3132,7 @@ main() { test_dns_via_outbound test_global_proxy test_check_update_stable + test_check_update_extended test_self_update_netshift ;; deps) test_deps ;; @@ -3053,13 +3149,14 @@ main() { dnsdetour) test_dns_via_outbound ;; globalproxy) test_global_proxy ;; stablecheck) test_check_update_stable ;; + extcheck) test_check_update_extended ;; selfupdate) test_self_update_netshift ;; jq) test_jq_helpers ;; cm) test_config_manager ;; sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription rejected jobstate selfheal dnsdetour globalproxy stablecheck selfupdate" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck selfupdate" exit 1 ;; esac From c860bf195cfe72b4ce33e6ca66fbc55ab7f3f6d8 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Sun, 7 Jun 2026 00:33:07 +0300 Subject: [PATCH 57/75] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=B0=D0=BA=D1=82=D1=83=D0=B0=D0=BB=D1=8C=D0=BD=D1=83?= =?UTF-8?q?=D1=8E=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20man?= =?UTF-8?q?gle=20output=20=D0=B8=D0=B7=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/luci-frontend-developer.md | 25 +++++++++++++ .../memory/shell-backend-developer.md | 35 +++++++++++++++++-- fe-app-netshift/locales/calls.json | 23 +++++------- fe-app-netshift/locales/netshift.pot | 24 ++++++------- fe-app-netshift/locales/netshift.ru.po | 7 ++-- .../tabs/diagnostic/checks/runNftCheck.ts | 7 ---- fe-app-netshift/src/netshift/types.ts | 1 - .../resources/view/netshift/main.js | 9 ++--- luci-app-netshift/po/ru/netshift.po | 7 ++-- luci-app-netshift/po/templates/netshift.pot | 24 ++++++------- netshift/files/usr/bin/netshift | 16 ++------- 11 files changed, 93 insertions(+), 85 deletions(-) diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 8d369ce5..81e38711 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -353,3 +353,28 @@ append findings; keep under ~200 lines. direct path → no leak). `tsc --noEmit` flags ONE pre-existing error in `getNetshiftVersionRow.test.ts` (sing_box_extended optionality) — NOT in CI (yarn ci = format/lint/vitest/build, no tsc), pre-existing, ignore. + +## Drop stale nft "mangle output counters" check (task-020b) + +- Backend 020a removed `rules_mangle_output_counters` from `check_nft` JSON + (router-output traffic is intentionally DIRECT now → that chain's counter is + legitimately 0, so the non-zero assertion was a FALSE positive). New STABLE + 7-key shape: `{table_exist, rules_mangle_exist, rules_mangle_counters, + rules_mangle_output_exist, rules_proxy_exist, rules_proxy_counters, + rules_other_mark_exist}` — `rules_mangle_output_exist` KEPT. +- FE removal touched exactly 2 source files: `runNftCheck.ts` (drop the field + from the allGood `&&` chain, the atLeastOneGood `||` chain, and its `items[]` + row — keep the "Rules mangle output exist" row) + `types.ts` + `NftRulesCheckResult` (drop `rules_mangle_output_counters: 0 | 1;`). +- main.js: runtime diff is the removed Boolean()s + the dropped items[] row; + second build BYTE-IDENTICAL (idempotent), banner + `return baseclass.extend` + intact; `grep -c rules_mangle_output_counters main.js` == 0. +- locales: ran `node {extract-calls,generate-pot,generate-po ru, + distribute-locales}.js` (NOT yarn → no corepack). The unused + `_('Rules mangle output counters')` msgid dropped cleanly from ALL 5 catalogs + (calls.json, locales/netshift.{pot,ru.po}, po/{templates/netshift.pot, + ru/netshift.po}). msgid-level delta = PURELY a removal (1 removed, 0 added); + "Rules mangle output exist" stays. generate-po reported 325/323 (2 stale + translations retained in source ru.po — harmless, additive-preserving). +- yarn was classic 1.22.22 → `yarn ci` safe; verified `git diff --exit-code -- + yarn.lock` clean and NO `.yarn`/`.yarnrc.yml`. No vitest referenced the field. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index b2395952..693146e0 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -580,6 +580,35 @@ findings; keep under ~200 lines. — simpler than awk-extract since it's not in bin/netshift. Cases: (1) installed == latest, only tag has v → latest + both v-stripped+equal (THE regression); (2) installed older → outdated; (3) empty releases → success:false. Registered - all 5 points (all)/case/usage/docker-compose comment). shellcheck -S error clean - (bin+libs+install.sh); `smoke-tests all` = 104 passed / 0 failed (101 baseline - + 3 new). `extcheck` alone = 3/0. + all 5 points (all)/case/usage/docker-compose comment). shellcheck -S error clean + (bin+libs+install.sh); `smoke-tests all` = 104 passed / 0 failed (101 baseline + + 3 new). `extcheck` alone = 3/0. + +## task-020a: drop stale "mangle output counters" diagnostic (PR#11 B-02 align) + +- The diagnostic function the spec calls `check_nft` is actually named + **`check_nft_rules`** in bin/netshift. After PR#11 (router-originated traffic + intentionally DIRECT) the `mangle_output` chain's only counter rule + (`meta mark 0x00200000 counter return`) is essentially never hit → counter + legitimately 0 → the old non-zero-counter assertion produced a FALSE ⚠️. + Operator decision Variant A = REMOVE the "mangle output counters" check + entirely; KEEP "mangle output exist". +- Backend fix (bin/netshift ONLY, 6 deletions): in `check_nft_rules` removed the + `rules_mangle_output_counters` local, the inner `grep -qv "packets 0 bytes 0"` + block that set it, and the key from the emitted JSON echo. In `global_check` + removed it from the `local` decl, its `jq -r '.rules_mangle_output_counters // + 0'` read, and the `if ... ✅/⚠️ Rules mangle output counters` print block. + KEPT the existence check (`grep -q "counter" → rules_mangle_output_exist=1`) + and its ✅/❌ print. Did NOT touch mangle(prerouting)/proxy/other_mark or + `create_nft_rules`. +- **STABLE check_nft_rules JSON shape (cross-layer contract for frontend 020b), + exactly ONE key removed, order otherwise unchanged:** `{table_exist, + rules_mangle_exist, rules_mangle_counters, rules_mangle_output_exist, + rules_proxy_exist, rules_proxy_counters, rules_other_mark_exist}`. +- **No smoke test referenced the field** — `tests/entrypoint.sh` has no + `test_diagnostics`/nft-check assertion on the check_nft_rules JSON at all (the + `nft` category tests rule installation, not the diagnostic JSON keys). So no + smoke change and no registration change. Diagnostics-only edit (read-only + checks), NOT a routing/config change — nft model unchanged. shellcheck -S error + clean; `smoke-tests all` = 104 passed / 0 failed (unchanged baseline); UTF-8 + intact (iconv round-trip OK, 0 рџ/в”/†mojibake). diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index 38403127..69a3bddc 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -38,7 +38,7 @@ "call": "Additional marking rules found", "key": "Additional marking rules found", "places": [ - "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:106" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99" ] }, { @@ -1330,7 +1330,7 @@ "call": "No other marking rules found", "key": "No other marking rules found", "places": [ - "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:105" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:98" ] }, { @@ -1554,42 +1554,35 @@ "call": "Rules mangle counters", "key": "Rules mangle counters", "places": [ - "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:79" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:77" ] }, { "call": "Rules mangle exist", "key": "Rules mangle exist", "places": [ - "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:74" - ] - }, - { - "call": "Rules mangle output counters", - "key": "Rules mangle output counters", - "places": [ - "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:89" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:72" ] }, { "call": "Rules mangle output exist", "key": "Rules mangle output exist", "places": [ - "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:84" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:82" ] }, { "call": "Rules proxy counters", "key": "Rules proxy counters", "places": [ - "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:92" ] }, { "call": "Rules proxy exist", "key": "Rules proxy exist", "places": [ - "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:94" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:87" ] }, { @@ -1963,7 +1956,7 @@ "call": "Table exist", "key": "Table exist", "places": [ - "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:69" + "src/netshift/tabs/diagnostic/checks/runNftCheck.ts:67" ] }, { diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index f4032464..edf8b110 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 19:26+0300\n" -"PO-Revision-Date: 2026-06-06 19:26+0300\n" +"POT-Creation-Date: 2026-06-06 21:27+0300\n" +"PO-Revision-Date: 2026-06-06 21:27+0300\n" "Last-Translator: spgsroot, yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -36,7 +36,7 @@ msgstr "" msgid "Active Connections" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:106 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 msgid "Additional marking rules found" msgstr "" @@ -791,7 +791,7 @@ msgstr "" msgid "Network Interface" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:105 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:98 msgid "No other marking rules found" msgstr "" @@ -925,27 +925,23 @@ msgstr "" msgid "Routing Excluded IPs" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:79 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:77 msgid "Rules mangle counters" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:74 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:72 msgid "Rules mangle exist" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:89 -msgid "Rules mangle output counters" -msgstr "" - -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:84 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:82 msgid "Rules mangle output exist" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:92 msgid "Rules proxy counters" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:94 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:87 msgid "Rules proxy exist" msgstr "" @@ -1160,7 +1156,7 @@ msgstr "" msgid "System information" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:69 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:67 msgid "Table exist" msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 4ded1a6e..8fba41c3 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 22:26+0300\n" -"PO-Revision-Date: 2026-06-06 22:26+0300\n" +"POT-Creation-Date: 2026-06-06 00:27+0300\n" +"PO-Revision-Date: 2026-06-06 00:27+0300\n" "Last-Translator: spgsroot, yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -665,9 +665,6 @@ msgstr "Счётчики правил mangle" msgid "Rules mangle exist" msgstr "Правила mangle существуют" -msgid "Rules mangle output counters" -msgstr "Счётчики правил mangle output" - msgid "Rules mangle output exist" msgstr "Правила mangle output существуют" diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runNftCheck.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runNftCheck.ts index 65bc065f..04277163 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runNftCheck.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/checks/runNftCheck.ts @@ -40,7 +40,6 @@ export async function runNftCheck() { Boolean(data.rules_mangle_exist) && Boolean(data.rules_mangle_counters) && Boolean(data.rules_mangle_output_exist) && - Boolean(data.rules_mangle_output_counters) && Boolean(data.rules_proxy_exist) && Boolean(data.rules_proxy_counters) && !data.rules_other_mark_exist; @@ -50,7 +49,6 @@ export async function runNftCheck() { Boolean(data.rules_mangle_exist) || Boolean(data.rules_mangle_counters) || Boolean(data.rules_mangle_output_exist) || - Boolean(data.rules_mangle_output_counters) || Boolean(data.rules_proxy_exist) || Boolean(data.rules_proxy_counters) || !data.rules_other_mark_exist; @@ -84,11 +82,6 @@ export async function runNftCheck() { key: _('Rules mangle output exist'), value: '', }, - { - state: data.rules_mangle_output_counters ? 'success' : 'error', - key: _('Rules mangle output counters'), - value: '', - }, { state: data.rules_proxy_exist ? 'success' : 'error', key: _('Rules proxy exist'), diff --git a/fe-app-netshift/src/netshift/types.ts b/fe-app-netshift/src/netshift/types.ts index 17572349..df2373f7 100644 --- a/fe-app-netshift/src/netshift/types.ts +++ b/fe-app-netshift/src/netshift/types.ts @@ -183,7 +183,6 @@ export namespace NetShift { rules_mangle_exist: 0 | 1; rules_mangle_counters: 0 | 1; rules_mangle_output_exist: 0 | 1; - rules_mangle_output_counters: 0 | 1; rules_proxy_exist: 0 | 1; rules_proxy_counters: 0 | 1; rules_other_mark_exist: 0 | 1; diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index f8b39409..9b11abcd 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -3161,8 +3161,8 @@ async function runNftCheck() { throw new Error("Nftables checks failed"); } const data = nftablesChecks.data; - const allGood = Boolean(data.table_exist) && Boolean(data.rules_mangle_exist) && Boolean(data.rules_mangle_counters) && Boolean(data.rules_mangle_output_exist) && Boolean(data.rules_mangle_output_counters) && Boolean(data.rules_proxy_exist) && Boolean(data.rules_proxy_counters) && !data.rules_other_mark_exist; - const atLeastOneGood = Boolean(data.table_exist) || Boolean(data.rules_mangle_exist) || Boolean(data.rules_mangle_counters) || Boolean(data.rules_mangle_output_exist) || Boolean(data.rules_mangle_output_counters) || Boolean(data.rules_proxy_exist) || Boolean(data.rules_proxy_counters) || !data.rules_other_mark_exist; + const allGood = Boolean(data.table_exist) && Boolean(data.rules_mangle_exist) && Boolean(data.rules_mangle_counters) && Boolean(data.rules_mangle_output_exist) && Boolean(data.rules_proxy_exist) && Boolean(data.rules_proxy_counters) && !data.rules_other_mark_exist; + const atLeastOneGood = Boolean(data.table_exist) || Boolean(data.rules_mangle_exist) || Boolean(data.rules_mangle_counters) || Boolean(data.rules_mangle_output_exist) || Boolean(data.rules_proxy_exist) || Boolean(data.rules_proxy_counters) || !data.rules_other_mark_exist; const { state, description } = getMeta({ atLeastOneGood, allGood }); updateCheckStore({ order, @@ -3191,11 +3191,6 @@ async function runNftCheck() { key: _("Rules mangle output exist"), value: "" }, - { - state: data.rules_mangle_output_counters ? "success" : "error", - key: _("Rules mangle output counters"), - value: "" - }, { state: data.rules_proxy_exist ? "success" : "error", key: _("Rules proxy exist"), diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 4ded1a6e..8fba41c3 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 22:26+0300\n" -"PO-Revision-Date: 2026-06-06 22:26+0300\n" +"POT-Creation-Date: 2026-06-06 00:27+0300\n" +"PO-Revision-Date: 2026-06-06 00:27+0300\n" "Last-Translator: spgsroot, yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -665,9 +665,6 @@ msgstr "Счётчики правил mangle" msgid "Rules mangle exist" msgstr "Правила mangle существуют" -msgid "Rules mangle output counters" -msgstr "Счётчики правил mangle output" - msgid "Rules mangle output exist" msgstr "Правила mangle output существуют" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index f4032464..edf8b110 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 19:26+0300\n" -"PO-Revision-Date: 2026-06-06 19:26+0300\n" +"POT-Creation-Date: 2026-06-06 21:27+0300\n" +"PO-Revision-Date: 2026-06-06 21:27+0300\n" "Last-Translator: spgsroot, yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -36,7 +36,7 @@ msgstr "" msgid "Active Connections" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:106 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 msgid "Additional marking rules found" msgstr "" @@ -791,7 +791,7 @@ msgstr "" msgid "Network Interface" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:105 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:98 msgid "No other marking rules found" msgstr "" @@ -925,27 +925,23 @@ msgstr "" msgid "Routing Excluded IPs" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:79 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:77 msgid "Rules mangle counters" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:74 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:72 msgid "Rules mangle exist" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:89 -msgid "Rules mangle output counters" -msgstr "" - -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:84 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:82 msgid "Rules mangle output exist" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:92 msgid "Rules proxy counters" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:94 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:87 msgid "Rules proxy exist" msgstr "" @@ -1160,7 +1156,7 @@ msgstr "" msgid "System information" msgstr "" -#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:69 +#: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:67 msgid "Table exist" msgstr "" diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 2396a689..4ad6fa7b 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -3537,7 +3537,6 @@ check_nft_rules() { local rules_mangle_exist=0 local rules_mangle_counters=0 local rules_mangle_output_exist=0 - local rules_mangle_output_counters=0 local rules_proxy_exist=0 local rules_proxy_counters=0 local rules_other_mark_exist=0 @@ -3575,10 +3574,6 @@ check_nft_rules() { mangle_output_output=$(nft list chain inet "$NFT_TABLE_NAME" mangle_output) if echo "$mangle_output_output" | grep -q "counter"; then rules_mangle_output_exist=1 - - if echo "$mangle_output_output" | grep "counter" | grep -qv "packets 0 bytes 0"; then - rules_mangle_output_counters=1 - fi fi fi @@ -3613,7 +3608,7 @@ check_nft_rules() { rm -f /tmp/netshift_mark_check.$$ fi - echo "{\"table_exist\":$table_exist,\"rules_mangle_exist\":$rules_mangle_exist,\"rules_mangle_counters\":$rules_mangle_counters,\"rules_mangle_output_exist\":$rules_mangle_output_exist,\"rules_mangle_output_counters\":$rules_mangle_output_counters,\"rules_proxy_exist\":$rules_proxy_exist,\"rules_proxy_counters\":$rules_proxy_counters,\"rules_other_mark_exist\":$rules_other_mark_exist}" | jq . + echo "{\"table_exist\":$table_exist,\"rules_mangle_exist\":$rules_mangle_exist,\"rules_mangle_counters\":$rules_mangle_counters,\"rules_mangle_output_exist\":$rules_mangle_output_exist,\"rules_proxy_exist\":$rules_proxy_exist,\"rules_proxy_counters\":$rules_proxy_counters,\"rules_other_mark_exist\":$rules_other_mark_exist}" | jq . } check_sing_box() { @@ -4005,13 +4000,12 @@ global_check() { nft_check_json=$(check_nft_rules) if [ -n "$nft_check_json" ]; then - local table_exist rules_mangle_exist rules_mangle_counters rules_mangle_output_exist rules_mangle_output_counters rules_proxy_exist rules_proxy_counters rules_other_mark_exist + local table_exist rules_mangle_exist rules_mangle_counters rules_mangle_output_exist rules_proxy_exist rules_proxy_counters rules_other_mark_exist table_exist=$(echo "$nft_check_json" | jq -r '.table_exist // 0') rules_mangle_exist=$(echo "$nft_check_json" | jq -r '.rules_mangle_exist // 0') rules_mangle_counters=$(echo "$nft_check_json" | jq -r '.rules_mangle_counters // 0') rules_mangle_output_exist=$(echo "$nft_check_json" | jq -r '.rules_mangle_output_exist // 0') - rules_mangle_output_counters=$(echo "$nft_check_json" | jq -r '.rules_mangle_output_counters // 0') rules_proxy_exist=$(echo "$nft_check_json" | jq -r '.rules_proxy_exist // 0') rules_proxy_counters=$(echo "$nft_check_json" | jq -r '.rules_proxy_counters // 0') rules_other_mark_exist=$(echo "$nft_check_json" | jq -r '.rules_other_mark_exist // 0') @@ -4040,12 +4034,6 @@ global_check() { print_global "❌ Rules mangle output exist" fi - if [ "$rules_mangle_output_counters" -eq 1 ]; then - print_global "✅ Rules mangle output counters" - else - print_global "⚠️ Rules mangle output counters" - fi - if [ "$rules_proxy_exist" -eq 1 ]; then print_global "✅ Rules proxy exist" else From eb9cedb10adfe195f3f6bd59a78587b6ab7a5471 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Sun, 7 Jun 2026 01:16:42 +0300 Subject: [PATCH 58/75] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BD=D0=B5=D0=B1=D0=B5=D0=B7=D0=BE=D0=BF=D0=B0?= =?UTF-8?q?=D1=81=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/luci-frontend-developer.md | 44 ++++ .../memory/shell-backend-developer.md | 39 ++++ fe-app-netshift/locales/calls.json | 215 ++++++++++-------- fe-app-netshift/locales/netshift.pot | 211 +++++++++-------- fe-app-netshift/locales/netshift.ru.po | 20 +- .../src/validators/tests/validateUrl.test.js | 6 + fe-app-netshift/src/validators/validateUrl.ts | 57 ++++- .../resources/view/netshift/main.js | 34 ++- .../resources/view/netshift/section.js | 14 ++ luci-app-netshift/po/ru/netshift.po | 20 +- luci-app-netshift/po/templates/netshift.pot | 211 +++++++++-------- netshift/files/etc/config/netshift | 4 + netshift/files/usr/bin/netshift | 12 +- netshift/files/usr/lib/helpers.sh | 119 +++++----- tests/docker-compose.yml | 6 +- tests/entrypoint.sh | 149 +++++++++++- 16 files changed, 798 insertions(+), 363 deletions(-) diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 81e38711..302b0846 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -378,3 +378,47 @@ append findings; keep under ~200 lines. translations retained in source ru.po — harmless, additive-preserving). - yarn was classic 1.22.22 → `yarn ci` safe; verified `git diff --exit-code -- yarn.lock` clean and NO `.yarn`/`.yarnrc.yml`. No vitest referenced the field. + +## validateUrl accepts IP host + subscription_insecure checkbox (task-021a) + +- `validateUrl.ts` REWRITE: old regex required an ALPHA TLD so an IPv4/IPv6 host + was rejected ("Invalid URL format"). New approach mirrors `validateSocksUrl`: + keep the protocol check (default `['http:','https:']`), then a pure + module-private `extractHost(url)` strips `scheme://` (indexOf '://'), the + `/path?query#frag` (`rest.search(/[/?#]/)` — first of `/ ? #`), optional + `userinfo@` (`lastIndexOf('@')`), and the `:port`. BRACKETED IPv6: + if `rest.startsWith('[')`, return the substring between `[` and the first `]` + (so `[2001:db8::1]:2096` → `2001:db8::1`); else strip trailing `:port` via + `lastIndexOf(':')`. Then accept if `validateIPV4(host).valid || + validateIPV6(host).valid || validateDomain(host).valid`. Kept the 3 existing + messages verbatim (`Invalid URL format`, the protocol message, `Valid`). +- NB `validateIPV6` ALREADY unwraps brackets internally (`.replace(/^\[/,'')`), + but extractHost must unwrap anyway so the `:port` after `]` is dropped before + the validator sees it. `extractHost` is NOT barrel-exported (module-private) → + no `main.*` leak; main.js diff is +30/-4 (the helper + the host-check), second + build BYTE-IDENTICAL, banner + `return baseclass.extend({` intact. +- This single fix covers ALL FOUR callers (subscription_url, urltest_testing_url, + remote_domain_lists, remote_subnet_lists) — callers unchanged. +- TESTS: `validateDomain` accepts a trailing path (`example.com/path` regex has + `(?:\/[^\s]*)?$`), so existing domain-with-path valid cases still pass through + the domain branch. Added valid: `https://91.199.111.52:2096/sub/abc`, + `http://10.0.0.1/x`, `https://[2001:db8::1]:2096/sub`. Added invalid: + `https://999.1.1.1/x` (bad IPv4 → not domain either), `ftp://1.2.3.4` (protocol + fails first), `https://` (extractHost → '' → "Invalid URL format"). Kept + `https://google` invalid (no TLD, not an IP). +- section.js (HAND-WRITTEN, NOT bundled → 0 in main.js): added `form.Flag` + `subscription_insecure` right AFTER subscription_url (~line 113), default `"0"`, + `rmempty=false`, `depends({connection_type:'proxy',proxy_config_type: + 'subscription'})` exactly like its siblings (no `subscription_user_agent` exists + here despite the spec mention). Multi-sentence description = three `_()` calls + joined with `+ " " +` OUTSIDE `_()` (F-02 rule). UCI contract option name + `subscription_insecure` (0|1) consumed by backend 021b. +- locales: `node {extract-calls,generate-pot,generate-po ru,distribute-locales}.js` + (NOT yarn). 4 new msgids (1 label + 3 description sentences), PURELY additive + (4 added, 0 removed at msgid level). Filled RU in SOURCE `locales/netshift.ru.po` + then distributed → po/ru + po/templates byte-identical to source. Only the PO + HEADER msgstr stays empty (`grep -nB1 'msgstr ""'` shows just line 6/7). +- yarn classic 1.22.22 → `yarn ci` green (format/lint --max-warnings=0/471 tests/ + build); verified yarn.lock unchanged + NO `.yarn`/`.yarnrc.yml`. The + `netshift/files/**` + `tests/**` changes in git status are 021b (other agent), + not mine. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 693146e0..6110206e 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -612,3 +612,42 @@ findings; keep under ~200 lines. checks), NOT a routing/config change — nft model unchanged. shellcheck -S error clean; `smoke-tests all` = 104 passed / 0 failed (unchanged baseline); UTF-8 intact (iconv round-trip OK, 0 рџ/в”/†mojibake). + +## task-021b: opt-in insecure subscription fetch (--no-check-certificate) + +- Cross-layer UCI contract (STABLE, shared with 021a frontend): + `option subscription_insecure '0'` (0|1), per `config section`. Default OFF = + unchanged secure behavior. On device wget=uclient-fetch supports + `--no-check-certificate` (confirmed) — for IP-host panels with invalid/ + self-signed/missing-SAN HTTPS certs. +- `download_subscription` (helpers.sh) had SIX identical wget invocations (4 in + the main loop: ipv4/normal × proxy/no-proxy + 2 in the IPv4 retry), each with + the same 7 `--header` set. Refactored ALL six through a new private helper + `_wget_subscription_request "$cert_flag" UA HWID MODEL KERNEL OUT ERR URL -- + <leading flags>`: it runs `wget $cert_flag "$@" -O "$out" <headers> "$url" + 2>"$err"`. The `$cert_flag` is the ONE intentional unquoted expansion + (`# shellcheck disable=SC2086` on that line): empty string word-splits to ZERO + args (byte-identical secure default), `--no-check-certificate` adds exactly + one. NO eval. Per-branch `-4`/`-T <timeout>` are passed as the trailing + `"$@"` flags; proxy env (`http_proxy=`/`https_proxy=`) still set on the call + line. Retry/fallback/rc/mv/errfile logic untouched. +- 8th positional `insecure="${8:-0}"`; `cert_flag` derived once at top + (`[ "$insecure" = "1" ]`). +- bin/netshift `download_subscription_into_cache`: read + `subscription_insecure="$(uci -q get "netshift.${section}.subscription_insecure" + 2>/dev/null)"`, default 0, log ONE redacted `warn` + (`...uses --no-check-certificate (TLS verification disabled): url=$(redact_url_for_log ...)`) + when =1, pass as the NEW 8th arg after the existing + `... 3 2 10 "$effective_user_agent"`. Declared `local subscription_insecure`. +- UCI example: added commented `#option subscription_insecure '0'` + 3-line + comment near the `subscription_url` example in `etc/config/netshift`. +- Smoke: NEW top-level `test_insecure_fetch` (alias `insecure`, 6 cases). A + PATH-prepended fake `wget` records full argv (`printf '%s\n' "$*"`) and writes + a dummy body to its `-O` target so attempt-1 succeeds (no retry). Driver + sources REAL helpers.sh, stubs log/metadata helpers + `should_force_wget_ipv4` + (per-scenario normal vs ipv4) + inert `has_ipv4_default_route`/ + `wget_supports_ipv4_flag`. Asserts `--no-check-certificate` ABSENT@insecure=0 / + PRESENT@insecure=1 across normal+proxy+ipv4 branches (`-4` co-present on ipv4). + Registered all 5 points (all)/case alias/usage line/docker-compose comment). + shellcheck -S error clean; `smoke-tests all` = 110 passed / 0 failed (104 + baseline + 6 new); UTF-8/LF intact. Additive, NO runtime-contract change. diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index 69a3bddc..e296df4e 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -48,6 +48,13 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:469" ] }, + { + "call": "Allow insecure TLS for subscription fetch", + "key": "Allow insecure TLS for subscription fetch", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116" + ] + }, { "call": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "key": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", @@ -59,21 +66,21 @@ "call": "Applicable for SOCKS and Shadowsocks proxy", "key": "Applicable for SOCKS and Shadowsocks proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:300" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:314" ] }, { "call": "At least one valid domain must be specified. Comments-only content is not allowed.", "key": "At least one valid domain must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:562" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576" ] }, { "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:643" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:657" ] }, { @@ -208,7 +215,7 @@ "call": "Community Lists", "key": "Community Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:417" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431" ] }, { @@ -356,8 +363,15 @@ "call": "Disabled", "key": "Disabled", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:508", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:522", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602" + ] + }, + { + "call": "Disables TLS certificate verification when downloading the subscription.", + "key": "Disables TLS certificate verification when downloading the subscription.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117" ] }, { @@ -378,7 +392,7 @@ "call": "DNS over HTTPS (DoH)", "key": "DNS over HTTPS (DoH)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15" ] }, @@ -386,7 +400,7 @@ "call": "DNS over TLS (DoT)", "key": "DNS over TLS (DoT)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16" ] }, @@ -394,7 +408,7 @@ "call": "DNS Protocol Type", "key": "DNS Protocol Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12" ] }, @@ -409,7 +423,7 @@ "call": "DNS Server", "key": "DNS Server", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:395", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:409", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24" ] }, @@ -431,7 +445,7 @@ "call": "Domain Resolver", "key": "Domain Resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:372" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386" ] }, { @@ -482,15 +496,15 @@ "call": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "key": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169" ] }, { "call": "Dynamic List", "key": "Dynamic List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:509", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:589" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:523", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603" ] }, { @@ -504,14 +518,14 @@ "call": "Enable built-in DNS resolver for domains handled by this section", "key": "Enable built-in DNS resolver for domains handled by this section", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387" ] }, { "call": "Enable DNS resolve to get real IP when routing", "key": "Enable DNS resolve to get real IP when routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:812" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:826" ] }, { @@ -532,7 +546,7 @@ "call": "Enable Mixed Proxy", "key": "Enable Mixed Proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:797" ] }, { @@ -546,7 +560,7 @@ "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:798" ] }, { @@ -574,21 +588,21 @@ "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:558" ] }, { "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:518" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532" ] }, { "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:598" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612" ] }, { @@ -602,70 +616,70 @@ "call": "Every 1 minute", "key": "Every 1 minute", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:233" ] }, { "call": "Every 12 hours", "key": "Every 12 hours", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:137" ] }, { "call": "Every 3 hours", "key": "Every 3 hours", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:135" ] }, { "call": "Every 3 minutes", "key": "Every 3 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:234" ] }, { "call": "Every 30 minutes", "key": "Every 30 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:133" ] }, { "call": "Every 30 seconds", "key": "Every 30 seconds", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:218" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232" ] }, { "call": "Every 5 minutes", "key": "Every 5 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:221" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:235" ] }, { "call": "Every 6 hours", "key": "Every 6 hours", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:136" ] }, { "call": "Every day", "key": "Every day", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:124" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:138" ] }, { "call": "Every hour", "key": "Every hour", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:120" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:134" ] }, { @@ -686,7 +700,7 @@ "call": "Exclude servers by keyword", "key": "Exclude servers by keyword", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168" ] }, { @@ -729,7 +743,7 @@ "call": "Fully Routed IPs", "key": "Fully Routed IPs", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:756" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:770" ] }, { @@ -750,14 +764,14 @@ "call": "Global Proxy", "key": "Global Proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323" ] }, { "call": "How often to automatically update the subscription", "key": "How often to automatically update the subscription", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131" ] }, { @@ -771,7 +785,7 @@ "call": "Include servers by keyword", "key": "Include servers by keyword", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:143" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:157" ] }, { @@ -1110,8 +1124,9 @@ "call": "Invalid URL format", "key": "Invalid URL format", "places": [ - "src/validators/validateUrl.ts:8", - "src/validators/validateUrl.ts:31" + "src/validators/validateUrl.ts:49", + "src/validators/validateUrl.ts:66", + "src/validators/validateUrl.ts:78" ] }, { @@ -1196,7 +1211,7 @@ "call": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "key": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:144" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:158" ] }, { @@ -1232,14 +1247,14 @@ "call": "Local Domain Lists", "key": "Local Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:664" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:678" ] }, { "call": "Local Subnet Lists", "key": "Local Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:687" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701" ] }, { @@ -1274,7 +1289,7 @@ "call": "Mixed Proxy Port", "key": "Mixed Proxy Port", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:796" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:810" ] }, { @@ -1288,7 +1303,7 @@ "call": "Must be a number in the range of 50 - 1000", "key": "Must be a number in the range of 50 - 1000", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:255" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269" ] }, { @@ -1323,7 +1338,7 @@ "call": "Network Interface", "key": "Network Interface", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:340" ] }, { @@ -1381,7 +1396,7 @@ "call": "Only one section can be global at a time.", "key": "Only one section can be global at a time.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:318" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332" ] }, { @@ -1484,28 +1499,28 @@ "call": "Regional options cannot be used together", "key": "Regional options cannot be used together", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:451" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465" ] }, { "call": "Remote Domain Lists", "key": "Remote Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:710" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:724" ] }, { "call": "Remote Subnet Lists", "key": "Remote Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:733" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:747" ] }, { "call": "Resolve real IP for routing", "key": "Resolve real IP for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:811" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:825" ] }, { @@ -1519,7 +1534,7 @@ "call": "Route all unmatched traffic through this section's outbound.", "key": "Route all unmatched traffic through this section's outbound.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:310" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324" ] }, { @@ -1596,7 +1611,7 @@ "call": "Russia inside restrictions", "key": "Russia inside restrictions", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:470" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:484" ] }, { @@ -1617,7 +1632,7 @@ "call": "Select a predefined list for routing", "key": "Select a predefined list for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432" ] }, { @@ -1652,14 +1667,14 @@ "call": "Select network interface for VPN connection", "key": "Select network interface for VPN connection", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:327" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:341" ] }, { "call": "Select or enter DNS server address", "key": "Select or enter DNS server address", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:410", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25" ] }, @@ -1681,21 +1696,21 @@ "call": "Select the DNS protocol type for the domain resolver", "key": "Select the DNS protocol type for the domain resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:397" ] }, { "call": "Select the list type for adding custom domains", "key": "Select the list type for adding custom domains", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:520" ] }, { "call": "Select the list type for adding custom subnets", "key": "Select the list type for adding custom subnets", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:600" ] }, { @@ -1737,7 +1752,7 @@ "call": "Selector Proxy Links", "key": "Selector Proxy Links", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179" ] }, { @@ -1850,29 +1865,29 @@ "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:771" ] }, { "call": "Specify remote URLs to download and use domain lists", "key": "Specify remote URLs to download and use domain lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:711" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:725" ] }, { "call": "Specify remote URLs to download and use subnet lists", "key": "Specify remote URLs to download and use subnet lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:748" ] }, { "call": "Specify the path to the list file located on the router filesystem", "key": "Specify the path to the list file located on the router filesystem", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:688" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:679", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702" ] }, { @@ -1900,7 +1915,7 @@ "call": "Subscription Update Interval", "key": "Subscription Update Interval", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:130" ] }, { @@ -1970,8 +1985,8 @@ "call": "Text List", "key": "Text List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:510", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:524", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:604" ] }, { @@ -1985,21 +2000,28 @@ "call": "The interval between connectivity tests", "key": "The interval between connectivity tests", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230" ] }, { "call": "The maximum difference in response times (ms) allowed when comparing servers", "key": "The maximum difference in response times (ms) allowed when comparing servers", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:244" ] }, { "call": "The URL used to test server connectivity", "key": "The URL used to test server connectivity", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:262" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:276" + ] + }, + { + "call": "This is a security trade-off: an attacker could intercept the fetch.", + "key": "This is a security trade-off: an attacker could intercept the fetch.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121" ] }, { @@ -2055,7 +2077,7 @@ "call": "UDP (Unprotected DNS)", "key": "UDP (Unprotected DNS)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17" ] }, @@ -2063,7 +2085,7 @@ "call": "UDP over TCP", "key": "UDP over TCP", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:313" ] }, { @@ -2142,7 +2164,7 @@ "call": "URL must use one of the following protocols:", "key": "URL must use one of the following protocols:", "places": [ - "src/validators/validateUrl.ts:17" + "src/validators/validateUrl.ts:58" ] }, { @@ -2156,28 +2178,35 @@ "call": "URLTest Check Interval", "key": "URLTest Check Interval", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:215" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229" ] }, { "call": "URLTest Proxy Links", "key": "URLTest Proxy Links", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:190" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204" ] }, { "call": "URLTest Testing URL", "key": "URLTest Testing URL", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:261" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:275" ] }, { "call": "URLTest Tolerance", "key": "URLTest Tolerance", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243" + ] + }, + { + "call": "Use only for IP-host panels that serve an invalid or self-signed certificate.", + "key": "Use only for IP-host panels that serve an invalid or self-signed certificate.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119" ] }, { @@ -2191,49 +2220,49 @@ "call": "Use with Exclusion sections to route specific domains directly.", "key": "Use with Exclusion sections to route specific domains directly.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:330" ] }, { "call": "User Domain List Type", "key": "User Domain List Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:519" ] }, { "call": "User Domains", "key": "User Domains", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:517" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:531" ] }, { "call": "User Domains List", "key": "User Domains List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:557" ] }, { "call": "User Subnet List Type", "key": "User Subnet List Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:599" ] }, { "call": "User Subnets", "key": "User Subnets", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:597" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611" ] }, { "call": "User Subnets List", "key": "User Subnets List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:623" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:637" ] }, { @@ -2256,7 +2285,7 @@ "src/validators/validateSubnet.ts:30", "src/validators/validateSubnet.ts:52", "src/validators/validateTrojanUrl.ts:59", - "src/validators/validateUrl.ts:28", + "src/validators/validateUrl.ts:75", "src/validators/validateVlessUrl.ts:108", "src/validators/validateVmessUrl.ts:86" ] @@ -2265,8 +2294,8 @@ "call": "Validation errors:", "key": "Validation errors:", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:655" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:669" ] }, { @@ -2296,29 +2325,29 @@ "key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "places": [ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:38", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:191" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205" ] }, { "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:453" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467" ] }, { "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:486" ] }, { "call": "When enabled, traffic not matching any other section's lists will go through this proxy.", "key": "When enabled, traffic not matching any other section's lists will go through this proxy.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:312" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326" ] }, { @@ -2346,14 +2375,14 @@ "call": "Группировать по странам", "key": "Группировать по странам", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:145" ] }, { "call": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", "key": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:132" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:146" ] } ] \ No newline at end of file diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index edf8b110..034131a9 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# spgsroot, yandexru45 <>, 2026. +# yandexru45 <>, 2026. #, fuzzy msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 21:27+0300\n" -"PO-Revision-Date: 2026-06-06 21:27+0300\n" -"Last-Translator: spgsroot, yandexru45 <>\n" +"POT-Creation-Date: 2026-06-06 21:53+0300\n" +"PO-Revision-Date: 2026-06-06 21:53+0300\n" +"Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" @@ -44,19 +44,23 @@ msgstr "" msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116 +msgid "Allow insecure TLS for subscription fetch" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:300 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:314 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:562 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:643 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:657 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -137,7 +141,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:417 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431 msgid "Community Lists" msgstr "" @@ -222,11 +226,15 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:508 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:522 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602 msgid "Disabled" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117 +msgid "Disables TLS certificate verification when downloading the subscription." +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:88 msgid "DNS on router" msgstr "" @@ -235,17 +243,17 @@ msgstr "" msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12 msgid "DNS Protocol Type" msgstr "" @@ -254,7 +262,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:395 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:409 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24 msgid "DNS Server" msgstr "" @@ -267,7 +275,7 @@ msgstr "" msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:372 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386 msgid "Domain Resolver" msgstr "" @@ -297,12 +305,12 @@ msgstr "" msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:509 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:589 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:523 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603 msgid "Dynamic List" msgstr "" @@ -310,11 +318,11 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:812 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:826 msgid "Enable DNS resolve to get real IP when routing" msgstr "" @@ -326,7 +334,7 @@ msgstr "" msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:797 msgid "Enable Mixed Proxy" msgstr "" @@ -334,7 +342,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:798 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -350,15 +358,15 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:558 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:518 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:598 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" @@ -366,43 +374,43 @@ msgstr "" msgid "Enter the subscription URL to fetch proxy configurations from your provider" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:233 msgid "Every 1 minute" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:137 msgid "Every 12 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:135 msgid "Every 3 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:234 msgid "Every 3 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:133 msgid "Every 30 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:218 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:221 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:235 msgid "Every 5 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:136 msgid "Every 6 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:124 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:138 msgid "Every day" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:120 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:134 msgid "Every hour" msgstr "" @@ -414,7 +422,7 @@ msgstr "" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 msgid "Exclude servers by keyword" msgstr "" @@ -445,7 +453,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:756 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:770 msgid "Fully Routed IPs" msgstr "" @@ -457,11 +465,11 @@ msgstr "" msgid "Global check" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323 msgid "Global Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131 msgid "How often to automatically update the subscription" msgstr "" @@ -469,7 +477,7 @@ msgstr "" msgid "HTTP error" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:143 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:157 msgid "Include servers by keyword" msgstr "" @@ -664,8 +672,9 @@ msgstr "" msgid "Invalid Trojan URL: parsing failed" msgstr "" -#: src/validators/validateUrl.ts:8 -#: src/validators/validateUrl.ts:31 +#: src/validators/validateUrl.ts:49 +#: src/validators/validateUrl.ts:66 +#: src/validators/validateUrl.ts:78 msgid "Invalid URL format" msgstr "" @@ -714,7 +723,7 @@ msgstr "" msgid "Issues detected" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:144 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:158 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" @@ -735,11 +744,11 @@ msgstr "" msgid "List Update Frequency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:664 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:678 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:687 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701 msgid "Local Subnet Lists" msgstr "" @@ -759,7 +768,7 @@ msgstr "" msgid "Memory Usage" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:796 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:810 msgid "Mixed Proxy Port" msgstr "" @@ -767,7 +776,7 @@ msgstr "" msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:255 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -787,7 +796,7 @@ msgstr "" msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:340 msgid "Network Interface" msgstr "" @@ -824,7 +833,7 @@ msgstr "" msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:318 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332 msgid "Only one section can be global at a time." msgstr "" @@ -885,19 +894,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:451 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:710 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:724 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:733 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:747 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:811 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:825 msgid "Resolve real IP for routing" msgstr "" @@ -905,7 +914,7 @@ msgstr "" msgid "Restart NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:310 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 msgid "Route all unmatched traffic through this section's outbound." msgstr "" @@ -949,7 +958,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:470 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:484 msgid "Russia inside restrictions" msgstr "" @@ -961,7 +970,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432 msgid "Select a predefined list for routing" msgstr "" @@ -981,11 +990,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:327 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:341 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:410 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25 msgid "Select or enter DNS server address" msgstr "" @@ -998,15 +1007,15 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:397 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:520 msgid "Select the list type for adding custom domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:600 msgid "Select the list type for adding custom subnets" msgstr "" @@ -1030,7 +1039,7 @@ msgstr "" msgid "Selector" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 msgid "Selector Proxy Links" msgstr "" @@ -1095,20 +1104,20 @@ msgstr "" msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:771 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:711 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:725 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:748 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:688 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:679 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1124,7 +1133,7 @@ msgstr "" msgid "Subscription" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:130 msgid "Subscription Update Interval" msgstr "" @@ -1164,8 +1173,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:510 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:524 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:604 msgid "Text List" msgstr "" @@ -1173,18 +1182,22 @@ msgstr "" msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:244 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:262 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:276 msgid "The URL used to test server connectivity" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121 +msgid "This is a security trade-off: an attacker could intercept the fetch." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:465 msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." msgstr "" @@ -1213,12 +1226,12 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:313 msgid "UDP over TCP" msgstr "" @@ -1270,7 +1283,7 @@ msgstr "" msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" -#: src/validators/validateUrl.ts:17 +#: src/validators/validateUrl.ts:58 msgid "URL must use one of the following protocols:" msgstr "" @@ -1278,51 +1291,55 @@ msgstr "" msgid "URLTest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:215 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:190 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:261 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:275 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243 msgid "URLTest Tolerance" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119 +msgid "Use only for IP-host panels that serve an invalid or self-signed certificate." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:486 msgid "Use this only when the router has working IPv6 connectivity." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:330 msgid "Use with Exclusion sections to route specific domains directly." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:519 msgid "User Domain List Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:517 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:531 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:557 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:599 msgid "User Subnet List Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:597 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:623 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:637 msgid "User Subnets List" msgstr "" @@ -1342,14 +1359,14 @@ msgstr "" #: src/validators/validateSubnet.ts:30 #: src/validators/validateSubnet.ts:52 #: src/validators/validateTrojanUrl.ts:59 -#: src/validators/validateUrl.ts:28 +#: src/validators/validateUrl.ts:75 #: src/validators/validateVlessUrl.ts:108 #: src/validators/validateVmessUrl.ts:86 msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:655 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:669 msgid "Validation errors:" msgstr "" @@ -1367,20 +1384,20 @@ msgid "Visit Wiki" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:38 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:191 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:453 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:486 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:312 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326 msgid "When enabled, traffic not matching any other section's lists will go through this proxy." msgstr "" @@ -1396,10 +1413,10 @@ msgstr "" msgid "You can select Output Network Interface, by default autodetect" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:145 msgid "Группировать по странам" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:132 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:146 msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 8fba41c3..b18932de 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -1,15 +1,15 @@ # RU translations for NETSHIFT package. # Copyright (C) 2026 THE NETSHIFT'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# spgsroot, yandexru45, 2026. +# yandexru45, 2026. # msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 00:27+0300\n" -"PO-Revision-Date: 2026-06-06 00:27+0300\n" -"Last-Translator: spgsroot, yandexru45\n" +"POT-Creation-Date: 2026-06-06 00:53+0300\n" +"PO-Revision-Date: 2026-06-06 00:53+0300\n" +"Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -38,6 +38,9 @@ msgstr "Найдены дополнительные правила маркир msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." msgstr "Затрагивает публичные DoH-серверы Cloudflare, Google, Quad9, OpenDNS, AdGuard и Yandex." +msgid "Allow insecure TLS for subscription fetch" +msgstr "Разрешить небезопасный TLS при загрузке подписки" + msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт." @@ -167,6 +170,9 @@ msgstr "Отключить QUIC протокол для улучшения со msgid "Disabled" msgstr "Отключено" +msgid "Disables TLS certificate verification when downloading the subscription." +msgstr "Отключает проверку TLS-сертификата при загрузке подписки." + msgid "DNS on router" msgstr "DNS на роутере" @@ -851,6 +857,9 @@ msgstr "Максимально допустимая разница во врем msgid "The URL used to test server connectivity" msgstr "URL-адрес, используемый для проверки подключения к серверу" +msgid "This is a security trade-off: an attacker could intercept the fetch." +msgstr "Это компромисс в безопасности: злоумышленник может перехватить загрузку." + msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." msgstr "Это не позволяет приложениям обходить DNS-фильтрацию роутера за счёт использования собственного шифрованного DNS." @@ -920,6 +929,9 @@ msgstr "URLTest ссылка для проверки" msgid "URLTest Tolerance" msgstr "URLTest допустимое отклонение" +msgid "Use only for IP-host panels that serve an invalid or self-signed certificate." +msgstr "Используйте только для панелей с IP-адресом, у которых недействительный или самоподписанный сертификат." + msgid "Use this only when the router has working IPv6 connectivity." msgstr "Используйте это только если на роутере есть рабочее подключение по IPv6." diff --git a/fe-app-netshift/src/validators/tests/validateUrl.test.js b/fe-app-netshift/src/validators/tests/validateUrl.test.js index f8ae3d69..c6cbef2c 100644 --- a/fe-app-netshift/src/validators/tests/validateUrl.test.js +++ b/fe-app-netshift/src/validators/tests/validateUrl.test.js @@ -8,6 +8,9 @@ const validUrls = [ ['With query', 'https://example.com/?q=test'], ['With port', 'http://example.com:8080'], ['With subdomain', 'https://sub.example.com'], + ['IPv4 host with port and path', 'https://91.199.111.52:2096/sub/abc'], + ['IPv4 host with path', 'http://10.0.0.1/x'], + ['Bracketed IPv6 host with port and path', 'https://[2001:db8::1]:2096/sub'], ]; const invalidUrls = [ @@ -17,6 +20,9 @@ const invalidUrls = [ ['Unsupported protocol (ws)', 'ws://example.com'], ['Empty string', ''], ['Without tld', 'https://google'], + ['Bad IPv4 host', 'https://999.1.1.1/x'], + ['Bad protocol with IP host', 'ftp://1.2.3.4'], + ['No host', 'https://'], ]; describe('validateUrl', () => { diff --git a/fe-app-netshift/src/validators/validateUrl.ts b/fe-app-netshift/src/validators/validateUrl.ts index 417b4e79..38944bea 100644 --- a/fe-app-netshift/src/validators/validateUrl.ts +++ b/fe-app-netshift/src/validators/validateUrl.ts @@ -1,4 +1,45 @@ import { ValidationResult } from './types'; +import { validateDomain } from './validateDomain'; +import { validateIPV4, validateIPV6 } from './validateIp'; + +// Extracts the bare host from a URL, stripping the scheme, optional userinfo, +// the port, and the path/query/fragment. A bracketed IPv6 literal (e.g. +// "[2001:db8::1]") is unwrapped to "2001:db8::1". +function extractHost(url: string): string { + // Strip scheme:// + const schemeIndex = url.indexOf('://'); + let rest = schemeIndex === -1 ? url : url.slice(schemeIndex + 3); + + // Strip path/query/fragment (everything from the first '/', '?' or '#'). + const pathIndex = rest.search(/[/?#]/); + if (pathIndex !== -1) { + rest = rest.slice(0, pathIndex); + } + + // Strip optional userinfo ("user:pass@"). + const atIndex = rest.lastIndexOf('@'); + if (atIndex !== -1) { + rest = rest.slice(atIndex + 1); + } + + // Bracketed IPv6 literal: "[2001:db8::1]:2096" -> "2001:db8::1". + if (rest.startsWith('[')) { + const closeIndex = rest.indexOf(']'); + if (closeIndex !== -1) { + return rest.slice(1, closeIndex); + } + // Unterminated bracket: drop the leading '[' and any ':port' suffix. + return rest.slice(1).split(':')[0]; + } + + // Bare host: strip a trailing ":port". + const colonIndex = rest.lastIndexOf(':'); + if (colonIndex !== -1) { + rest = rest.slice(0, colonIndex); + } + + return rest; +} export function validateUrl( url: string, @@ -19,12 +60,18 @@ export function validateUrl( protocols.join(', '), }; - const regex = new RegExp( - `^(?:${protocols.map((p) => p.replace(':', '')).join('|')})://` + - `(?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,}(?::\\d+)?(?:/[^\\s]*)?$`, - ); + const host = extractHost(url); + + if (!host) { + return { valid: false, message: _('Invalid URL format') }; + } + + const isValidHost = + validateIPV4(host).valid || + validateIPV6(host).valid || + validateDomain(host).valid; - if (regex.test(url)) { + if (isValidHost) { return { valid: true, message: _('Valid') }; } diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 9b11abcd..c3c12ab1 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -134,6 +134,30 @@ function validateDNS(value) { } // src/validators/validateUrl.ts +function extractHost(url) { + const schemeIndex = url.indexOf("://"); + let rest = schemeIndex === -1 ? url : url.slice(schemeIndex + 3); + const pathIndex = rest.search(/[/?#]/); + if (pathIndex !== -1) { + rest = rest.slice(0, pathIndex); + } + const atIndex = rest.lastIndexOf("@"); + if (atIndex !== -1) { + rest = rest.slice(atIndex + 1); + } + if (rest.startsWith("[")) { + const closeIndex = rest.indexOf("]"); + if (closeIndex !== -1) { + return rest.slice(1, closeIndex); + } + return rest.slice(1).split(":")[0]; + } + const colonIndex = rest.lastIndexOf(":"); + if (colonIndex !== -1) { + rest = rest.slice(0, colonIndex); + } + return rest; +} function validateUrl(url, protocols = ["http:", "https:"]) { if (!url.length) { return { valid: false, message: _("Invalid URL format") }; @@ -144,10 +168,12 @@ function validateUrl(url, protocols = ["http:", "https:"]) { valid: false, message: _("URL must use one of the following protocols:") + " " + protocols.join(", ") }; - const regex = new RegExp( - `^(?:${protocols.map((p) => p.replace(":", "")).join("|")})://(?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,}(?::\\d+)?(?:/[^\\s]*)?$` - ); - if (regex.test(url)) { + const host = extractHost(url); + if (!host) { + return { valid: false, message: _("Invalid URL format") }; + } + const isValidHost = validateIPV4(host).valid || validateIPV6(host).valid || validateDomain(host).valid; + if (isValidHost) { return { valid: true, message: _("Valid") }; } return { valid: false, message: _("Invalid URL format") }; diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index 1600d909..a7bc9f1d 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -110,6 +110,20 @@ function createSectionContent(section) { return validation.message; }; + o = section.option( + form.Flag, + "subscription_insecure", + _("Allow insecure TLS for subscription fetch"), + _("Disables TLS certificate verification when downloading the subscription.") + + " " + + _("Use only for IP-host panels that serve an invalid or self-signed certificate.") + + " " + + _("This is a security trade-off: an attacker could intercept the fetch."), + ); + o.default = "0"; + o.rmempty = false; + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o = section.option( form.ListValue, "subscription_update_interval", diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 8fba41c3..b18932de 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -1,15 +1,15 @@ # RU translations for NETSHIFT package. # Copyright (C) 2026 THE NETSHIFT'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# spgsroot, yandexru45, 2026. +# yandexru45, 2026. # msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 00:27+0300\n" -"PO-Revision-Date: 2026-06-06 00:27+0300\n" -"Last-Translator: spgsroot, yandexru45\n" +"POT-Creation-Date: 2026-06-06 00:53+0300\n" +"PO-Revision-Date: 2026-06-06 00:53+0300\n" +"Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -38,6 +38,9 @@ msgstr "Найдены дополнительные правила маркир msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." msgstr "Затрагивает публичные DoH-серверы Cloudflare, Google, Quad9, OpenDNS, AdGuard и Yandex." +msgid "Allow insecure TLS for subscription fetch" +msgstr "Разрешить небезопасный TLS при загрузке подписки" + msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт." @@ -167,6 +170,9 @@ msgstr "Отключить QUIC протокол для улучшения со msgid "Disabled" msgstr "Отключено" +msgid "Disables TLS certificate verification when downloading the subscription." +msgstr "Отключает проверку TLS-сертификата при загрузке подписки." + msgid "DNS on router" msgstr "DNS на роутере" @@ -851,6 +857,9 @@ msgstr "Максимально допустимая разница во врем msgid "The URL used to test server connectivity" msgstr "URL-адрес, используемый для проверки подключения к серверу" +msgid "This is a security trade-off: an attacker could intercept the fetch." +msgstr "Это компромисс в безопасности: злоумышленник может перехватить загрузку." + msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." msgstr "Это не позволяет приложениям обходить DNS-фильтрацию роутера за счёт использования собственного шифрованного DNS." @@ -920,6 +929,9 @@ msgstr "URLTest ссылка для проверки" msgid "URLTest Tolerance" msgstr "URLTest допустимое отклонение" +msgid "Use only for IP-host panels that serve an invalid or self-signed certificate." +msgstr "Используйте только для панелей с IP-адресом, у которых недействительный или самоподписанный сертификат." + msgid "Use this only when the router has working IPv6 connectivity." msgstr "Используйте это только если на роутере есть рабочее подключение по IPv6." diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index edf8b110..034131a9 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -1,15 +1,15 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the NETSHIFT package. -# spgsroot, yandexru45 <>, 2026. +# yandexru45 <>, 2026. #, fuzzy msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 21:27+0300\n" -"PO-Revision-Date: 2026-06-06 21:27+0300\n" -"Last-Translator: spgsroot, yandexru45 <>\n" +"POT-Creation-Date: 2026-06-06 21:53+0300\n" +"PO-Revision-Date: 2026-06-06 21:53+0300\n" +"Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" "MIME-Version: 1.0\n" @@ -44,19 +44,23 @@ msgstr "" msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116 +msgid "Allow insecure TLS for subscription fetch" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:300 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:314 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:562 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:643 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:657 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -137,7 +141,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:417 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431 msgid "Community Lists" msgstr "" @@ -222,11 +226,15 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:508 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:522 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602 msgid "Disabled" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117 +msgid "Disables TLS certificate verification when downloading the subscription." +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:88 msgid "DNS on router" msgstr "" @@ -235,17 +243,17 @@ msgstr "" msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12 msgid "DNS Protocol Type" msgstr "" @@ -254,7 +262,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:395 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:409 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24 msgid "DNS Server" msgstr "" @@ -267,7 +275,7 @@ msgstr "" msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:372 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386 msgid "Domain Resolver" msgstr "" @@ -297,12 +305,12 @@ msgstr "" msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:509 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:589 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:523 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603 msgid "Dynamic List" msgstr "" @@ -310,11 +318,11 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:812 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:826 msgid "Enable DNS resolve to get real IP when routing" msgstr "" @@ -326,7 +334,7 @@ msgstr "" msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:797 msgid "Enable Mixed Proxy" msgstr "" @@ -334,7 +342,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:798 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -350,15 +358,15 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:544 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:558 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:518 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:598 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" @@ -366,43 +374,43 @@ msgstr "" msgid "Enter the subscription URL to fetch proxy configurations from your provider" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:233 msgid "Every 1 minute" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:137 msgid "Every 12 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:135 msgid "Every 3 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:234 msgid "Every 3 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:133 msgid "Every 30 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:218 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:221 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:235 msgid "Every 5 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:136 msgid "Every 6 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:124 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:138 msgid "Every day" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:120 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:134 msgid "Every hour" msgstr "" @@ -414,7 +422,7 @@ msgstr "" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 msgid "Exclude servers by keyword" msgstr "" @@ -445,7 +453,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:756 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:770 msgid "Fully Routed IPs" msgstr "" @@ -457,11 +465,11 @@ msgstr "" msgid "Global check" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323 msgid "Global Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131 msgid "How often to automatically update the subscription" msgstr "" @@ -469,7 +477,7 @@ msgstr "" msgid "HTTP error" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:143 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:157 msgid "Include servers by keyword" msgstr "" @@ -664,8 +672,9 @@ msgstr "" msgid "Invalid Trojan URL: parsing failed" msgstr "" -#: src/validators/validateUrl.ts:8 -#: src/validators/validateUrl.ts:31 +#: src/validators/validateUrl.ts:49 +#: src/validators/validateUrl.ts:66 +#: src/validators/validateUrl.ts:78 msgid "Invalid URL format" msgstr "" @@ -714,7 +723,7 @@ msgstr "" msgid "Issues detected" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:144 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:158 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" @@ -735,11 +744,11 @@ msgstr "" msgid "List Update Frequency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:664 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:678 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:687 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701 msgid "Local Subnet Lists" msgstr "" @@ -759,7 +768,7 @@ msgstr "" msgid "Memory Usage" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:796 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:810 msgid "Mixed Proxy Port" msgstr "" @@ -767,7 +776,7 @@ msgstr "" msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:255 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -787,7 +796,7 @@ msgstr "" msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:340 msgid "Network Interface" msgstr "" @@ -824,7 +833,7 @@ msgstr "" msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:318 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332 msgid "Only one section can be global at a time." msgstr "" @@ -885,19 +894,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:451 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:710 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:724 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:733 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:747 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:811 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:825 msgid "Resolve real IP for routing" msgstr "" @@ -905,7 +914,7 @@ msgstr "" msgid "Restart NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:310 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 msgid "Route all unmatched traffic through this section's outbound." msgstr "" @@ -949,7 +958,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:470 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:484 msgid "Russia inside restrictions" msgstr "" @@ -961,7 +970,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432 msgid "Select a predefined list for routing" msgstr "" @@ -981,11 +990,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:327 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:341 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:410 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25 msgid "Select or enter DNS server address" msgstr "" @@ -998,15 +1007,15 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:383 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:397 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:506 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:520 msgid "Select the list type for adding custom domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:586 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:600 msgid "Select the list type for adding custom subnets" msgstr "" @@ -1030,7 +1039,7 @@ msgstr "" msgid "Selector" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:165 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 msgid "Selector Proxy Links" msgstr "" @@ -1095,20 +1104,20 @@ msgstr "" msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:757 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:771 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:711 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:725 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:748 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:688 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:679 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1124,7 +1133,7 @@ msgstr "" msgid "Subscription" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:130 msgid "Subscription Update Interval" msgstr "" @@ -1164,8 +1173,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:510 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:524 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:604 msgid "Text List" msgstr "" @@ -1173,18 +1182,22 @@ msgstr "" msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:244 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:262 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:276 msgid "The URL used to test server connectivity" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121 +msgid "This is a security trade-off: an attacker could intercept the fetch." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:465 msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." msgstr "" @@ -1213,12 +1226,12 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:313 msgid "UDP over TCP" msgstr "" @@ -1270,7 +1283,7 @@ msgstr "" msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" -#: src/validators/validateUrl.ts:17 +#: src/validators/validateUrl.ts:58 msgid "URL must use one of the following protocols:" msgstr "" @@ -1278,51 +1291,55 @@ msgstr "" msgid "URLTest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:215 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:190 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:261 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:275 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243 msgid "URLTest Tolerance" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119 +msgid "Use only for IP-host panels that serve an invalid or self-signed certificate." +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:486 msgid "Use this only when the router has working IPv6 connectivity." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:330 msgid "Use with Exclusion sections to route specific domains directly." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:505 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:519 msgid "User Domain List Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:517 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:531 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:543 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:557 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:585 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:599 msgid "User Subnet List Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:597 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:623 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:637 msgid "User Subnets List" msgstr "" @@ -1342,14 +1359,14 @@ msgstr "" #: src/validators/validateSubnet.ts:30 #: src/validators/validateSubnet.ts:52 #: src/validators/validateTrojanUrl.ts:59 -#: src/validators/validateUrl.ts:28 +#: src/validators/validateUrl.ts:75 #: src/validators/validateVlessUrl.ts:108 #: src/validators/validateVmessUrl.ts:86 msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:655 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:669 msgid "Validation errors:" msgstr "" @@ -1367,20 +1384,20 @@ msgid "Visit Wiki" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:38 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:191 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:453 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:486 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:312 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326 msgid "When enabled, traffic not matching any other section's lists will go through this proxy." msgstr "" @@ -1396,10 +1413,10 @@ msgstr "" msgid "You can select Output Network Interface, by default autodetect" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:145 msgid "Группировать по странам" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:132 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:146 msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" msgstr "" diff --git a/netshift/files/etc/config/netshift b/netshift/files/etc/config/netshift index ff30f208..98b29715 100644 --- a/netshift/files/etc/config/netshift +++ b/netshift/files/etc/config/netshift @@ -48,6 +48,10 @@ config section 'main' # option connection_type 'proxy' # option proxy_config_type 'subscription' # option subscription_url 'https://example.com/api/sub' +# # Allow insecure TLS for the subscription fetch (default 0). Set to 1 to +# # add wget --no-check-certificate for IP-host panels whose HTTPS cert is +# # invalid/self-signed/missing-SAN. Disables certificate verification. +# #option subscription_insecure '0' # option subscription_update_interval '1h' # #option subscription_group_by_countries '0' # #option urltest_check_interval '3m' diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 4ad6fa7b..ad92bdf4 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -388,6 +388,7 @@ download_subscription_into_cache() { local tmpfile persist_tmpfile url_tmpfile rejected_cache_path tmp_hash rejected_hash validation_reason file_size fallback_tmp local configured_user_agent user_agent_cache_path cached_user_agent candidates_file local effective_user_agent download_ok winning_user_agent ua_tmpfile + local subscription_insecure ensure_subscription_cache_dir || { log "Failed to prepare persistent subscription cache directory '$SUBSCRIPTION_CACHE_FOLDER' for section '$section'" "error" @@ -416,6 +417,15 @@ download_subscription_into_cache() { return 11 fi + # Opt-in per-section insecure TLS fetch (default OFF = secure). When enabled + # download_subscription adds wget --no-check-certificate so IP-host panels + # with invalid/self-signed/missing-SAN certs work. Log once (redacted). + subscription_insecure="$(uci -q get "netshift.${section}.subscription_insecure" 2>/dev/null)" + [ -n "$subscription_insecure" ] || subscription_insecure=0 + if [ "$subscription_insecure" = "1" ]; then + log "Subscription fetch for section '$section' uses --no-check-certificate (TLS verification disabled): url=$(redact_url_for_log "$subscription_url")" "warn" + fi + download_ok=0 winning_user_agent="" fallback_tmp="${tmpfile}.fb" @@ -426,7 +436,7 @@ download_subscription_into_cache() { log "Trying subscription User-Agent for section '$section': $effective_user_agent" "info" - if ! download_subscription "$subscription_url" "$tmpfile" "$service_proxy_address" 3 2 10 "$effective_user_agent"; then + if ! download_subscription "$subscription_url" "$tmpfile" "$service_proxy_address" 3 2 10 "$effective_user_agent" "$subscription_insecure"; then log "Subscription download failed for section '$section' with User-Agent '$effective_user_agent'; trying next candidate" "warn" continue fi diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index e940502f..d0cbeef4 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -762,6 +762,43 @@ $candidate done } +# Runs a single wget subscription request with the shared client-mimicking +# headers and an optional --no-check-certificate flag, so all branches of +# download_subscription stay byte-identical. +# Arguments: +# $1 - cert flag ("" or "--no-check-certificate") +# $2 - User-Agent header value +# $3 - X-HWID header value +# $4 - X-Device-Model header value +# $5 - X-Ver-OS header value +# $6 - output file path (passed to wget -O) +# $7 - error file path (wget stderr is redirected here) +# $8 - subscription URL +# $9.. - leading wget flags (e.g. -4, -T, <timeout>) +# Caller is responsible for exporting http_proxy/https_proxy when needed. +_wget_subscription_request() { + local cert_flag="$1" + local req_user_agent="$2" + local req_hwid="$3" + local req_device_model="$4" + local req_kernel_version="$5" + local req_outfile="$6" + local req_errfile="$7" + local req_url="$8" + shift 8 + + # shellcheck disable=SC2086 + wget $cert_flag "$@" -O "$req_outfile" \ + --header "User-Agent: $req_user_agent" \ + --header "X-HWID: $req_hwid" \ + --header "X-Device-OS: OpenWrt Linux" \ + --header "X-Device-Model: $req_device_model" \ + --header "X-Ver-OS: $req_kernel_version" \ + --header "Accept-Language: ru-RU,en,*" \ + --header "X-Device-Locale: EN" \ + "$req_url" 2>"$req_errfile" +} + # Downloads a subscription body from the given URL with client-mimicking headers # Arguments: # $1 - subscription URL @@ -771,6 +808,7 @@ $candidate # $5 - wait between retries (optional, default 2) # $6 - timeout seconds (optional, default 10) # $7 - User-Agent (optional; default "singbox/<version>") +# $8 - insecure (optional, default 0; when 1 adds --no-check-certificate) download_subscription() { local url="$1" local filepath="$2" @@ -779,6 +817,7 @@ download_subscription() { local wait="${5:-2}" local timeout="${6:-10}" local user_agent="${7:-}" + local insecure="${8:-0}" local sb_version device_model kernel_version hwid sb_version="$(get_sing_box_version)" @@ -787,6 +826,14 @@ download_subscription() { hwid="$(generate_hwid)" [ -n "$user_agent" ] || user_agent="$(get_subscription_user_agent)" + # Optional TLS-verification bypass for IP-host panels with broken certs. + # Empty string keeps the secure default; word-splitting it into the wget + # argv (via _wget_subscription_request) yields zero extra args when off. + local cert_flag="" + if [ "$insecure" = "1" ]; then + cert_flag="--no-check-certificate" + fi + local tmpfile errfile rc family tmpfile="${filepath}.part.$$" errfile="${filepath}.err.$$" @@ -798,48 +845,24 @@ download_subscription() { family="ipv4" if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ - wget -4 -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: $user_agent" \ - --header "X-HWID: $hwid" \ - --header "X-Device-OS: OpenWrt Linux" \ - --header "X-Device-Model: $device_model" \ - --header "X-Ver-OS: $kernel_version" \ - --header "Accept-Language: ru-RU,en,*" \ - --header "X-Device-Locale: EN" \ - "$url" 2>"$errfile" + _wget_subscription_request "$cert_flag" "$user_agent" "$hwid" \ + "$device_model" "$kernel_version" "$tmpfile" "$errfile" "$url" \ + -4 -T "$timeout" else - wget -4 -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: $user_agent" \ - --header "X-HWID: $hwid" \ - --header "X-Device-OS: OpenWrt Linux" \ - --header "X-Device-Model: $device_model" \ - --header "X-Ver-OS: $kernel_version" \ - --header "Accept-Language: ru-RU,en,*" \ - --header "X-Device-Locale: EN" \ - "$url" 2>"$errfile" + _wget_subscription_request "$cert_flag" "$user_agent" "$hwid" \ + "$device_model" "$kernel_version" "$tmpfile" "$errfile" "$url" \ + -4 -T "$timeout" fi else if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ - wget -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: $user_agent" \ - --header "X-HWID: $hwid" \ - --header "X-Device-OS: OpenWrt Linux" \ - --header "X-Device-Model: $device_model" \ - --header "X-Ver-OS: $kernel_version" \ - --header "Accept-Language: ru-RU,en,*" \ - --header "X-Device-Locale: EN" \ - "$url" 2>"$errfile" + _wget_subscription_request "$cert_flag" "$user_agent" "$hwid" \ + "$device_model" "$kernel_version" "$tmpfile" "$errfile" "$url" \ + -T "$timeout" else - wget -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: $user_agent" \ - --header "X-HWID: $hwid" \ - --header "X-Device-OS: OpenWrt Linux" \ - --header "X-Device-Model: $device_model" \ - --header "X-Ver-OS: $kernel_version" \ - --header "Accept-Language: ru-RU,en,*" \ - --header "X-Device-Locale: EN" \ - "$url" 2>"$errfile" + _wget_subscription_request "$cert_flag" "$user_agent" "$hwid" \ + "$device_model" "$kernel_version" "$tmpfile" "$errfile" "$url" \ + -T "$timeout" fi fi @@ -866,25 +889,13 @@ download_subscription() { log "Retrying subscription download over IPv4-only" "warn" if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \ - wget -4 -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: $user_agent" \ - --header "X-HWID: $hwid" \ - --header "X-Device-OS: OpenWrt Linux" \ - --header "X-Device-Model: $device_model" \ - --header "X-Ver-OS: $kernel_version" \ - --header "Accept-Language: ru-RU,en,*" \ - --header "X-Device-Locale: EN" \ - "$url" 2>"$errfile" + _wget_subscription_request "$cert_flag" "$user_agent" "$hwid" \ + "$device_model" "$kernel_version" "$tmpfile" "$errfile" "$url" \ + -4 -T "$timeout" else - wget -4 -T "$timeout" -O "$tmpfile" \ - --header "User-Agent: $user_agent" \ - --header "X-HWID: $hwid" \ - --header "X-Device-OS: OpenWrt Linux" \ - --header "X-Device-Model: $device_model" \ - --header "X-Ver-OS: $kernel_version" \ - --header "Accept-Language: ru-RU,en,*" \ - --header "X-Device-Locale: EN" \ - "$url" 2>"$errfile" + _wget_subscription_request "$cert_flag" "$user_agent" "$hwid" \ + "$device_model" "$kernel_version" "$tmpfile" "$errfile" "$url" \ + -4 -T "$timeout" fi rc=$? if [ "$rc" -eq 0 ] && [ -s "$tmpfile" ]; then diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 135dbe6f..9c67c636 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -9,9 +9,9 @@ # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, -# nftv6, diagnostics, subscription, rejected, jobstate, -# selfheal, dnsdetour, globalproxy, stablecheck, extcheck, -# selfupdate +# nftv6, diagnostics, subscription, insecure, rejected, +# jobstate, selfheal, dnsdetour, globalproxy, stablecheck, +# extcheck, selfupdate # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index ac41f9cc..ecf7ffad 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1541,6 +1541,151 @@ FBEOF rm -f "$fb" } +# ───────────────────────────────────────────────────────────────── +# Test: Insecure subscription fetch flag (task-021b) +# +# Exercises download_subscription's 8th positional arg (insecure 0|1). A +# PATH-prepended fake `wget` records its full argv to a log and writes a dummy +# body to its -O target (so the FIRST attempt succeeds → no retry/fallback). +# A driver sources the REAL helpers.sh (real download_subscription + +# _wget_subscription_request), stubs the metadata/logging helpers, and pins +# should_force_wget_ipv4 per scenario to drive the normal vs ipv4 branch. We +# assert --no-check-certificate is ABSENT when insecure=0 and PRESENT when +# insecure=1, across the normal and proxy branches (plus the ipv4 branch). +# Tokens use the same name:OK/FAIL convention as test_subscription. +# ───────────────────────────────────────────────────────────────── +test_insecure_fetch() { + header "Insecure Subscription Fetch Flag (task-021b)" + + local helpers="${NETSHIFT_LIB_DIR}/helpers.sh" + if [ ! -r "$helpers" ]; then + skip "helpers.sh not found in ${NETSHIFT_LIB_DIR}" + return + fi + + local work="/tmp/netshift-insecure-$$" + rm -rf "$work" + mkdir -p "$work/bin" + + # Fake wget: append the FULL argv to $WGET_ARGV_LOG (one line, NUL-free), + # then satisfy download_subscription's success check by writing a non-empty + # body to whatever follows -O. Always exit 0 so the first attempt wins. + cat > "$work/bin/wget" << 'WGETEOF' +#!/bin/sh +# Record argv as a single space-joined line for substring assertions. +printf '%s\n' "$*" >> "$WGET_ARGV_LOG" +# Find the -O target and write a dummy body there. +out="" +prev="" +for a in "$@"; do + [ "$prev" = "-O" ] && { out="$a"; break; } + prev="$a" +done +[ -n "$out" ] && printf 'dummy-body' > "$out" +exit 0 +WGETEOF + chmod 0755 "$work/bin/wget" + + local drv="$work/driver.sh" + cat > "$drv" << 'IFEOF' +# Quiet logging + deterministic metadata stubs (no real device probing). +log() { :; } +echolog() { :; } +nolog() { :; } +get_sing_box_version() { echo "1.12.0"; } +get_device_model() { echo "test-model"; } +get_kernel_version() { echo "test-kernel"; } +generate_hwid() { echo "test-hwid"; } +get_subscription_user_agent() { echo "singbox/test"; } + +# Real download_subscription + _wget_subscription_request from helpers.sh. +. "HELPERS_PATH" + +# Scenario knobs: $1 = branch (normal|ipv4), rest of the call is fixed. +case "$1" in +ipv4) should_force_wget_ipv4() { return 0; } ;; +*) should_force_wget_ipv4() { return 1; } ;; +esac +# IPv4 fallback retry helpers — keep them inert so a success on attempt 1 is +# unambiguous (the fake wget always succeeds anyway). +has_ipv4_default_route() { return 1; } +wget_supports_ipv4_flag() { return 1; } + +branch="$1" +proxy="$2" +insecure="$3" +out="$WGET_OUT_FILE" +rm -f "$out" +: > "$WGET_ARGV_LOG" + +# url, tmpfile, proxy, retries=1, wait=0, timeout=5, user_agent, insecure +download_subscription "https://1.2.3.4:2096/sub/abc" "$out" "$proxy" 1 0 5 "singbox/test" "$insecure" +echo "DONE" +IFEOF + sed -i "s|HELPERS_PATH|$helpers|g" "$drv" + + export WGET_ARGV_LOG="$work/wget.argv" + export WGET_OUT_FILE="$work/sub.json" + + # Helper: run one scenario, return the recorded argv on stdout. + _if_run() { + : > "$WGET_ARGV_LOG" + PATH="$work/bin:$PATH" ash "$drv" "$1" "$2" "$3" > /dev/null 2>&1 + cat "$WGET_ARGV_LOG" 2>/dev/null + } + + local argv + + # ── normal branch, insecure=0 → NO --no-check-certificate ── + argv="$(_if_run normal "" 0)" + case "$argv" in + *--no-check-certificate*) fail "if-normal-off: flag present (should be absent): $argv" ;; + *) pass "if-normal-off: no --no-check-certificate (secure default)" ;; + esac + + # ── normal branch, insecure=1 → HAS --no-check-certificate ── + argv="$(_if_run normal "" 1)" + case "$argv" in + *--no-check-certificate*) pass "if-normal-on: --no-check-certificate present" ;; + *) fail "if-normal-on: flag missing (should be present): $argv" ;; + esac + + # ── proxy branch, insecure=0 → NO --no-check-certificate ── + argv="$(_if_run normal "127.0.0.1:4534" 0)" + case "$argv" in + *--no-check-certificate*) fail "if-proxy-off: flag present (should be absent): $argv" ;; + *) pass "if-proxy-off: no --no-check-certificate (secure default)" ;; + esac + + # ── proxy branch, insecure=1 → HAS --no-check-certificate ── + argv="$(_if_run normal "127.0.0.1:4534" 1)" + case "$argv" in + *--no-check-certificate*) pass "if-proxy-on: --no-check-certificate present" ;; + *) fail "if-proxy-on: flag missing (should be present): $argv" ;; + esac + + # ── ipv4 branch, insecure=1 → HAS both -4 and --no-check-certificate ── + argv="$(_if_run ipv4 "" 1)" + case "$argv" in + *--no-check-certificate*) + case "$argv" in + *-4*) pass "if-ipv4-on: -4 and --no-check-certificate both present" ;; + *) fail "if-ipv4-on: -4 missing: $argv" ;; + esac + ;; + *) fail "if-ipv4-on: flag missing (should be present): $argv" ;; + esac + + # ── ipv4 branch, insecure=0 → -4 present, NO --no-check-certificate ── + argv="$(_if_run ipv4 "" 0)" + case "$argv" in + *--no-check-certificate*) fail "if-ipv4-off: flag present (should be absent): $argv" ;; + *) pass "if-ipv4-off: no --no-check-certificate (secure default)" ;; + esac + + rm -rf "$work" +} + # ───────────────────────────────────────────────────────────────── # Test: Async component-action job state (updater.sh) # ───────────────────────────────────────────────────────────────── @@ -3126,6 +3271,7 @@ main() { test_nft_ipv6 test_diagnostics test_subscription + test_insecure_fetch test_rejected_hash test_jobstate test_selfheal @@ -3143,6 +3289,7 @@ main() { nftv6) test_nft_ipv6 ;; diagnostics) test_diagnostics ;; subscription) test_subscription ;; + insecure) test_insecure_fetch ;; rejected) test_rejected_hash ;; jobstate) test_jobstate ;; selfheal) test_selfheal ;; @@ -3156,7 +3303,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck selfupdate" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck selfupdate" exit 1 ;; esac From 7ebdd96bcf210ba170f3f9d26b057ff812057c74 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Sun, 7 Jun 2026 10:49:21 +0300 Subject: [PATCH 59/75] =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B0=20=D0=BD=D0=B5=D1=81=D0=BA=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=BA=D0=B8=D1=85=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BE?= =?UTF-8?q?=D0=BA=20=D1=81=D1=80=D0=B0=D0=B7=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 49 ++ .../memory/luci-frontend-developer.md | 29 + .../memory/shell-backend-developer.md | 78 +++ fe-app-netshift/locales/calls.json | 18 +- fe-app-netshift/locales/netshift.pot | 14 +- fe-app-netshift/locales/netshift.ru.po | 14 +- fe-app-netshift/src/netshift/types.ts | 2 +- .../resources/view/netshift/section.js | 8 +- luci-app-netshift/po/ru/netshift.po | 14 +- luci-app-netshift/po/templates/netshift.pot | 14 +- netshift/files/etc/config/netshift | 7 + netshift/files/usr/bin/netshift | 621 ++++++++++++------ netshift/files/usr/lib/constants.sh | 6 + tests/entrypoint.sh | 292 ++++++++ 14 files changed, 937 insertions(+), 229 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 4f42ec05..691c56fc 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -408,3 +408,52 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> smoke all = 101 passed / 0 failed (84 -> +17 new: stablecheck x4 + selfupdate x13). Both layers code-reviewer APPROVED (backend 1st pass; frontend after a C1/S1 fix round). Ready for human commit. + +## Multi-URL subscriptions (task-022 backend + task-023 frontend, 2026-06-07) + +- FEATURE: a subscription section may now list MULTIPLE `subscription_url` feeds + (UI "+"/add-another-field). Backend downloads each independently, merges all + usable feeds' nodes into ONE node set driving the section's single + selector/urltest group. Operator decisions (all "recommended"): UCI list + + NO migration (lone legacy `option` reads as 1-element list via + config_list_foreach, same as community_lists); per-URL hashed cache key + `${section}.<md5(url)>.{json,url,rejected,user_agent}`; best-effort merge + (section available if >=1 feed yields outbounds, unavailable only if ALL fail); + reuse the existing facade global tag-dedup (-2/-3) for same-named nodes across + feeds; keyword filter + country grouping apply to the MERGED set. +- BACKEND approach that WON: build ONE merged subscription JSON (concat each + usable cache's proxy `.outbounds[]` via --slurpfile, no Oniguruma) and call + `sing_box_cf_add_subscription_outbounds` ONCE on it. This reuses the facade's + keyword-filter + global dedup + per-batch `sing-box check` bisection + + selector/urltest/country-group builder UNCHANGED. The facade RESETS its public + globals (SUBSCRIPTION_OUTBOUND_TAGS_JSON etc.) every call, so a per-feed loop + would force hand-accumulation of the tag union — strictly more code, same + result. Always-hash (even single URL) + `reap_legacy_subscription_cache_files` + for the stale bare `${section}.<ext>` files = uniform path, no single-vs-multi + branch bug. Rejected-hash kept PER-URL so one bad feed can't poison another. +- FRONTEND: trivial — `subscription_url` form.Value -> form.DynamicList modelled + byte-for-byte on `remote_domain_lists` (per-row main.validateUrl, rmempty=true); + types.ts string->string[]; locales actualized (fe<->luci byte-identical, ru + filled). NOTHING in the FE reads subscription_url back, so the TS type is erased + at runtime -> `yarn build` produces NO main.js diff (correct, not a missed + rebuild). FE code-reviewer APPROVED first pass. +- GATES: shellcheck (error) clean; smoke `all` = 110 passed / 0 failed (was 101; + +9 net from the new mu-case1..6 subscription assertions — see M1 below for the + counter quirk); vitest 471 passed; tsup build idempotent, main.js no diff; no + yarn pollution. Backend APPROVED WITH CONDITIONS (the sole condition = run full + smoke `all`, which I did = the §4 whole-chain check). Frontend APPROVED. +- LANDMINE (verifying FE lint myself): the repo `yarn lint` script is + `eslint src --ext .ts,.tsx` — SCOPED TO src/. Running a bare `eslint .` from + fe-app-netshift lints the ROOT locale scripts (distribute-locales.js, + extract-calls.js, generate-po.js/pot.js) which have pre-existing no-undef + (console/process) errors and are NOT in the gate scope and NOT touched by FE + tasks. Always verify FE lint with `eslint src --ext .ts,.tsx --max-warnings=0`, + never `eslint .` — the latter is a false-alarm generator. +- M1 (smoke harness, confirmed by reviewer): the `subscription` category parses + `mu-*`/token results in a `sh "$x" | while read; do pass/fail; done` PIPE, so + pass/fail run in a SUBSHELL and DON'T propagate to summary()'s PASS/FAIL globals + -> the per-✓ marks are truth, but a `:FAIL` token prints red WITHOUT failing the + suite count. Pre-existing project-wide convention (cm/sb/jobstate/selfheal/...), + NOT a task-022 defect. When a smoke category uses this pattern, trust the ✓/✗ + marks, not just the "Results: N passed" line; a real gating test needs + `done < tmpfile`. diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 302b0846..32fa6ed9 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -422,3 +422,32 @@ append findings; keep under ~200 lines. build); verified yarn.lock unchanged + NO `.yarn`/`.yarnrc.yml`. The `netshift/files/**` + `tests/**` changes in git status are 021b (other agent), not mine. + +## subscription_url → form.DynamicList (multi-URL) (task-023) + +- Converted `subscription_url` from `form.Value` to `form.DynamicList` in + section.js (~88-111), modelled EXACTLY on `remote_domain_lists` (:721-742): + same per-row validate (`!value||value.length===0 → true`, else + `main.validateUrl(value)`), `rmempty=true` (was `false`; the empty-row guard + already short-circuited so emptiness was never enforced; backend keeps the + "no URL" guard). Kept option name `subscription_url`, depends + `{connection_type:'proxy',proxy_config_type:'subscription'}`, placeholder + `https://example.com/api/sub`. Title → plural `_("Subscription URLs")`; + description → single literal `_("Add one or more subscription URLs to fetch + proxy configurations from. All feeds are downloaded and merged.")`. +- types.ts:120 `subscription_url: string` → `string[]` (kept required, matches + sibling list fields `selector_proxy_links`/`urltest_proxy_links`). +- PURE TYPE-ONLY CHANGE: nothing in the FE reads `subscription_url` back (verified + repo-wide) → `tsup` build produced ZERO main.js diff (confirmed via + `git diff --exit-code main.js`). This is correct, NOT a missed rebuild. Still + ran the build to confirm. (Same lesson as the type-only note in i18n section.) +- locales: `node {extract-calls,generate-pot,generate-po ru,distribute-locales}.js` + (NOT yarn → no corepack). msgid delta = clean SWAP: removed "Subscription URL" + + "Enter the subscription URL...provider"; added "Subscription URLs" + the new + merged-feeds description. Filled 2 ru msgstr in SOURCE locales/netshift.ru.po + ("URL подписок" / "Добавьте один или несколько URL подписок...объединяются.") + then distribute → po/ru + po/templates byte-identical to source (verified via + diff). Only header msgstr empty (line 7). 5 catalog files touched: calls.json, + locales/netshift.{pot,ru.po}, po/{templates/netshift.pot,ru/netshift.po}. +- yarn classic 1.22.22 again but ran inner gate via node_modules/.bin + (prettier/eslint/vitest/tsup) to be safe; yarn.lock unchanged, no .yarn/.yarnrc. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 6110206e..5e051737 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -651,3 +651,81 @@ findings; keep under ~200 lines. Registered all 5 points (all)/case alias/usage line/docker-compose comment). shellcheck -S error clean; `smoke-tests all` = 110 passed / 0 failed (104 baseline + 6 new); UTF-8/LF intact. Additive, NO runtime-contract change. + +## task-022: multiple subscription_url feeds per section (merge pipeline) + +- **subscription_url is now a UCI list.** Back-compat verified: a lone legacy + `option subscription_url` is read by `config_list_foreach` as a 1-element list + (same as community_lists) — NO migration code. New collector + `get_subscription_urls_for_section` resets global `SUBSCRIPTION_URLS_COLLECTED`, + runs `config_list_foreach "$section" "subscription_url" _collect_..._handler`, + prints newline-delimited URLs. URLs are opaque user text → ALWAYS + newline-delimited + `while IFS= read -r` from a temp file, NEVER word-split. +- **Per-URL cache keying = ALWAYS hash (recommendation ii).** `get_subscription_url_hash` + = `printf '%s' "$url" | md5sum | awk '{print $1}'`. The 4 cache-path builders + gained an OPTIONAL 2nd arg `urlhash`: `${section}${urlhash:+.$urlhash}.<ext>` + — present = hashed `${section}.<hash>.<ext>`, absent = legacy bare path (kept + only for the tmp-migration source + the reaper). New + `reap_legacy_subscription_cache_files "$section"` rm's the 4 bare files; called + at the top of startup/refresh/config-gen so a stale bare body is never read. + `subscription_cache_is_usable` derives rejected via `${json%.json}.rejected` + which maps correctly onto the hashed path (no change needed there). +- **download_subscription_into_cache gained a 6th positional `urlhash`** used for + the UA + rejected cache paths (json/url paths are already passed in as $3/$4). +- **Merge-file approach (primary, chosen — NOT the per-feed-loop fallback).** In + the config-gen `subscription)` branch: (1) per-feed best-effort download loop + (one dead feed never aborts others); (2) concat every `subscription_cache_is_usable` + feed's PROXY `.outbounds[]` into one temp `{"outbounds":[...]}` under + `TMP_SUBSCRIPTION_MERGE_FOLDER` (new constant) via + `jq -c --slurpfile feed '... .outbounds += [ $feed[0].outbounds[]? | select(not selector/urltest/direct/dns/block) ]'` + (NO Oniguruma — pure array concat); (3) call + `sing_box_cf_add_subscription_outbounds` ONCE on the merged file. WHY merge-file: + the facade RESETS its public globals every call (`:757-760`) AND seeds the + dedup `used`-set from tags already in `$config`, so a single call over the + union reuses the keyword filter + GLOBAL tag-dedup (auto `-1`/`-2`…) + per-batch + `sing-box check` bisection + the country-group/selector builder UNCHANGED. The + per-feed-loop fallback would force me to re-accumulate SUBSCRIPTION_OUTBOUND_TAGS_JSON + by hand (facade resets it each call) — strictly more code for the same result. +- **Dedup suffix is `-1` then `-2`** (facade loops `range(1; ...)` → + `$base + "-" + n`), so two same-named nodes become `X` and `X-1` (NOT `X-2`). + Assert base + any `startswith("X-")`, not a specific number. +- **Best-effort semantics:** section "available" iff merged set has ≥1 proxy + (`usable_feed_count>0 && merged_node_count>0`); else + `mark_subscription_outbound_unavailable "$section" "$kw_filter_active"` as + before. Startup/refresh: section "changed"→restart if ANY feed changed (rc 0); + section "failed" only if NO feed usable AND none changed. Per-feed rc 2 + (unchanged) counts as usable, not failed. +- **mark_subscription_outbound_unavailable is now per-URL** (memory landmine: + keep rejected-hash PER-URL so one bad feed can't poison another). It iterates + the section's URLs: keyword-filter case → `rm` each per-URL `.rejected` + (self-heal); genuine case → write each usable cached feed's md5 to its per-URL + `.rejected`. The section-level unavailable LIST + `subscription_startup_blocked` + stay section-level. +- **section_has_usable_subscription_cache** (new) returns 0 if ANY per-URL cache + is usable; replaces the single-json `subscription_cache_is_usable` checks in + `get_subscription_download_proxy_address`. Uses temp-file + `while read` (NOT a + pipe) so the `found` flag survives. +- **migrate_subscription_cache_from_tmp** now maps a bare tmp `<section>.json` + onto the hashed dest of the URL in its `.url` sidecar (else the section's first + configured URL) so a legacy single-feed upgrade keeps working w/o re-download. +- **Smoke landmine (test stub):** the REAL LuCI `config_list_foreach` iterates in + the CURRENT shell (no pipe) so the callback mutates accumulator globals. A test + stub that pipes `printf | while` subshell-traps the mutation → collector + returns empty. Stub MUST use temp-file + `while read`. Likewise the facade sets + globals (SUBSCRIPTION_OUTBOUND_TAGS*) — call it `>/dev/null` (like the real + bin) and read `$SING_BOX_CF_LAST_CONFIG`, NOT `out=$(facade ...)` (the `$()` + subshell drops the globals). Both cost a debug cycle. +- Extended `test_subscription` with the 6 spec cases (no registration change — + `subscription` already in `all)`): mu-case1 multi-URL merge (count+both feeds+ + tags-json), mu-case2 same-name dedup (no dup tags + suffix present), mu-case3 + partial best-effort (A usable/B invalid → still available, not unavailable), + mu-case4 all-fail→unavailable, mu-case5 cache-key isolation (distinct + `${section}.<hash>.json` + per-URL `.rejected`), mu-case6 single-option + back-compat (1-elem list + working config). Driver awk-extracts the shipped + path/hash/collector/cache-usable/mark-unavailable fns + mirrors the inline + merge jq; feeds are stock shadowsocks so the live `sing-box check` accepts them. +- shellcheck -S error clean (bin+libs+install.sh); `smoke-tests all` = 110 passed + / 0 failed (per-test ✓ marks are truth: 12 green mu-case marks; suite total + unchanged due to the documented piped-while subshell counter quirk). UTF-8 + intact. New constant `TMP_SUBSCRIPTION_MERGE_FOLDER`; UCI example documents the + `list subscription_url` form. NO sacred constant/port/mark/path changed. diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index e296df4e..f49986cf 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -34,6 +34,13 @@ "src/netshift/tabs/dashboard/initController.ts:307" ] }, + { + "call": "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged.", + "key": "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92" + ] + }, { "call": "Additional marking rules found", "key": "Additional marking rules found", @@ -605,13 +612,6 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612" ] }, - { - "call": "Enter the subscription URL to fetch proxy configurations from your provider", - "key": "Enter the subscription URL to fetch proxy configurations from your provider", - "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92" - ] - }, { "call": "Every 1 minute", "key": "Every 1 minute", @@ -1919,8 +1919,8 @@ ] }, { - "call": "Subscription URL", - "key": "Subscription URL", + "call": "Subscription URLs", + "key": "Subscription URLs", "places": [ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:91" ] diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index 034131a9..3c8bcd91 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 21:53+0300\n" -"PO-Revision-Date: 2026-06-06 21:53+0300\n" +"POT-Creation-Date: 2026-06-07 07:23+0300\n" +"PO-Revision-Date: 2026-06-07 07:23+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -36,6 +36,10 @@ msgstr "" msgid "Active Connections" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92 +msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 msgid "Additional marking rules found" msgstr "" @@ -370,10 +374,6 @@ msgstr "" msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92 -msgid "Enter the subscription URL to fetch proxy configurations from your provider" -msgstr "" - #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:233 msgid "Every 1 minute" msgstr "" @@ -1138,7 +1138,7 @@ msgid "Subscription Update Interval" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:91 -msgid "Subscription URL" +msgid "Subscription URLs" msgstr "" #: src/helpers/copyToClipboard.ts:10 diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index b18932de..4377ec3a 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 00:53+0300\n" -"PO-Revision-Date: 2026-06-06 00:53+0300\n" +"POT-Creation-Date: 2026-06-07 10:23+0300\n" +"PO-Revision-Date: 2026-06-07 10:23+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -32,6 +32,9 @@ msgstr "✘ Остановлен" msgid "Active Connections" msgstr "Активные соединения" +msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." +msgstr "Добавьте один или несколько URL подписок для получения конфигураций прокси. Все источники загружаются и объединяются." + msgid "Additional marking rules found" msgstr "Найдены дополнительные правила маркировки" @@ -269,9 +272,6 @@ msgstr "Введите доменные имена без протоколов, msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса" -msgid "Enter the subscription URL to fetch proxy configurations from your provider" -msgstr "Введите URL подписки для получения конфигураций прокси от вашего провайдера" - msgid "Every 1 minute" msgstr "Каждую минуту" @@ -815,8 +815,8 @@ msgstr "Подписка" msgid "Subscription Update Interval" msgstr "Интервал обновления подписки" -msgid "Subscription URL" -msgstr "URL подписки" +msgid "Subscription URLs" +msgstr "URL подписок" msgid "Successfully copied!" msgstr "Успешно скопировано!" diff --git a/fe-app-netshift/src/netshift/types.ts b/fe-app-netshift/src/netshift/types.ts index df2373f7..0a296e11 100644 --- a/fe-app-netshift/src/netshift/types.ts +++ b/fe-app-netshift/src/netshift/types.ts @@ -117,7 +117,7 @@ export namespace NetShift { export interface ConfigProxySubscriptionSection { connection_type: 'proxy'; proxy_config_type: 'subscription'; - subscription_url: string; + subscription_url: string[]; subscription_update_interval?: string; subscription_group_by_countries?: '0' | '1'; subscription_filter_include_keywords?: string[]; diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index a7bc9f1d..232da717 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -86,16 +86,16 @@ function createSectionContent(section) { }; o = section.option( - form.Value, + form.DynamicList, "subscription_url", - _("Subscription URL"), + _("Subscription URLs"), _( - "Enter the subscription URL to fetch proxy configurations from your provider", + "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged.", ), ); o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o.placeholder = "https://example.com/api/sub"; - o.rmempty = false; + o.rmempty = true; o.validate = function (section_id, value) { if (!value || value.length === 0) { return true; diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index b18932de..4377ec3a 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 00:53+0300\n" -"PO-Revision-Date: 2026-06-06 00:53+0300\n" +"POT-Creation-Date: 2026-06-07 10:23+0300\n" +"PO-Revision-Date: 2026-06-07 10:23+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -32,6 +32,9 @@ msgstr "✘ Остановлен" msgid "Active Connections" msgstr "Активные соединения" +msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." +msgstr "Добавьте один или несколько URL подписок для получения конфигураций прокси. Все источники загружаются и объединяются." + msgid "Additional marking rules found" msgstr "Найдены дополнительные правила маркировки" @@ -269,9 +272,6 @@ msgstr "Введите доменные имена без протоколов, msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса" -msgid "Enter the subscription URL to fetch proxy configurations from your provider" -msgstr "Введите URL подписки для получения конфигураций прокси от вашего провайдера" - msgid "Every 1 minute" msgstr "Каждую минуту" @@ -815,8 +815,8 @@ msgstr "Подписка" msgid "Subscription Update Interval" msgstr "Интервал обновления подписки" -msgid "Subscription URL" -msgstr "URL подписки" +msgid "Subscription URLs" +msgstr "URL подписок" msgid "Successfully copied!" msgstr "Успешно скопировано!" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index 034131a9..3c8bcd91 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-06 21:53+0300\n" -"PO-Revision-Date: 2026-06-06 21:53+0300\n" +"POT-Creation-Date: 2026-06-07 07:23+0300\n" +"PO-Revision-Date: 2026-06-07 07:23+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -36,6 +36,10 @@ msgstr "" msgid "Active Connections" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92 +msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 msgid "Additional marking rules found" msgstr "" @@ -370,10 +374,6 @@ msgstr "" msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92 -msgid "Enter the subscription URL to fetch proxy configurations from your provider" -msgstr "" - #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:233 msgid "Every 1 minute" msgstr "" @@ -1138,7 +1138,7 @@ msgid "Subscription Update Interval" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:91 -msgid "Subscription URL" +msgid "Subscription URLs" msgstr "" #: src/helpers/copyToClipboard.ts:10 diff --git a/netshift/files/etc/config/netshift b/netshift/files/etc/config/netshift index 98b29715..91da2191 100644 --- a/netshift/files/etc/config/netshift +++ b/netshift/files/etc/config/netshift @@ -47,7 +47,14 @@ config section 'main' #config section 'subscription_example' # option connection_type 'proxy' # option proxy_config_type 'subscription' +# # subscription_url may be a single option (legacy, still supported) or a +# # UCI list of one or more feed URLs. With multiple URLs every feed is +# # downloaded/validated independently and all nodes are MERGED into one +# # selector/urltest group (same-named nodes auto-deduped). A dead feed +# # never breaks the others; the section is blocked only if ALL feeds fail. # option subscription_url 'https://example.com/api/sub' +# #list subscription_url 'https://example.com/api/sub' +# #list subscription_url 'https://another-panel.example/api/sub' # # Allow insecure TLS for the subscription fetch (default 0). Set to 1 to # # add wget --no-check-certificate for IP-host panels whose HTTPS cert is # # invalid/self-signed/missing-SAN. Disables certificate verification. diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index ad92bdf4..afc853d8 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -117,9 +117,10 @@ section_has_configured_outbound() { [ -n "$outbound_json" ] && return 0 ;; subscription) - local subscription_url - config_get subscription_url "$section" "subscription_url" - [ -n "$subscription_url" ] && return 0 + # subscription_url is now a UCI list (a lone legacy option reads as a + # 1-element list). The section has a configured outbound if at least + # one URL is present. + [ -n "$(get_subscription_urls_for_section "$section")" ] && return 0 ;; esac ;; @@ -149,22 +150,41 @@ has_outbound_section() { return $section_exists } +# Stable, filesystem-safe subkey for a subscription URL within a section. A +# section may now list MULTIPLE subscription_url entries; each feed keeps its +# own independent download/UA/rejected/json cache keyed by this hash so the +# feeds never collide and one bad feed cannot poison another. md5sum is +# busybox-safe and the URL bytes are opaque user text (never parsed here). +get_subscription_url_hash() { + local url="$1" + + printf '%s' "$url" | md5sum 2>/dev/null | awk '{print $1}' +} + +# Subscription cache-path builders. The optional second argument is the per-URL +# hash (see get_subscription_url_hash): when present the path is keyed +# "${section}.<urlhash>.<ext>"; when absent it falls back to the legacy bare +# "${section}.<ext>" path (still used by migrate_subscription_cache_from_tmp and +# the legacy-cleanup reaper). Multi-URL feeds always pass a hash. get_subscription_json_path() { local section="$1" + local urlhash="$2" - echo "$SUBSCRIPTION_CACHE_FOLDER/${section}.json" + echo "$SUBSCRIPTION_CACHE_FOLDER/${section}${urlhash:+.$urlhash}.json" } get_subscription_url_cache_path() { local section="$1" + local urlhash="$2" - echo "$SUBSCRIPTION_CACHE_FOLDER/${section}.url" + echo "$SUBSCRIPTION_CACHE_FOLDER/${section}${urlhash:+.$urlhash}.url" } get_subscription_rejected_cache_path() { local section="$1" + local urlhash="$2" - echo "$SUBSCRIPTION_CACHE_FOLDER/${section}.rejected" + echo "$SUBSCRIPTION_CACHE_FOLDER/${section}${urlhash:+.$urlhash}.rejected" } # Path of the cached "winning" User-Agent for a subscription source: the first @@ -172,8 +192,50 @@ get_subscription_rejected_cache_path() { # refresh so we don't re-probe the whole whitelist every time. get_subscription_user_agent_cache_path() { local section="$1" + local urlhash="$2" + + echo "$SUBSCRIPTION_CACHE_FOLDER/${section}${urlhash:+.$urlhash}.user_agent" +} + +# Collect a section's subscription_url entries (a UCI list, but a lone legacy +# `option subscription_url` reads as a 1-element list exactly like +# community_lists) into the newline-delimited global SUBSCRIPTION_URLS_COLLECTED. +# URLs are opaque user text and may contain shell-special chars, so they are +# accumulated newline-delimited (URLs cannot contain a newline) and consumers +# read them with `while IFS= read -r`, never via word-splitting. +_collect_subscription_url_handler() { + local url="$1" + + [ -n "$url" ] || return 0 + if [ -z "$SUBSCRIPTION_URLS_COLLECTED" ]; then + SUBSCRIPTION_URLS_COLLECTED="$url" + else + SUBSCRIPTION_URLS_COLLECTED="$SUBSCRIPTION_URLS_COLLECTED +$url" + fi +} + +get_subscription_urls_for_section() { + local section="$1" + + SUBSCRIPTION_URLS_COLLECTED="" + config_list_foreach "$section" "subscription_url" _collect_subscription_url_handler + printf '%s' "$SUBSCRIPTION_URLS_COLLECTED" +} + +# Remove any stale legacy bare-"${section}.<ext>" cache files left over from +# before per-URL hashing. We now ALWAYS hash (uniform code path), so a bare file +# is never read again; reaping it keeps the cache dir from accumulating orphans +# and prevents subscription_cache_is_usable ever picking up a stale bare body. +reap_legacy_subscription_cache_files() { + local section="$1" - echo "$SUBSCRIPTION_CACHE_FOLDER/${section}.user_agent" + rm -f \ + "$(get_subscription_json_path "$section")" \ + "$(get_subscription_url_cache_path "$section")" \ + "$(get_subscription_rejected_cache_path "$section")" \ + "$(get_subscription_user_agent_cache_path "$section")" \ + 2>/dev/null } ensure_subscription_cache_dir() { @@ -206,22 +268,40 @@ ensure_subscription_cache_dir() { } migrate_subscription_cache_from_tmp() { - local src_json src_url dst_json dst_url + local src_json src_url src_section src_url_value dst_url dst_json dst_url_cache urlhash [ -d "$TMP_SUBSCRIPTION_FOLDER" ] || return 0 ensure_subscription_cache_dir || return 0 + # Legacy tmp caches are bare-keyed "<section>.json" (one feed per section). + # We now always hash per URL, so migrate each usable tmp body to the hashed + # destination of the URL it belongs to: prefer the URL recorded in the tmp + # "<section>.url" sidecar, else fall back to the section's first configured + # subscription_url. This keeps a legacy single-feed section working across an + # upgrade without an immediate re-download. for src_json in "$TMP_SUBSCRIPTION_FOLDER"/*.json; do [ -e "$src_json" ] || continue + src_section="$(basename "${src_json%.json}")" src_url="${src_json%.json}.url" - dst_json="$(get_subscription_json_path "$(basename "${src_json%.json}")")" - dst_url="$(get_subscription_url_cache_path "$(basename "${src_json%.json}")")" - if [ ! -e "$dst_json" ] && subscription_cache_is_usable "$src_json"; then + subscription_cache_is_usable "$src_json" || continue + + src_url_value="$(cat "$src_url" 2>/dev/null)" + if [ -z "$src_url_value" ]; then + src_url_value="$(get_subscription_urls_for_section "$src_section" | head -n 1)" + fi + [ -n "$src_url_value" ] || continue + + urlhash="$(get_subscription_url_hash "$src_url_value")" + [ -n "$urlhash" ] || continue + dst_json="$(get_subscription_json_path "$src_section" "$urlhash")" + dst_url_cache="$(get_subscription_url_cache_path "$src_section" "$urlhash")" + + if [ ! -e "$dst_json" ]; then cp "$src_json" "$dst_json" 2>/dev/null - [ -f "$src_url" ] && cp "$src_url" "$dst_url" 2>/dev/null - chmod 600 "$dst_json" "$dst_url" 2>/dev/null - log "Migrated subscription cache for section '$(basename "${src_json%.json}")' to persistent storage" "info" + printf '%s' "$src_url_value" > "$dst_url_cache" 2>/dev/null + chmod 600 "$dst_json" "$dst_url_cache" 2>/dev/null + log "Migrated subscription cache for section '$src_section' to per-URL persistent storage" "info" fi done } @@ -229,38 +309,49 @@ migrate_subscription_cache_from_tmp() { mark_subscription_outbound_unavailable() { local section="$1" local keyword_filter_active="${2:-0}" - local subscription_json_path rejected_cache_path rejected_hash + local url urlhash subscription_json_path rejected_cache_path rejected_hash case " $SUBSCRIPTION_UNAVAILABLE_SECTIONS " in *" $section "*) ;; *) SUBSCRIPTION_UNAVAILABLE_SECTIONS="$SUBSCRIPTION_UNAVAILABLE_SECTIONS $section" ;; esac - subscription_json_path="$(get_subscription_json_path "$section")" - rejected_cache_path="$(get_subscription_rejected_cache_path "$section")" - if [ "$keyword_filter_active" -eq 1 ]; then # The empty result came from the user's keyword filter, not from a bad - # feed: the body itself may be perfectly valid. Recording its hash here - # would poison the cache and wedge future downloads (return 14 loop), and - # would also survive the user loosening the filter. So never write the - # rejected-hash for this case, and proactively clear any stale one so a - # body poisoned by an earlier empty-filter run self-heals on this pass. - rm -f "$rejected_cache_path" - log "Subscription keyword filter for section '$section' removed all nodes; matching traffic for this section will be rejected until the filter is loosened or a matching node appears (the feed itself is not rejected)" "warn" + # feed: the bodies themselves may be perfectly valid. Recording their + # hashes here would poison the per-URL caches and wedge future downloads + # (return 14 loop), and would survive the user loosening the filter. So + # never write the rejected-hash for this case, and proactively clear any + # stale per-URL rejected hash so a feed poisoned by an earlier + # empty-filter run self-heals on this pass. + get_subscription_urls_for_section "$section" | while IFS= read -r url; do + [ -n "$url" ] || continue + urlhash="$(get_subscription_url_hash "$url")" + rm -f "$(get_subscription_rejected_cache_path "$section" "$urlhash")" + done + log "Subscription keyword filter for section '$section' removed all nodes; matching traffic for this section will be rejected until the filter is loosened or a matching node appears (the feeds themselves are not rejected)" "warn" subscription_startup_blocked=1 return 0 fi log "Subscription cache for section '$section' is unavailable; matching traffic for this section will be rejected until refresh succeeds" "warn" - # A structurally valid subscription can still contain no sing-box usable - # proxy outbounds. Remember its hash so the retry worker does not persist, - # restart and reject exactly the same unusable feed in a flash-writing loop. - rejected_hash="$(md5sum "$subscription_json_path" 2>/dev/null | awk '{print $1}')" - if [ -n "$rejected_hash" ] && [ "$(cat "$rejected_cache_path" 2>/dev/null)" != "$rejected_hash" ]; then - printf '%s' "$rejected_hash" > "${rejected_cache_path}.tmp.$$" && mv "${rejected_cache_path}.tmp.$$" "$rejected_cache_path" - chmod 600 "$rejected_cache_path" 2>/dev/null - fi + # A structurally valid subscription feed can still contain no sing-box usable + # proxy outbounds (e.g. all statically-unsupported). Remember each usable + # cached feed's hash PER-URL so the retry worker does not persist, restart + # and reject exactly the same unusable feed in a flash-writing loop; keeping + # this per-URL means one bad feed cannot poison another feed's retry state. + get_subscription_urls_for_section "$section" | while IFS= read -r url; do + [ -n "$url" ] || continue + urlhash="$(get_subscription_url_hash "$url")" + subscription_json_path="$(get_subscription_json_path "$section" "$urlhash")" + rejected_cache_path="$(get_subscription_rejected_cache_path "$section" "$urlhash")" + [ -s "$subscription_json_path" ] || continue + rejected_hash="$(md5sum "$subscription_json_path" 2>/dev/null | awk '{print $1}')" + if [ -n "$rejected_hash" ] && [ "$(cat "$rejected_cache_path" 2>/dev/null)" != "$rejected_hash" ]; then + printf '%s' "$rejected_hash" > "${rejected_cache_path}.tmp.$$" && mv "${rejected_cache_path}.tmp.$$" "$rejected_cache_path" + chmod 600 "$rejected_cache_path" 2>/dev/null + fi + done subscription_startup_blocked=1 } @@ -297,7 +388,7 @@ get_subscription_download_proxy_address() { fi if [ "$download_lists_via_proxy_section" = "$section" ]; then - if subscription_cache_is_usable "$(get_subscription_json_path "$section")"; then + if section_has_usable_subscription_cache "$section"; then log "Updating subscription for section '$section' through its currently active cached proxy" "info" echo "$(get_service_proxy_address 2>/dev/null || echo '')" return 0 @@ -311,7 +402,7 @@ get_subscription_download_proxy_address() { config_get selected_connection_type "$download_lists_via_proxy_section" "connection_type" config_get selected_proxy_config_type "$download_lists_via_proxy_section" "proxy_config_type" if [ "$selected_connection_type" = "proxy" ] && [ "$selected_proxy_config_type" = "subscription" ] && \ - ! subscription_cache_is_usable "$(get_subscription_json_path "$download_lists_via_proxy_section")"; then + ! section_has_usable_subscription_cache "$download_lists_via_proxy_section"; then log "Selected download proxy section '$download_lists_via_proxy_section' has no usable subscription cache; using direct mode for recovery of '$section'" "warn" return 0 fi @@ -356,6 +447,31 @@ subscription_cache_is_usable() { return 0 } +# True (0) if ANY of the section's per-URL subscription caches is usable. Used +# wherever a single-feed "section has a working cache" decision is needed (e.g. +# download-proxy selection); the section is considered to have a usable cache as +# soon as one feed does. URLs are written to a temp file and read with a plain +# `while read` (NOT a pipe) so the `found` flag survives in this shell. +section_has_usable_subscription_cache() { + local section="$1" + local url urlhash found urls_tmp + + found=1 + urls_tmp="$(mktemp "${TMPDIR:-/tmp}/netshift-sub-urls.XXXXXX")" || return 1 + get_subscription_urls_for_section "$section" > "$urls_tmp" + while IFS= read -r url || [ -n "$url" ]; do + [ -n "$url" ] || continue + urlhash="$(get_subscription_url_hash "$url")" + if subscription_cache_is_usable "$(get_subscription_json_path "$section" "$urlhash")"; then + found=0 + break + fi + done < "$urls_tmp" + rm -f "$urls_tmp" + + return "$found" +} + wait_for_subscription_connectivity() { local section="$1" local subscription_url="$2" @@ -385,6 +501,9 @@ download_subscription_into_cache() { local subscription_json_path="$3" local subscription_url_cache_path="$4" local service_proxy_address="$5" + # Optional per-URL hash subkey: keys the UA and rejected caches so each feed + # in a multi-URL section keeps independent state. Empty = legacy bare paths. + local urlhash="$6" local tmpfile persist_tmpfile url_tmpfile rejected_cache_path tmp_hash rejected_hash validation_reason file_size fallback_tmp local configured_user_agent user_agent_cache_path cached_user_agent candidates_file local effective_user_agent download_ok winning_user_agent ua_tmpfile @@ -407,7 +526,7 @@ download_subscription_into_cache() { # client User-Agent, so when none is configured we probe a whitelist of # well-known clients and keep the first that yields valid outbounds. The # previously successful UA (cached) is tried first to avoid re-probing. - user_agent_cache_path="$(get_subscription_user_agent_cache_path "$section")" + user_agent_cache_path="$(get_subscription_user_agent_cache_path "$section" "$urlhash")" configured_user_agent="$(uci -q get "netshift.${section}.subscription_user_agent" 2>/dev/null)" cached_user_agent="$(cat "$user_agent_cache_path" 2>/dev/null)" candidates_file="${tmpfile}.ua" @@ -482,7 +601,7 @@ download_subscription_into_cache() { fi fi - rejected_cache_path="$(get_subscription_rejected_cache_path "$section")" + rejected_cache_path="$(get_subscription_rejected_cache_path "$section" "$urlhash")" tmp_hash="$(md5sum "$tmpfile" 2>/dev/null | awk '{print $1}')" rejected_hash="$(cat "$rejected_cache_path" 2>/dev/null)" if [ -n "$tmp_hash" ] && [ "$tmp_hash" = "$rejected_hash" ]; then @@ -529,8 +648,10 @@ download_subscription_into_cache() { prepare_subscription_cache_for_startup() { local section="$1" - local connection_type proxy_config_type subscription_url subscription_json_path subscription_url_cache_path - local cached_subscription_url service_proxy_address had_usable_cache cache_needs_refresh + local connection_type proxy_config_type urls_tmp url urlhash + local subscription_json_path subscription_url_cache_path cached_subscription_url + local service_proxy_address had_usable_cache cache_needs_refresh + local _download_lists_via_proxy section_any_usable url_count config_get connection_type "$section" "connection_type" [ "$connection_type" = "proxy" ] || return 0 @@ -538,59 +659,83 @@ prepare_subscription_cache_for_startup() { config_get proxy_config_type "$section" "proxy_config_type" [ "$proxy_config_type" = "subscription" ] || return 0 - config_get subscription_url "$section" "subscription_url" - if [ -z "$subscription_url" ]; then - log "Subscription URL is not set for section '$section'. Aborted." "fatal" - exit 1 - fi - - subscription_json_path="$(get_subscription_json_path "$section")" - subscription_url_cache_path="$(get_subscription_url_cache_path "$section")" - cached_subscription_url="" - had_usable_cache=0 - cache_needs_refresh=0 - ensure_subscription_cache_dir || true migrate_subscription_cache_from_tmp + # Reap any stale legacy bare-"${section}.<ext>" cache so it can never be read + # again (we always hash now). + reap_legacy_subscription_cache_files "$section" - if subscription_cache_is_usable "$subscription_json_path"; then - had_usable_cache=1 - else - rm -f "$subscription_json_path" - fi - - if [ -f "$subscription_url_cache_path" ]; then - cached_subscription_url="$(cat "$subscription_url_cache_path" 2> /dev/null)" - fi - - if [ "$had_usable_cache" -eq 0 ] || [ "$cached_subscription_url" != "$subscription_url" ]; then - cache_needs_refresh=1 - fi - - if [ "$cache_needs_refresh" -eq 0 ]; then + urls_tmp="$(mktemp "${TMPDIR:-/tmp}/netshift-startup-urls.XXXXXX")" || { + log "Failed to create temporary URL list for section '$section'" "error" return 0 + } + get_subscription_urls_for_section "$section" > "$urls_tmp" + url_count=0 + while IFS= read -r url || [ -n "$url" ]; do + [ -n "$url" ] && url_count=$((url_count + 1)) + done < "$urls_tmp" + + if [ "$url_count" -eq 0 ]; then + rm -f "$urls_tmp" + log "Subscription URL is not set for section '$section'. Aborted." "fatal" + exit 1 fi - # Bootstrap subscription directly: sing-box (and its service proxy) is not - # running yet at this stage, so proxy-based bootstrap would deadlock here. - service_proxy_address="" - local _download_lists_via_proxy config_get_bool _download_lists_via_proxy "settings" "download_lists_via_proxy" 0 if [ "$_download_lists_via_proxy" -eq 1 ]; then log "download_lists_via_proxy is set, but sing-box is not running yet during startup. Bootstrapping subscription for section '$section' over a direct connection" "info" fi - if wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 2 2 5; then - if download_subscription_into_cache \ - "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address"; then - return 0 + # Best-effort: each URL is downloaded independently; a single dead feed must + # not abort the others. The section is "available" if at least one feed has + # a usable cache after this pass. + section_any_usable=0 + while IFS= read -r url || [ -n "$url" ]; do + [ -n "$url" ] || continue + urlhash="$(get_subscription_url_hash "$url")" + subscription_json_path="$(get_subscription_json_path "$section" "$urlhash")" + subscription_url_cache_path="$(get_subscription_url_cache_path "$section" "$urlhash")" + + had_usable_cache=0 + if subscription_cache_is_usable "$subscription_json_path"; then + had_usable_cache=1 + else + rm -f "$subscription_json_path" fi - fi - if [ "$had_usable_cache" -eq 1 ]; then - log "Keeping cached subscription for section '$section' until a fresh download succeeds" "warn" - else - log "No usable subscription cache for section '$section'; startup will continue with a temporary blocked outbound until subscription becomes reachable" "warn" + cached_subscription_url="" + [ -f "$subscription_url_cache_path" ] && cached_subscription_url="$(cat "$subscription_url_cache_path" 2>/dev/null)" + + cache_needs_refresh=0 + if [ "$had_usable_cache" -eq 0 ] || [ "$cached_subscription_url" != "$url" ]; then + cache_needs_refresh=1 + fi + + if [ "$cache_needs_refresh" -eq 0 ]; then + section_any_usable=1 + continue + fi + + # Bootstrap directly: sing-box (and its service proxy) is not running yet. + service_proxy_address="" + if wait_for_subscription_connectivity "$section" "$url" "$service_proxy_address" 2 2 5 && + download_subscription_into_cache \ + "$section" "$url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address" "$urlhash"; then + section_any_usable=1 + continue + fi + + if [ "$had_usable_cache" -eq 1 ]; then + log "Keeping cached subscription feed for section '$section' (url=$(redact_url_for_log "$url")) until a fresh download succeeds" "warn" + section_any_usable=1 + else + log "Subscription feed for section '$section' (url=$(redact_url_for_log "$url")) is not reachable and has no usable cache yet" "warn" + fi + done < "$urls_tmp" + rm -f "$urls_tmp" + + if [ "$section_any_usable" -eq 0 ]; then + log "No usable subscription cache for any feed of section '$section'; startup will continue with a temporary blocked outbound until a subscription becomes reachable" "warn" subscription_startup_blocked=1 fi @@ -1401,8 +1546,9 @@ subscription_update() { _update_subscription_for_section() { local section="$1" - local connection_type proxy_config_type subscription_url subscription_json_path + local connection_type proxy_config_type url urlhash subscription_json_path local subscription_url_cache_path service_proxy_address update_result outbounds_count + local urls_tmp url_count section_changed feed_usable_count feed_failed_count config_get connection_type "$section" "connection_type" if [ "$connection_type" != "proxy" ]; then @@ -1415,85 +1561,108 @@ subscription_update() { return fi - config_get subscription_url "$section" "subscription_url" - if [ -z "$subscription_url" ]; then - echolog "❌ Subscription URL not set for section '$section'" - failed_sections=$((failed_sections + 1)) - return - fi - mkdir -p "$TMP_SUBSCRIPTION_FOLDER" if ! ensure_subscription_cache_dir; then echolog "❌ Subscription cache directory is unavailable for section '$section'" failed_sections=$((failed_sections + 1)) return fi - subscription_json_path="$(get_subscription_json_path "$section")" - subscription_url_cache_path="$(get_subscription_url_cache_path "$section")" - - echolog "📥 Updating subscription for section '$section'..." - - service_proxy_address="$(get_subscription_download_proxy_address "$section" "runtime" || echo '')" - if [ -n "$service_proxy_address" ]; then - log "Updating subscription for section '$section' via service proxy $service_proxy_address" "info" - else - log "Updating subscription for section '$section' directly" "info" - fi + reap_legacy_subscription_cache_files "$section" - if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 6 5 5; then - echolog "❌ Subscription source is not reachable for section '$section'" + urls_tmp="$(mktemp "${TMPDIR:-/tmp}/netshift-refresh-urls.XXXXXX")" || { + echolog "❌ Failed to enumerate subscription URLs for section '$section'" + failed_sections=$((failed_sections + 1)) + return + } + get_subscription_urls_for_section "$section" > "$urls_tmp" + url_count=0 + while IFS= read -r url || [ -n "$url" ]; do + [ -n "$url" ] && url_count=$((url_count + 1)) + done < "$urls_tmp" + + if [ "$url_count" -eq 0 ]; then + rm -f "$urls_tmp" + echolog "❌ Subscription URL not set for section '$section'" failed_sections=$((failed_sections + 1)) return fi - download_subscription_into_cache \ - "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address" - update_result=$? + echolog "📥 Updating subscription for section '$section' ($url_count feed(s))..." + + # Best-effort per-feed download. The section is "changed" (→ restart) if + # ANY feed changed; the section "failed" only if ALL feeds failed AND none + # has a usable cache. One dead feed never aborts the others. + section_changed=0 + feed_usable_count=0 + feed_failed_count=0 + while IFS= read -r url || [ -n "$url" ]; do + [ -n "$url" ] || continue + urlhash="$(get_subscription_url_hash "$url")" + subscription_json_path="$(get_subscription_json_path "$section" "$urlhash")" + subscription_url_cache_path="$(get_subscription_url_cache_path "$section" "$urlhash")" + + service_proxy_address="$(get_subscription_download_proxy_address "$section" "runtime" || echo '')" + if [ -n "$service_proxy_address" ]; then + log "Updating subscription feed for section '$section' (url=$(redact_url_for_log "$url")) via service proxy $service_proxy_address" "info" + else + log "Updating subscription feed for section '$section' (url=$(redact_url_for_log "$url")) directly" "info" + fi + + if ! wait_for_subscription_connectivity "$section" "$url" "$service_proxy_address" 6 5 5; then + echolog "⚠️ Subscription feed not reachable for section '$section': url=$(redact_url_for_log "$url")" + feed_failed_count=$((feed_failed_count + 1)) + subscription_cache_is_usable "$subscription_json_path" && feed_usable_count=$((feed_usable_count + 1)) + continue + fi - case "$update_result" in - 0) + download_subscription_into_cache \ + "$section" "$url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address" "$urlhash" + update_result=$? + + case "$update_result" in + 0) + section_changed=1 + feed_usable_count=$((feed_usable_count + 1)) + outbounds_count=$(jq -r '[.outbounds[] | select( + .type != "selector" and + .type != "urltest" and + .type != "direct" and + .type != "dns" and + .type != "block" + )] | length' "$subscription_json_path" 2>/dev/null) + echolog "✅ Subscription feed updated for section '$section': $outbounds_count outbounds (url=$(redact_url_for_log "$url"))" + ;; + 2) + feed_usable_count=$((feed_usable_count + 1)) + echolog "ℹ️ Subscription feed for section '$section' is unchanged (url=$(redact_url_for_log "$url"))" + ;; + *) + feed_failed_count=$((feed_failed_count + 1)) + # A previously cached body for this feed still counts as usable. + subscription_cache_is_usable "$subscription_json_path" && feed_usable_count=$((feed_usable_count + 1)) + echolog "⚠️ Subscription feed failed for section '$section' (rc=$update_result): url=$(redact_url_for_log "$url")" + ;; + esac + done < "$urls_tmp" + rm -f "$urls_tmp" + + if [ "$section_changed" -eq 1 ]; then updated_sections=$((updated_sections + 1)) - outbounds_count=$(jq -r '[.outbounds[] | select( - .type != "selector" and - .type != "urltest" and - .type != "direct" and - .type != "dns" and - .type != "block" - )] | length' "$subscription_json_path" 2>/dev/null) - - echolog "✅ Subscription updated for section '$section': $outbounds_count outbounds" - ;; - 2) - echolog "ℹ️ Subscription for section '$section' is unchanged" + if [ "$feed_failed_count" -gt 0 ]; then + echolog "✅ Subscription updated for section '$section' ($feed_failed_count feed(s) failed and kept their previous cache)" + else + echolog "✅ Subscription updated for section '$section'" + fi return - ;; - 10) - echolog "❌ Failed to prepare subscription cache for section '$section'" - ;; - 11) - echolog "❌ Failed to create temporary subscription download file for section '$section'" - ;; - 12) - echolog "❌ Failed to download subscription body for section '$section'" - ;; - 13) - echolog "❌ Downloaded subscription for section '$section' is invalid" - ;; - 14) - echolog "❌ Downloaded subscription for section '$section' is unchanged and still has no usable outbounds" - ;; - 15) - echolog "❌ Failed to persist subscription cache for section '$section'" - ;; - *) - echolog "❌ Failed to update subscription for section '$section': internal error rc=$update_result" - ;; - esac + fi - if [ "$update_result" -ne 0 ]; then + if [ "$feed_usable_count" -eq 0 ]; then + echolog "❌ Failed to update any subscription feed for section '$section'" failed_sections=$((failed_sections + 1)) return fi + + echolog "ℹ️ Subscription for section '$section' is unchanged" } config_foreach _update_subscription_for_section "section" @@ -1762,14 +1931,15 @@ configure_outbound_handler() { ;; subscription) log "Detected proxy configuration type: subscription" "debug" - local subscription_url subscription_json_path urltest_tag selector_tag \ + local subscription_urls_tmp subscription_url subscription_url_count urltest_tag selector_tag \ urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance \ urltest_testing_url subscription_group_by_countries subscription_group_by_countries_raw \ subscription_outbound_tags_json service_proxy_address subscription_ready \ subscription_filter_include_keywords_json subscription_filter_exclude_keywords_json \ - subscription_keyword_filter_active + subscription_keyword_filter_active urlhash subscription_json_path subscription_url_cache_path \ + cached_subscription_url should_download had_usable_cache merged_json_path merged_node_count \ + usable_feed_count feed_node_count merged_tmp - config_get subscription_url "$section" "subscription_url" config_get urltest_check_interval "$section" "urltest_check_interval" "3m" config_get urltest_tolerance "$section" "urltest_tolerance" 50 config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204" @@ -1797,62 +1967,138 @@ configure_outbound_handler() { log "Subscription keyword filter enabled for section '$section': include=$subscription_filter_include_keywords_json, exclude=$subscription_filter_exclude_keywords_json" "debug" fi - if [ -z "$subscription_url" ]; then - log "Subscription URL is not set. Aborted." "fatal" - exit 1 - fi - mkdir -p "$TMP_SUBSCRIPTION_FOLDER" - subscription_json_path="$(get_subscription_json_path "$section")" - local subscription_url_cache_path cached_subscription_url should_download had_usable_cache - subscription_url_cache_path="$(get_subscription_url_cache_path "$section")" - should_download=0 - had_usable_cache=0 - - if subscription_cache_is_usable "$subscription_json_path"; then - had_usable_cache=1 - else - rm -f "$subscription_json_path" - should_download=1 - fi + ensure_subscription_cache_dir || true + migrate_subscription_cache_from_tmp + # Always hash per URL; reap any stale legacy bare-"${section}.<ext>". + reap_legacy_subscription_cache_files "$section" - if [ -f "$subscription_url_cache_path" ]; then - cached_subscription_url="$(cat "$subscription_url_cache_path" 2>/dev/null)" - else - cached_subscription_url="" + subscription_urls_tmp="$(mktemp "${TMPDIR:-/tmp}/netshift-cfg-urls.XXXXXX")" || { + log "Failed to enumerate subscription URLs for section '$section'. Aborted." "fatal" + exit 1 + } + get_subscription_urls_for_section "$section" > "$subscription_urls_tmp" + subscription_url_count=0 + while IFS= read -r subscription_url || [ -n "$subscription_url" ]; do + [ -n "$subscription_url" ] && subscription_url_count=$((subscription_url_count + 1)) + done < "$subscription_urls_tmp" + + if [ "$subscription_url_count" -eq 0 ]; then + rm -f "$subscription_urls_tmp" + log "Subscription URL is not set. Aborted." "fatal" + exit 1 fi - if [ "$cached_subscription_url" != "$subscription_url" ]; then - if [ -n "$cached_subscription_url" ]; then - log "Subscription URL changed for section '$section'" "warn" - fi - if [ "$had_usable_cache" -eq 0 ]; then + log "Section '$section' has $subscription_url_count subscription feed(s)" "debug" + + # ── Per-feed download (best-effort) ───────────────────────────── + # Each URL is downloaded/validated/normalized into its own per-URL + # cache. One dead/empty feed must NOT abort the others. + while IFS= read -r subscription_url || [ -n "$subscription_url" ]; do + [ -n "$subscription_url" ] || continue + urlhash="$(get_subscription_url_hash "$subscription_url")" + subscription_json_path="$(get_subscription_json_path "$section" "$urlhash")" + subscription_url_cache_path="$(get_subscription_url_cache_path "$section" "$urlhash")" + should_download=0 + had_usable_cache=0 + + if subscription_cache_is_usable "$subscription_json_path"; then + had_usable_cache=1 + else + rm -f "$subscription_json_path" should_download=1 + fi + + if [ -f "$subscription_url_cache_path" ]; then + cached_subscription_url="$(cat "$subscription_url_cache_path" 2>/dev/null)" else - log "Using cached subscription for section '$section' until a fresh download succeeds" "warn" + cached_subscription_url="" fi - fi - if [ "$should_download" -eq 1 ]; then - log "Downloading subscription for section '$section'" - # Config generation runs before sing-box is started. Never use - # its local download proxy here; runtime refresh will use it. - service_proxy_address="$(get_subscription_download_proxy_address "$section" "bootstrap" 2>/dev/null || echo '')" - - if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 1 0 5 || - ! download_subscription_into_cache \ - "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address"; then - if [ "$had_usable_cache" -eq 1 ]; then - log "Failed to refresh subscription for section '$section', continuing with cached data" "warn" + if [ "$cached_subscription_url" != "$subscription_url" ]; then + if [ "$had_usable_cache" -eq 0 ]; then + should_download=1 else - log "Failed to download subscription for section '$section'; using a temporary blocked outbound" "warn" + log "Using cached subscription feed for section '$section' (url=$(redact_url_for_log "$subscription_url")) until a fresh download succeeds" "warn" fi fi - fi + + if [ "$should_download" -eq 1 ]; then + log "Downloading subscription feed for section '$section' (url=$(redact_url_for_log "$subscription_url"))" + # Config generation runs before sing-box is started. Never + # use its local download proxy here; runtime refresh will. + service_proxy_address="$(get_subscription_download_proxy_address "$section" "bootstrap" 2>/dev/null || echo '')" + + if ! wait_for_subscription_connectivity "$section" "$subscription_url" "$service_proxy_address" 1 0 5 || + ! download_subscription_into_cache \ + "$section" "$subscription_url" "$subscription_json_path" "$subscription_url_cache_path" "$service_proxy_address" "$urlhash"; then + if [ "$had_usable_cache" -eq 1 ]; then + log "Failed to refresh subscription feed for section '$section' (url=$(redact_url_for_log "$subscription_url")), continuing with cached data" "warn" + else + log "Failed to download subscription feed for section '$section' (url=$(redact_url_for_log "$subscription_url"))" "warn" + fi + fi + fi + done < "$subscription_urls_tmp" + + # ── Merge all usable per-URL caches into one JSON ─────────────── + # Concatenate the proxy .outbounds[] of every usable feed into a + # single { "outbounds": [...] } temp file, then run the existing + # facade ONCE so its keyword filter + global tag-dedup (auto -2/-3 + # for same-named nodes across feeds) + per-batch sing-box check + # bisection + the country-group/selector builder all operate over + # the MERGED union, exactly as for a single feed. + mkdir -p "$TMP_SUBSCRIPTION_MERGE_FOLDER" + merged_json_path="$(mktemp "$TMP_SUBSCRIPTION_MERGE_FOLDER/${section}.merged.XXXXXX")" || { + rm -f "$subscription_urls_tmp" + log "Failed to create merged subscription file for section '$section'. Aborted." "fatal" + exit 1 + } + printf '%s' '{"outbounds":[]}' > "$merged_json_path" + usable_feed_count=0 + while IFS= read -r subscription_url || [ -n "$subscription_url" ]; do + [ -n "$subscription_url" ] || continue + urlhash="$(get_subscription_url_hash "$subscription_url")" + subscription_json_path="$(get_subscription_json_path "$section" "$urlhash")" + subscription_cache_is_usable "$subscription_json_path" || { + log "Subscription feed for section '$section' has no usable cache; skipping (url=$(redact_url_for_log "$subscription_url"))" "warn" + continue + } + + feed_node_count="$(jq -r '[.outbounds[]? | select( + .type != "selector" and .type != "urltest" and + .type != "direct" and .type != "dns" and .type != "block" + )] | length' "$subscription_json_path" 2>/dev/null)" + [ -n "$feed_node_count" ] || feed_node_count=0 + + # Append this feed's proxy outbounds onto the merged set. No + # Oniguruma: pure array concat with --slurpfile. + merged_tmp="${merged_json_path}.tmp.$$" + if jq -c --slurpfile feed "$subscription_json_path" ' + .outbounds += [ $feed[0].outbounds[]? | select( + .type != "selector" and .type != "urltest" and + .type != "direct" and .type != "dns" and .type != "block" + ) ] + ' "$merged_json_path" > "$merged_tmp" 2>/dev/null && [ -s "$merged_tmp" ]; then + mv "$merged_tmp" "$merged_json_path" + usable_feed_count=$((usable_feed_count + 1)) + log "Subscription feed for section '$section' contributed $feed_node_count node(s) (url=$(redact_url_for_log "$subscription_url"))" "info" + else + rm -f "$merged_tmp" + log "Failed to merge subscription feed for section '$section' (url=$(redact_url_for_log "$subscription_url"))" "warn" + fi + done < "$subscription_urls_tmp" + rm -f "$subscription_urls_tmp" + + merged_node_count="$(jq -r '.outbounds | length' "$merged_json_path" 2>/dev/null)" + [ -n "$merged_node_count" ] || merged_node_count=0 + log "Subscription for section '$section': merged $merged_node_count node(s) from $usable_feed_count usable feed(s)" "info" subscription_ready=0 - if subscription_cache_is_usable "$subscription_json_path"; then - if sing_box_cf_add_subscription_outbounds "$config" "$section" "$subscription_json_path" \ + # A merged file with >=1 proxy outbound is a real subscription set. + if [ "$usable_feed_count" -gt 0 ] && [ "$merged_node_count" -gt 0 ]; then + chmod 600 "$merged_json_path" 2>/dev/null + if sing_box_cf_add_subscription_outbounds "$config" "$section" "$merged_json_path" \ "$subscription_filter_include_keywords_json" "$subscription_filter_exclude_keywords_json" > /dev/null; then if [ -n "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then config="$SING_BOX_CF_LAST_CONFIG" @@ -1860,6 +2106,7 @@ configure_outbound_handler() { fi fi fi + rm -f "$merged_json_path" if [ "$subscription_ready" -eq 0 ]; then # When the keyword filter empties the set, the precise cause is diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index a1cacb48..799c6dae 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -14,6 +14,12 @@ TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets" TMP_SUBSCRIPTION_FOLDER="$TMP_SING_BOX_FOLDER/subscriptions" SUBSCRIPTION_CACHE_FOLDER="$NETSHIFT_STATE_DIR/subscriptions" TMP_SUBSCRIPTION_DOWNLOAD_FOLDER="$TMP_SING_BOX_FOLDER/subscription-downloads" +# A section may list MULTIPLE subscription_url feeds. At config generation the +# usable per-URL caches are concatenated into one merged subscription JSON in +# this folder, then passed ONCE through the facade (keyword filter + global +# tag-dedup + sing-box check bisection). Per-feed cache files are keyed +# "${section}.<md5(url)>.<ext>" under SUBSCRIPTION_CACHE_FOLDER. +TMP_SUBSCRIPTION_MERGE_FOLDER="$TMP_SING_BOX_FOLDER/subscription-merge" # Subscription User-Agent fallback. Many panels return a DIFFERENT body format # depending on the client User-Agent (sing-box JSON vs base64 URI list vs Clash # vs Xray JSON, or an HTML/403 stub for unknown clients). When no User-Agent is diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index ecf7ffad..c953af79 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1539,6 +1539,298 @@ FBEOF done rm -f "$fb" + + # ── Multi-URL subscription merge (task-022) ───────────────────── + # Exercises the per-URL hashed cache keying + the config-gen merge-file + # approach against the REAL facade (live sing-box check bisection). The + # cache-path builders / URL-hash / URL-list collector / cache-usable / + # mark-unavailable functions are awk-extracted VERBATIM from the live bin so + # the test runs shipped code; the merge jq mirrors the inline subscription) + # branch program exactly. Tokens use the same name:OK/FAIL convention. + printf "\n ${BOLD}Multi-URL Subscription Merge${NC}\n" + + local bin="${NETSHIFT_SRC}/usr/bin/netshift" + if [ ! -r "$bin" ] || [ ! -r "$lib/sing_box_config_facade.sh" ]; then + skip "multi-url merge (bin / facade not found)" + return + fi + + local mu="/tmp/netshift-sub-multiurl-$$.sh" + cat > "$mu" << 'MUEOF' +mkdir -p /usr/lib/netshift +for f in constants.sh helpers.sh logging.sh sing_box_config_manager.sh sing_box_config_facade.sh; do + ln -sf "LIB_DIR/$f" "/usr/lib/netshift/$f" +done +. /usr/lib/netshift/constants.sh +. /usr/lib/netshift/logging.sh +. /usr/lib/netshift/sing_box_config_facade.sh + +# Isolated per-run cache dir for the path builders. +SUBSCRIPTION_CACHE_FOLDER="/tmp/netshift-mu-cache-$$" +mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" + +# Quiet logger + redaction stub (functions under test call these). +log() { :; } +echolog() { :; } +nolog() { :; } +redact_url_for_log() { printf '%s' "redacted"; } + +# Stub config_list_foreach to feed the URLs of the "current" section from a +# global newline list MU_URLS (mimics UCI list iteration; a 1-element list +# proves the legacy single-option back-compat path). +config_list_foreach() { + # $1=section $2=option $3=callback [extra...]; we only honour subscription_url. + # The real LuCI config_list_foreach iterates in the CURRENT shell (no pipe), + # so the callback CAN mutate accumulator globals; mirror that with a temp + # file + plain `while read` (a pipe would subshell-trap the mutation). + [ "$2" = "subscription_url" ] || return 0 + _clf_tmp="/tmp/netshift-mu-clf-$$" + printf '%s\n' "$MU_URLS" > "$_clf_tmp" + while IFS= read -r _u || [ -n "$_u" ]; do + [ -n "$_u" ] || continue + "$3" "$_u" + done < "$_clf_tmp" + rm -f "$_clf_tmp" +} + +# Extract the shipped functions verbatim (column-0 opener to column-0 '}'). +for fn in get_subscription_url_hash get_subscription_json_path \ + get_subscription_url_cache_path get_subscription_rejected_cache_path \ + get_subscription_user_agent_cache_path _collect_subscription_url_handler \ + get_subscription_urls_for_section reap_legacy_subscription_cache_files \ + subscription_cache_is_usable section_has_usable_subscription_cache \ + mark_subscription_outbound_unavailable; do + eval "$(awk -v f="$fn" '$0 ~ "^"f"\\(\\) \\{"{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")" +done + +# Globals the extracted functions touch. +SUBSCRIPTION_UNAVAILABLE_SECTIONS="" +subscription_startup_blocked=0 + +base_config='{"outbounds":[]}' + +# Helper: write a per-URL cache for (section,url) from a JSON body. +write_feed() { + _sec="$1"; _url="$2"; _body="$3" + _h="$(get_subscription_url_hash "$_url")" + printf '%s' "$_body" > "$(get_subscription_json_path "$_sec" "$_h")" + printf '%s' "$_url" > "$(get_subscription_url_cache_path "$_sec" "$_h")" +} + +# Helper: build the merged file exactly like the subscription) branch and run +# the facade once. Echoes the resulting config to stdout; sets MERGED_COUNT. +merge_and_add() { + _sec="$1" + _merged="/tmp/netshift-mu-merged-$$-$_sec.json" + printf '%s' '{"outbounds":[]}' > "$_merged" + MU_URLS="$2" + printf '%s\n' "$MU_URLS" | while IFS= read -r _u; do + [ -n "$_u" ] || continue + _h="$(get_subscription_url_hash "$_u")" + _j="$(get_subscription_json_path "$_sec" "$_h")" + subscription_cache_is_usable "$_j" || continue + _t="${_merged}.t" + jq -c --slurpfile feed "$_j" ' + .outbounds += [ $feed[0].outbounds[]? | select( + .type != "selector" and .type != "urltest" and + .type != "direct" and .type != "dns" and .type != "block" + ) ] + ' "$_merged" > "$_t" 2>/dev/null && mv "$_t" "$_merged" + done + MERGED_COUNT="$(jq -r '.outbounds | length' "$_merged" 2>/dev/null)" +} + +# ── CASE 1: multi-URL merge — two feeds, distinct node names ────────── +s1="sec1" +url1a="https://feed-a.example.com/sub" +url1b="https://feed-b.example.com/sub" +write_feed "$s1" "$url1a" '{"outbounds":[ + {"type":"shadowsocks","tag":"A-Tokyo","server":"a1.example.com","server_port":443,"method":"aes-256-gcm","password":"p"}, + {"type":"shadowsocks","tag":"A-Osaka","server":"a2.example.com","server_port":443,"method":"aes-256-gcm","password":"p"} +]}' +write_feed "$s1" "$url1b" '{"outbounds":[ + {"type":"shadowsocks","tag":"B-Berlin","server":"b1.example.com","server_port":443,"method":"aes-256-gcm","password":"p"} +]}' +s1_urls="$url1a +$url1b" +merge_and_add "$s1" "$s1_urls" +if [ "$MERGED_COUNT" = "3" ]; then + echo 'mu-case1-merged-count-3:OK' +else + echo "mu-case1-merged-count-3(got $MERGED_COUNT):FAIL" +fi +# Call the facade like the real bin: NO command-substitution (globals must +# propagate to this shell); read the result from SING_BOX_CF_LAST_CONFIG. +sing_box_cf_add_subscription_outbounds "$base_config" "$s1" "/tmp/netshift-mu-merged-$$-$s1.json" "[]" "[]" >/dev/null +out1="$SING_BOX_CF_LAST_CONFIG" +if printf '%s' "$out1" | jq -e '[.outbounds[] | select(.type=="shadowsocks") | .tag] | (index("A-Tokyo") != null) and (index("A-Osaka") != null) and (index("B-Berlin") != null)' >/dev/null 2>&1; then + echo 'mu-case1-both-feeds-present:OK' +else + echo 'mu-case1-both-feeds-present:FAIL' +fi +if [ "$(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'length' 2>/dev/null)" = "3" ]; then + echo 'mu-case1-tags-json-3:OK' +else + echo "mu-case1-tags-json-3(got $(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'length' 2>/dev/null)):FAIL" +fi +rm -f "/tmp/netshift-mu-merged-$$-$s1.json" + +# ── CASE 2: same-named nodes across feeds → dedup -2 suffix ─────────── +s2="sec2" +url2a="https://feed-a2.example.com/sub" +url2b="https://feed-b2.example.com/sub" +write_feed "$s2" "$url2a" '{"outbounds":[ + {"type":"shadowsocks","tag":"Same Node","server":"c1.example.com","server_port":443,"method":"aes-256-gcm","password":"p"} +]}' +write_feed "$s2" "$url2b" '{"outbounds":[ + {"type":"shadowsocks","tag":"Same Node","server":"c2.example.com","server_port":443,"method":"aes-256-gcm","password":"p"} +]}' +s2_urls="$url2a +$url2b" +merge_and_add "$s2" "$s2_urls" +sing_box_cf_add_subscription_outbounds "$base_config" "$s2" "/tmp/netshift-mu-merged-$$-$s2.json" "[]" "[]" >/dev/null +out2="$SING_BOX_CF_LAST_CONFIG" +# Two same-named nodes must both survive with distinct deduped tags, and the +# resulting config must have no duplicate outbound tags (would fail sing-box). +n2="$(printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -r 'length' 2>/dev/null)" +dup2="$(printf '%s' "$out2" | jq -r '[.outbounds[].tag] | (length) - ([.[]] | unique | length)' 2>/dev/null)" +if [ "$n2" = "2" ] && [ "$dup2" = "0" ]; then + echo 'mu-case2-samename-dedup:OK' +else + echo "mu-case2-samename-dedup(n=$n2 dup=$dup2):FAIL" +fi +# The facade's dedup appends a numeric suffix to the second same-named node +# ("Same Node" + "Same Node-1"); assert one base + one suffixed variant survive. +if printf '%s' "$SUBSCRIPTION_OUTBOUND_TAGS_JSON" | jq -e 'any(.[]; . == "Same Node") and any(.[]; (startswith("Same Node-")))' >/dev/null 2>&1; then + echo 'mu-case2-suffix-dedup-present:OK' +else + echo "mu-case2-suffix-dedup-present(tags=$SUBSCRIPTION_OUTBOUND_TAGS_JSON):FAIL" +fi +rm -f "/tmp/netshift-mu-merged-$$-$s2.json" + +# ── CASE 3: partial failure / best-effort — feed A usable, B invalid ── +s3="sec3" +url3a="https://feed-a3.example.com/sub" +url3b="https://feed-b3.example.com/sub" +write_feed "$s3" "$url3a" '{"outbounds":[ + {"type":"shadowsocks","tag":"Good","server":"d1.example.com","server_port":443,"method":"aes-256-gcm","password":"p"} +]}' +# Feed B is structurally invalid (not a sing-box object): NOT cache-usable. +_h3b="$(get_subscription_url_hash "$url3b")" +printf '%s' 'this is not json' > "$(get_subscription_json_path "$s3" "$_h3b")" +printf '%s' "$url3b" > "$(get_subscription_url_cache_path "$s3" "$_h3b")" +s3_urls="$url3a +$url3b" +merge_and_add "$s3" "$s3_urls" +sing_box_cf_add_subscription_outbounds "$base_config" "$s3" "/tmp/netshift-mu-merged-$$-$s3.json" "[]" "[]" >/dev/null +out3="$SING_BOX_CF_LAST_CONFIG" +if [ "$MERGED_COUNT" = "1" ] && [ -n "$SUBSCRIPTION_OUTBOUND_TAGS" ]; then + echo 'mu-case3-partial-best-effort:OK' +else + echo "mu-case3-partial-best-effort(count=$MERGED_COUNT tags='$SUBSCRIPTION_OUTBOUND_TAGS'):FAIL" +fi +case " $SUBSCRIPTION_UNAVAILABLE_SECTIONS " in +*" $s3 "*) echo 'mu-case3-not-unavailable:FAIL' ;; +*) echo 'mu-case3-not-unavailable:OK' ;; +esac +rm -f "/tmp/netshift-mu-merged-$$-$s3.json" + +# ── CASE 4: all feeds fail → section marked unavailable ─────────────── +s4="sec4" +url4a="https://feed-a4.example.com/sub" +url4b="https://feed-b4.example.com/sub" +_h4a="$(get_subscription_url_hash "$url4a")" +_h4b="$(get_subscription_url_hash "$url4b")" +printf '%s' 'garbage' > "$(get_subscription_json_path "$s4" "$_h4a")" +printf '%s' '{"outbounds":[]}' > "$(get_subscription_json_path "$s4" "$_h4b")" +s4_urls="$url4a +$url4b" +merge_and_add "$s4" "$s4_urls" +subscription_ready=0 +if [ "$MERGED_COUNT" -gt 0 ] 2>/dev/null; then subscription_ready=1; fi +if [ "$subscription_ready" -eq 0 ]; then + MU_URLS="$s4_urls" + mark_subscription_outbound_unavailable "$s4" 0 +fi +case " $SUBSCRIPTION_UNAVAILABLE_SECTIONS " in +*" $s4 "*) echo 'mu-case4-all-fail-unavailable:OK' ;; +*) echo "mu-case4-all-fail-unavailable(merged=$MERGED_COUNT list='$SUBSCRIPTION_UNAVAILABLE_SECTIONS'):FAIL" ;; +esac +rm -f "/tmp/netshift-mu-merged-$$-$s4.json" + +# ── CASE 5: cache-key isolation — distinct files; rejected per-URL ──── +s5="sec5" +url5a="https://feed-a5.example.com/sub" +url5b="https://feed-b5.example.com/sub" +write_feed "$s5" "$url5a" '{"outbounds":[ + {"type":"shadowsocks","tag":"Iso-A","server":"e1.example.com","server_port":443,"method":"aes-256-gcm","password":"p"} +]}' +write_feed "$s5" "$url5b" '{"outbounds":[ + {"type":"shadowsocks","tag":"Iso-B","server":"e2.example.com","server_port":443,"method":"aes-256-gcm","password":"p"} +]}' +_h5a="$(get_subscription_url_hash "$url5a")" +_h5b="$(get_subscription_url_hash "$url5b")" +p5a="$(get_subscription_json_path "$s5" "$_h5a")" +p5b="$(get_subscription_json_path "$s5" "$_h5b")" +if [ "$_h5a" != "$_h5b" ] && [ "$p5a" != "$p5b" ] && [ -s "$p5a" ] && [ -s "$p5b" ]; then + echo 'mu-case5-distinct-cache-files:OK' +else + echo "mu-case5-distinct-cache-files(ha=$_h5a hb=$_h5b):FAIL" +fi +# Poison URL-A's rejected hash with its own body hash → A vetoed, B untouched. +md5sum "$p5a" | awk '{print $1}' > "$(get_subscription_rejected_cache_path "$s5" "$_h5a")" +# Force the rejected-veto path: a body with no proxy outbound + matching hash. +# (subscription_cache_is_usable returns 0 for a body WITH proxies regardless of +# rejected, so prove isolation via the rejected FILE targeting, not the veto.) +ra="$(get_subscription_rejected_cache_path "$s5" "$_h5a")" +rb="$(get_subscription_rejected_cache_path "$s5" "$_h5b")" +if [ -s "$ra" ] && [ ! -e "$rb" ]; then + echo 'mu-case5-rejected-per-url-isolated:OK' +else + echo 'mu-case5-rejected-per-url-isolated:FAIL' +fi + +# ── CASE 6: back-compat — single (1-element) URL list works ─────────── +s6="sec6" +url6="https://feed-legacy.example.com/sub" +write_feed "$s6" "$url6" '{"outbounds":[ + {"type":"shadowsocks","tag":"Legacy","server":"f1.example.com","server_port":443,"method":"aes-256-gcm","password":"p"} +]}' +# A lone option reads as a 1-element list. +MU_URLS="$url6" +collected6="$(get_subscription_urls_for_section "$s6")" +if [ "$collected6" = "$url6" ]; then + echo 'mu-case6-single-option-1elem:OK' +else + echo "mu-case6-single-option-1elem(got '$collected6'):FAIL" +fi +merge_and_add "$s6" "$url6" +sing_box_cf_add_subscription_outbounds "$base_config" "$s6" "/tmp/netshift-mu-merged-$$-$s6.json" "[]" "[]" >/dev/null +out6="$SING_BOX_CF_LAST_CONFIG" +if [ "$MERGED_COUNT" = "1" ] && printf '%s' "$out6" | jq -e 'any(.outbounds[]; .tag=="Legacy")' >/dev/null 2>&1; then + echo 'mu-case6-backcompat-config:OK' +else + echo "mu-case6-backcompat-config(count=$MERGED_COUNT):FAIL" +fi +rm -f "/tmp/netshift-mu-merged-$$-$s6.json" + +rm -rf "$SUBSCRIPTION_CACHE_FOLDER" +echo 'DONE' +MUEOF + + sed -i "s|LIB_DIR|$lib|g; s|BIN_PATH|$bin|g" "$mu" + + sh "$mu" 2>/dev/null | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + *:SKIP) skip "$line" ;; + DONE) ;; + *) ;; + esac + done + + rm -f "$mu" } # ───────────────────────────────────────────────────────────────── From 904fd649112510734eb9f82b883534e91112a479 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Sun, 7 Jun 2026 12:15:44 +0300 Subject: [PATCH 60/75] =?UTF-8?q?=D1=80=D0=B5=D0=B2=D0=BE=D1=80=D0=BA=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 83 + .../memory/luci-frontend-developer.md | 200 + fe-app-netshift/locales/calls.json | 575 +-- fe-app-netshift/locales/netshift.pot | 493 +-- fe-app-netshift/locales/netshift.ru.po | 96 +- fe-app-netshift/src/helpers/showToast.ts | 2 +- .../tabs/dashboard/partials/renderSections.ts | 22 +- .../tabs/dashboard/partials/renderWidget.ts | 6 +- .../src/netshift/tabs/dashboard/styles.ts | 9 - .../partials/renderAvailableActions.ts | 2 +- .../diagnostic/partials/renderCheckSection.ts | 10 +- .../diagnostic/partials/renderSystemInfo.ts | 66 +- .../partials/renderWikiDisclaimer.ts | 1 + .../src/netshift/tabs/diagnostic/styles.ts | 17 - .../netshift/tabs/manager/initController.ts | 9 +- .../src/netshift/tabs/manager/styles.ts | 4 - fe-app-netshift/src/styles.ts | 80 +- .../resources/view/netshift/main.js | 3385 +++++++++-------- .../resources/view/netshift/netshift.js | 50 +- .../resources/view/netshift/section.js | 157 +- .../resources/view/netshift/settings.js | 309 +- luci-app-netshift/po/ru/netshift.po | 96 +- luci-app-netshift/po/templates/netshift.pot | 493 +-- 23 files changed, 3463 insertions(+), 2702 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 691c56fc..af007b82 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -449,6 +449,89 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> (console/process) errors and are NOT in the gate scope and NOT touched by FE tasks. Always verify FE lint with `eslint src --ext .ts,.tsx --max-warnings=0`, never `eslint .` — the latter is a false-alarm generator. + +## UI redesign "huge dump" -> card/tab (task-024..026, 2026-06-07, IN PROGRESS) + +- PROBLEM: operator says the UI is "one huge dump". Recon (2 explore agents) + localized it to the TWO CBI forms: Sections form (section.js, 36 flat options, + WORST = ~22 on screen for proxy/subscription) and Settings (settings.js, 27 + flat options). The 3 custom-rendered tabs (Dashboard/Diagnostic/Manager) are + already card/grid; MANAGER is the best-designed (cards+badges+descriptor + actions+overflow-safe CSS, pure cards.ts unit-tested) = the model to follow. +- DECISIVE RESEARCH (upstream LuCI form.js/ui.js, verified): CBI natively + supports intra-section option-group tabs via `section.tab(name,title,descr)` + + `section.taboption(tab, ...)`. HARD RULE: once a section has .tab(), EVERY + option must use taboption() — plain option() renders NOTHING (silent drop). + `depends()` works across tabs; a tab whose every option is depends-hidden + AUTO-HIDES from the strip (feature, exploit it). Tabs-inside-a-tabbed-Map is + supported: Map-level `.cbi-map-tabbed` and section-level + `.cbi-section-node-tabbed` are INDEPENDENT tab groups (ui.js initTabGroup runs + per group). Precedent: luci-app-firewall zones.js uses s.tab('general'/ + 'advanced'/'conntrack'/'extra') heavily. +- SectionValue(map,section,option,subsection_class,...args): embeds a whole + nested section inside an option slot (for card clusters). A SectionValue- + embedded subsection has parentoption!=null so it does NOT emit data-tab (won't + pollute the Map tab strip) — intended. For 025/026 the simpler/lower-risk + grouping is native section.tab() (used by firewall); reserve SectionValue for + inner card clusters only. +- STABLE CSS HOOKS for styling CBI as cards (target in styles.ts): .cbi-map-tabbed, + .cbi-section, .cbi-section-descr, .cbi-section-node[-tabbed], .cbi-value, + .cbi-value-title/-field/-description/-last, ul.cbi-tabmenu, li.cbi-tab / + li.cbi-tab-disabled, .cbi-tab-descr; ids #cbi-<config>-<sid>-<option>, + #container.<config>.<sid>.<tab>; attrs [data-section-id],[data-tab], + [data-field="cbid…"],[data-errors]. Tab STRIP DOM is generated by ui.js + (.cbi-tabmenu/.cbi-tab), pane/section DOM by form.js — style both. +- OPERATOR DECISIONS: Approach B-HYBRID (CBI stays the engine for load/save/ + depends/validation — zero loss; cards via CSS + section.tab grouping). Sections + form = 4 tabs (Connection/Subscription/Routing/Advanced) + unify the two + list-or-text triples into a smart list KEEPING all 4 UCI keys + the *_list_type + selectors. Fix subscription_group_by_countries RU-hardcode via _(). Dashboard + becomes FIRST tab. Mode = "all at once but ITERATE TO PERFECTION" (dev->review + ->fix->review until flawless; no rush). 3 staged task specs: 024 design-system + (.card + --ns-* tokens + warning/info toasts + Dashboard-first + renderButton + everywhere) FIRST because 025/026 reuse .card; then 025 (Sections) + 026 + (Settings) in PARALLEL (different files: section.js vs settings.js). +- task-024 DONE + code-reviewer APPROVED: .card on `:root,.cbi-map` with --ns-* + tokens (card-border/radius=4px/gap=10px/border-width=2px, success/warning/ + error/info layered over LuCI theme vars w/ hex fallbacks); MUST sit BEFORE the + per-tab style interpolations so colored-border modifiers win the cascade. The + four duplicated card boxes refactored to .card. showToast union now + success|error|warning|info. Dashboard=first (order: Dashboard·Sections· + Settings·Component Manager·Diagnostics). main.js rebuilt = big diff but PURELY + cosmetic esbuild module-reorder (new barrel import), export symbol set + byte-identical to HEAD, idempotent build — VERIFY export set unchanged when a + big-but-cosmetic main.js diff appears. yarn ci green (471 tests). +- REUSABLE VERIFICATION (reviewer): LuCI custom-tab lifecycle (TabService/ + coreService) keys off el.dataset.tab section NAME, never registration order — + reordering netshiftMap.section() calls is safe as long as section names are + unchanged. +- LANDMINE: NO browser / NO live LuCI backend in this env (Playwright "chrome not + found"; and LuCI needs a running rootfs anyway). VISUAL verification of the UI + is NOT possible here — devs+reviewer establish correctness STRUCTURALLY (CSS + cascade order, DOM-class analysis, taboption completeness, depends preserved). + Always flag "needs human eyeball before merge" for card/tab visual changes. +- task-025 (Sections, 36 opts->4 tabs Connection/Subscription/Routing/Advanced) + + task-026 (Settings, 27 opts->5 tabs DNS/Network/Lists&Updates/Dashboard-YACD/ + Advanced) BOTH DONE + code-reviewer APPROVED. Smart-list "unification" = VISUAL + grouping only (re-worded the *_list_type selector titles into group headings; + kept all 4 UCI keys + selectors) — a deeper single-widget merge was deferred as + risky (would touch UCI keys/validators). block_doh 4-paragraph help trimmed to + 1 sentence + caveat moved to the Advanced tab DESCRIPTION (section.tab 3rd arg). + subscription_group_by_countries RU-hardcode fixed to _() English msgids. +- REVIEW METHOD for tabbed-CBI conversions (reusable): grep `section.tab(`==N, + `section.taboption(`==total-opts, functional `section.option(`==0 (comment hits + ok); diff base-vs-current UCI-name SET (must be identical); diff full `.depends(` + text (must be identical); check validate/cfgvalue/load/filter/onchange counts == + base. This catches the silent-drop failure mode + any rename/depends regression. +- INTEGRATION (combined 024+025+026 tree): prettier(src) clean, eslint(src + --max-warnings=0) clean, vitest 471, main.js idempotent (md5 identical across 2 + tsup builds) + banner + `return baseclass.extend` intact, fe↔luci ru.po & pot + BYTE-IDENTICAL, no yarn pollution. Cosmetic double-blank-line inside the CSS + template literal is Prettier-IMMUNE (prettier doesn't format CSS-in-template) -> + collapse by hand if operator wants perfection, then REBUILD main.js (styles.ts + changed). Footprint ~21 files, main.js diff huge but cosmetic esbuild reorder. + Ready for human commit (agents never auto-commit). NEEDS HUMAN EYEBALL of the + rendered tabs before merge (no browser/LuCI in env). - M1 (smoke harness, confirmed by reviewer): the `subscription` category parses `mu-*`/token results in a `sh "$x" | while read; do pass/fail; done` PIPE, so pass/fail run in a SUBSHELL and DON'T propagate to summary()'s PASS/FAIL globals diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 32fa6ed9..147db4c4 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -451,3 +451,203 @@ append findings; keep under ~200 lines. locales/netshift.{pot,ru.po}, po/{templates/netshift.pot,ru/netshift.po}. - yarn classic 1.22.22 again but ran inner gate via node_modules/.bin (prettier/eslint/vitest/tsup) to be safe; yarn.lock unchanged, no .yarn/.yarnrc. + +## UI design-system foundation: .card + tokens + toasts (task-024) + +- DESIGN TOKENS (STABLE — task-025/026 reference these; do NOT rename) defined + in `src/styles.ts` `GlobalStyles` on `:root, .cbi-map`: + `--ns-card-border` (var(--background-color-low, lightgray)), + `--ns-card-border-width` (2px), `--ns-card-radius` (4px), `--ns-gap` (10px), + `--ns-card-padding` (var(--ns-gap)), `--ns-success`/`--ns-warning`/`--ns-error`/ + `--ns-info` (layered over success/warn/error-color-medium + primary-color-high + with hex fallbacks #28a745/#f0ad4e/#dc3545/#2196f3). +- `.card` primitive = `border: var(--ns-card-border-width) solid + var(--ns-card-border); border-radius: var(--ns-card-radius); padding: + var(--ns-card-padding); min-width:0`. Mirrors Manager's component card EXACTLY + (2px/4px/10px/min-width:0) — that's the standardised look, NOT the 1px/8px from + the spec's illustrative example. +- CASCADE RULE: `.card` MUST be defined in GlobalStyles BEFORE the + `${DashboardTab.styles}${DiagnosticTab.styles}${ManagerTab.styles}` + interpolations. The per-tab colored-border MODIFIERS (`.pdk_diagnostic_alert + --warning/--error/--loading/--success`, `__wiki--warning/--error`, outbound-grid + `--active`/`--selectable:hover`) are same-specificity single-class rules that + win ONLY via source order. Since injectGlobalStyles emits ONE `<style>` with + GlobalStyles, and the template renders tokens+`.card` first THEN the interpolated + tab CSS, the modifiers correctly override `.card`'s neutral border. (File + byte-offset of `.card` in main.js is LATER than the modifiers because + `DashboardTab.styles` is a separate `var stylesN` module — but runtime template + concatenation order is what matters, and that's correct.) +- REFACTOR PATTERN: removed the duplicated `border/border-radius/padding` (and + manager's `min-width:0`) from the per-tab `styles.ts`, added `class:'card …'` in + the RENDER `.ts`. Card boxes touched (more than the spec's "4" — there were + these render sites): dashboard renderWidget (3 states), renderSections + (failed/loading/default outbound-section + outbound-grid item), diagnostic + renderWikiDisclaimer (className array — prepend 'card'), renderAvailableActions, + renderSystemInfo, renderCheckSection (all 5 alert states incl. `--skipped` which + has NO modifier so it relies on `.card`), manager initController component. + `.card` adds `min-width:0` to dashboard/diagnostic boxes (was absent) — harmless + overflow hardening, visually identical. +- showToast union widened to `'success'|'error'|'warning'|'info'` + (showToast.ts:3). Added `.toast-warning`(--ns-warning) + `.toast-info`(--ns-info) + CSS; converted existing `.toast-success/.toast-error` to `var(--ns-success/error, + #hex)` (themeable, same fallback hex → visually identical). The + PREVIOUS memory note "showToast type is only success|error — use 'success' for + in-progress" is now SUPERSEDED: use `'info'` for in-progress, `'warning'` for + long/destructive-ish. Converted the 2 abuse sites in manager/initController.ts I + was already in: "Switching sing-box core…"→'info', "Updating NetShift…page will + reload"→'warning'. DEFERRED (not in touched files / debatable): manager line + ~155 "Latest version is unknown" still 'success' (check-result, not in-progress); + diagnostic/initController.ts had only 'error' toasts (nothing to fix). +- RAW BUTTON KILLED: dashboard renderSections.ts "Test latency" raw + `<button class="btn">` → `renderButton({text:_('Test latency'), onClick: + ()=>testLatency(), classNames:['dashboard-sections-grid-item-test-latency']})`. + renderButton already adds `btn`, so only the custom class goes in classNames. +- IMPORT-ORDER MAIN.JS CHURN (IMPORTANT): adding `import {renderButton} from + '../../../../partials'` into the DASHBOARD subtree (which previously never + imported the global `src/partials` barrel) makes esbuild REORDER ~every bundled + module block → a huge SYMMETRIC main.js diff (~1600/1600 lines) that is PURELY + cosmetic module reordering. Verified safe: build is IDEMPOTENT (same md5 twice), + banner intact, `return baseclass.extend({` intact, and the export-symbol SET is + BYTE-IDENTICAL to HEAD (diff /tmp/exports_old vs new = empty) → no barrel leak, + no new public API. Direct-path import (`…/partials/button/renderButton`) barely + reduced churn — the reorder is inherent to introducing the cross-subtree dep, so + I kept the barrel import for consistency with the 3 diagnostic callers. When a + reviewer sees a giant main.js diff for a tiny TS change, CHECK export-set + equality + idempotency before worrying. +- TAB REORDER: netshift.js (hand-written, edit directly) — moved the Dashboard + `form.TypedSection` block to FIRST. New order: Dashboard · Sections · Settings · + Component Manager · Diagnostics. ONLY block order changed; all 5 sections + their + cfgsections/anonymous/addremove wiring identical. coreService/TabService track by + `data-tab` (the active section name e.g. `current==='dashboard'`), NOT + registration index (tab.service.ts getActiveTabId reads `.cbi-tab:not( + .cbi-tab-disabled)` dataset.tab; dashboard initController keys on + `tabService.current==='dashboard'`) → reorder is SAFE, tracking unaffected. +- VISUAL VERIFY caveat: no chromium available in this env (playwright launch + failed: chrome not found), so screenshots were NOT possible. Verified instead by + CSS-cascade reasoning + programmatic checks: `.card` precedes modifiers in the + runtime-concatenated GlobalStyles, no `background-color-low` base border remains + in any per-tab styles.ts (all neutral borders now come from `.card`), colored + modifiers keep their 2px width matching `.card`. FLAG: visual confirmation is + reasoned, not screenshotted. +- yarn classic 1.22.22; ran gate via node_modules/.bin (prettier --write src clean + / eslint --max-warnings=0 / vitest 471 pass / tsup build). yarn.lock unchanged, + no `.yarn`/`.yarnrc.yml`. No locales change (no NEW user-facing literals — the + switching/updating toast strings already existed). + +## task-025 — section.js → 4 native CBI tabs (taboption) + +- CBI native tabs: `section.tab('name', _('Title'), _('descr'))` defines a tab, + then EVERY field MUST be `section.taboption('name', form.X, 'key', ...)`. HARD + RULE confirmed: once a section has `.tab()`, any leftover `section.option(...)` + silently renders nothing. Verified count: 36 taboption, 0 plain option (the + only `section.option(` grep hit was my own comment line). +- Conversion is mechanical & low-risk: only the constructor call line changes + (`section.option(\n form.X,` → `section.taboption(\n "tab",\n form.X,`). + All `.depends()` (33), `.validate` (17), `.value()`, defaults, placeholders, + and the `community_lists.onchange` (REGIONAL_OPTIONS/ALLOWED_WITH_RUSSIA_INSIDE + /DOMAIN_LIST_OPTIONS/getUIElement) stayed byte-identical. depends() works + across tabs; an all-depends-hidden tab auto-hides from the strip (Subscription + tab hides for proxy/url) — desired, no extra code. +- `widgets.DeviceSelect` (`interface`) works fine inside a taboption — just pass + the widget class as the 2nd arg after the tab name. +- Tab map (4 tabs, 36 fields): connection=11, subscription=10, routing=12, + advanced=3. +- SMART-LIST UNIFICATION: did the LOW-RISK visual grouping (NOT a single-widget + merge). Kept all 4 UCI keys + 2 *_list_type selectors. Achieved "one control" + feel by renaming the two list-type selector TITLES to group headings + ("Custom domains"/"Custom subnets") with descriptions naming the modes; + depends() already shows only the chosen input below. Deeper merge deferred + (would risk UCI/validator changes). NOTE: renaming a selector title drops its + old msgid from catalogs — fill the new ones. +- RU-HARDCODE FIX: `subscription_group_by_countries` had `_("Группировать по + странам")` as the SOURCE literal (msgid). Replaced with English `_("Group by + countries")` + English descr; moved the Russian into the ru.po msgstr. After + this the Cyrillic appears ONLY as msgstr, never as msgid. +- section.js is NOT in the `yarn ci` prettier scope (CI formats only `src`). + section.js uses DOUBLE QUOTES (LuCI convention) and does NOT pass the project + `.prettierrc` (singleQuote) — confirmed the ORIGINAL also failed prettier. + So: match the file's existing double-quote/2-space style; do NOT run prettier + on section.js (it would fight the whole file). +- i18n flow: `node extract-calls.js && node generate-pot.js && node + generate-po.js ru && node distribute-locales.js`. generate-po keys by msgid & + carries forward old msgstr; NEW/renamed msgids land empty → fill them in + fe-app-netshift/locales/netshift.ru.po, then RE-RUN distribute-locales.js to + copy into luci-app-netshift/po/{ru/netshift.po, templates/netshift.pot}. + Verify byte-consistency with `diff -q` (both fe↔luci pairs). 13 new strings + this task; all ru filled; 0 empty msgstr after. +- main.js drift rule confirmed: section.js-only + catalog changes need NO main.js + rebuild. I touched styles.ts so rebuilt — the ONLY main.js delta vs the + task-024 baseline was my new CSS block (#cbi-netshift-section + .cbi-section-node-tabbed card + ul.cbi-tabmenu margin). Build reproducible. +- styles.ts: reused task-024 --ns-* tokens; added `#cbi-netshift-section + .cbi-section-node-tabbed` (card border/radius/padding) + `ul.cbi-tabmenu` + margin. Existing h3-hide (`> h3:nth-child(1)`) and remove-button hack + (`> .cbi-section-remove { margin-bottom:-32px }`) left intact (new rules added + after them; both still valid — remove button is a direct child, unaffected by + the tabbed pane styling). +- VISUAL VERIFY caveat persists: no browser in env. Confirmed structurally + (taboption count/mapping, depends/validate counts == original, onchange grep + intact, catalog diff). FLAG for human: actual tab-strip/card rendering + + auto-hide behaviour of the Subscription/Advanced tabs not screenshot-verified. + +## task-026 — settings.js → 5 native CBI tabs (taboption) + +- Same mechanics as task-025. Converted ALL 27 settings options to + `section.taboption('tab', form.X, 'key', …)`. Verified: 27 taboption, 0 plain + `section.option(` (the 1 grep hit is my comment line). Tab map (5 tabs, 27): + dns(6)=dns_type,dns_server,bootstrap_dns_server,dns_via_outbound, + dns_outbound_section,dns_rewrite_ttl · network(6)=source_network_interfaces, + enable_output_network_interface,output_network_interface, + enable_badwan_interface_monitoring,badwan_monitored_interfaces, + badwan_reload_delay · lists(4)=update_interval,download_lists_via_proxy, + download_lists_via_proxy_section,routing_excluded_ips · yacd(3)=enable_yacd, + enable_yacd_wan_access,yacd_secret_key · advanced(8)=disable_quic, + dont_touch_dhcp,exclude_ntp,block_doh,enable_ipv6,config_path,cache_path, + log_level. +- YACD DECISION: kept as its OWN tab (3 fields), NOT folded into Advanced. + Rationale: self-contained feature w/ clean depends() chain + (enable_yacd→wan_access→secret_key); folding into an 11-field Advanced would + recreate the wall. Tab title is `_("Dashboard")` (already-existing msgid), + internal tab name "yacd". Documented. +- All 7 depends() preserved verbatim (only line order changed — irrelevant): + dns_outbound_section dep dns_via_outbound=1; output_network_interface dep + enable_output_network_interface=1; badwan_monitored_interfaces + + badwan_reload_delay dep enable_badwan_interface_monitoring=1; + enable_yacd_wan_access dep enable_yacd=1; yacd_secret_key dep + enable_yacd_wan_access=1; download_lists_via_proxy_section dep + download_lists_via_proxy=1. 6 validators + 3 custom widgets (2 DeviceSelect, + 1 NetworkSelect) intact; cfgvalue/load section-picker closures unchanged. +- HELP TRIM: block_doh was a 4-paragraph `_()+ " " +_()…` concat. Replaced with + ONE single-literal description "Block direct connections to known public DoH + servers (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex) so apps cannot + bypass router DNS filtering." The caveat ("enable only after switching to + UDP/DoT") moved into the ADVANCED tab description (section.tab 3rd arg). + enable_ipv6's 2-sentence concat LEFT inline (short, the 2nd sentence is a + genuine 1-line caveat; not bloating) — documented choice. +- BACKTICK TRAP IN styles.ts: GlobalStyles is a template literal. Putting a + backtick inside a CSS COMMENT (e.g. `#cbi-... > h3`) prematurely closes the + template → ESLint "Parsing error: ',' expected". NEVER use backticks anywhere + inside the styles.ts CSS string, even in comments. (Cost me one lint cycle.) +- styles.ts: added `#cbi-netshift-settings .cbi-section-node-tabbed` (card + border/radius/padding/min-width:0) + `#cbi-netshift-settings ul.cbi-tabmenu` + (margin-bottom:var(--ns-gap)) — exact mirror of the task-025 section block, + reusing task-024 --ns-* tokens (did NOT redefine tokens). The existing + `#cbi-netshift-settings > h3 { display:none }` rule stays valid (added new + rules after it). main.js delta = exactly these 2 CSS rules; build IDEMPOTENT + (md5 a7300a2… across 3 builds), banner + `return baseclass.extend` intact, + no new export symbol (only-loss vs HEAD is task-024's `styles` leak removal, + not mine). +- i18n: msgid delta = 9 added (tab titles "DNS"/"Network"/"Lists & Updates" — + "Dashboard"+"Advanced" already existed; 4 tab descriptions; 1 reworded + block_doh) / 4 removed (old block_doh fragments). Filled 9 ru msgstr in SOURCE + locales/netshift.ru.po then `node distribute-locales.js`. fe↔luci ru.po AND + pot byte-identical (diff -q); all LF; valid UTF-8 w/ Cyrillic; 0 empty + non-header msgstr. Ran scripts via `node {extract-calls,generate-pot, + generate-po ru,distribute-locales}.js` (generate-po reported 335/339 but 9 + were genuinely new — its count metric differs). +- yarn classic 1.22.22; ran gate via node_modules/.bin (prettier/eslint/vitest/ + tsup). yarn.lock unchanged, no .yarn/.yarnrc.yml. NB: working tree already + carried UNCOMMITTED task-024 + task-025 changes (showToast, dashboard/diag/ + manager styles+renders, section.js, netshift.js, #cbi-netshift-section CSS) — + so `git diff -- src` is large but only my settings block + the 4 catalogs + + main.js belong to task-026. format reported all-unchanged → no new churn. diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index f49986cf..bfc989dd 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -38,7 +38,21 @@ "call": "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged.", "key": "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123" + ] + }, + { + "call": "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", + "key": "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573" + ] + }, + { + "call": "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", + "key": "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661" ] }, { @@ -49,45 +63,46 @@ ] }, { - "call": "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers.", - "key": "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers.", + "call": "Advanced", + "key": "Advanced", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:469" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:31", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:36" ] }, { "call": "Allow insecure TLS for subscription fetch", "key": "Allow insecure TLS for subscription fetch", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148" ] }, { "call": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "key": "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:420" ] }, { "call": "Applicable for SOCKS and Shadowsocks proxy", "key": "Applicable for SOCKS and Shadowsocks proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:314" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:356" ] }, { "call": "At least one valid domain must be specified. Comments-only content is not allowed.", "key": "At least one valid domain must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:633" ] }, { "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:657" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:722" ] }, { @@ -98,17 +113,17 @@ ] }, { - "call": "Block direct connections to known public DNS-over-HTTPS (DoH) servers.", - "key": "Block direct connections to known public DNS-over-HTTPS (DoH) servers.", + "call": "Block direct connections to known public DoH servers (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex) so apps cannot bypass router DNS filtering.", + "key": "Block direct connections to known public DoH servers (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex) so apps cannot bypass router DNS filtering.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:463" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:480" ] }, { "call": "Block DoH Servers", "key": "Block DoH Servers", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:462" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:479" ] }, { @@ -122,7 +137,7 @@ "call": "Bootstrap DNS server", "key": "Bootstrap DNS server", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80" ] }, { @@ -143,14 +158,14 @@ "call": "Cache File Path", "key": "Cache File Path", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:399" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:517" ] }, { "call": "Cache file path cannot be empty", "key": "Cache file path cannot be empty", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:413" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:531" ] }, { @@ -222,7 +237,7 @@ "call": "Community Lists", "key": "Community Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479" ] }, { @@ -236,7 +251,7 @@ "call": "Config File Path", "key": "Config File Path", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:386" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:503" ] }, { @@ -250,21 +265,35 @@ "call": "Configuration Type", "key": "Configuration Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:23" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:51" + ] + }, + { + "call": "Connection", + "key": "Connection", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:16" ] }, { "call": "Connection Type", "key": "Connection Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:12" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:39" + ] + }, + { + "call": "Connection type, transport and DNS resolver for this section", + "key": "Connection type, transport and DNS resolver for this section", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:17" ] }, { "call": "Connection URL", "key": "Connection URL", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:26" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:54" ] }, { @@ -296,32 +325,47 @@ "src/netshift/tabs/dashboard/partials/renderWidget.ts:22" ] }, + { + "call": "Custom domains", + "key": "Custom domains", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:572" + ] + }, + { + "call": "Custom subnets", + "key": "Custom subnets", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:660" + ] + }, { "call": "Dashboard", "key": "Dashboard", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:98" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:39", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:31" ] }, { "call": "Dashboard currently unavailable", "key": "Dashboard currently unavailable", "places": [ - "src/netshift/tabs/dashboard/partials/renderSections.ts:19" + "src/netshift/tabs/dashboard/partials/renderSections.ts:20" ] }, { "call": "Delay in milliseconds before reloading NetShift after interface UP", "key": "Delay in milliseconds before reloading NetShift after interface UP", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:265" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:310" ] }, { "call": "Delay value cannot be empty", "key": "Delay value cannot be empty", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:272" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:317" ] }, { @@ -342,7 +386,7 @@ "call": "Diagnostics", "key": "Diagnostics", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:68" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:98" ] }, { @@ -356,29 +400,36 @@ "call": "Disable QUIC", "key": "Disable QUIC", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:312" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:445" ] }, { "call": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "key": "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:313" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:446" ] }, { "call": "Disabled", "key": "Disabled", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:522", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665" ] }, { "call": "Disables TLS certificate verification when downloading the subscription.", "key": "Disables TLS certificate verification when downloading the subscription.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149" + ] + }, + { + "call": "DNS", + "key": "DNS", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16" ] }, { @@ -392,46 +443,46 @@ "call": "DNS outbound section", "key": "DNS outbound section", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:79" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:116" ] }, { "call": "DNS over HTTPS (DoH)", "key": "DNS over HTTPS (DoH)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:445", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48" ] }, { "call": "DNS over TLS (DoT)", "key": "DNS over TLS (DoT)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49" ] }, { "call": "DNS Protocol Type", "key": "DNS Protocol Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:442", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45" ] }, { "call": "DNS Rewrite TTL", "key": "DNS Rewrite TTL", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:113" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:151" ] }, { "call": "DNS Server", "key": "DNS Server", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:409", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:456", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58" ] }, { @@ -445,21 +496,28 @@ "call": "Do not panic, everything can be fixed, just...", "key": "Do not panic, everything can be fixed, just...", "places": [ - "src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26" + "src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:27" + ] + }, + { + "call": "Domain and subnet lists that decide which traffic uses this section", + "key": "Domain and subnet lists that decide which traffic uses this section", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:27" ] }, { "call": "Domain Resolver", "key": "Domain Resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431" ] }, { "call": "Dont Touch My DHCP!", "key": "Dont Touch My DHCP!", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:377" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:457" ] }, { @@ -481,37 +539,37 @@ "call": "Download Lists via Proxy/VPN", "key": "Download Lists via Proxy/VPN", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:335" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:340" ] }, { "call": "Download Lists via specific proxy section", "key": "Download Lists via specific proxy section", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:344" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:350" ] }, { "call": "Downloading all lists via specific Proxy/VPN", "key": "Downloading all lists via specific Proxy/VPN", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:336", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:345" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:341", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:351" ] }, { "call": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "key": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205" ] }, { "call": "Dynamic List", "key": "Dynamic List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:523", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:578", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:666" ] }, { @@ -525,182 +583,182 @@ "call": "Enable built-in DNS resolver for domains handled by this section", "key": "Enable built-in DNS resolver for domains handled by this section", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432" ] }, { "call": "Enable DNS resolve to get real IP when routing", "key": "Enable DNS resolve to get real IP when routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:826" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:899" ] }, { "call": "Enable IPv6 Support", "key": "Enable IPv6 Support", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:483" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:491" ] }, { "call": "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support.", "key": "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:484" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:492" ] }, { "call": "Enable Mixed Proxy", "key": "Enable Mixed Proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:797" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:868" ] }, { "call": "Enable Output Network Interface", "key": "Enable Output Network Interface", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:171" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:212" ] }, { "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:798" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:869" ] }, { "call": "Enable YACD", "key": "Enable YACD", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:280" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:409" ] }, { "call": "Enable YACD WAN Access", "key": "Enable YACD WAN Access", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:289" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:419" ] }, { "call": "Enter complete outbound configuration in JSON format", "key": "Enter complete outbound configuration in JSON format", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:69" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:99" ] }, { "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:558" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:615" ] }, { "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588" ] }, { "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676" ] }, { "call": "Every 1 minute", "key": "Every 1 minute", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:233" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:272" ] }, { "call": "Every 12 hours", "key": "Every 12 hours", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:137" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:170" ] }, { "call": "Every 3 hours", "key": "Every 3 hours", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:135" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168" ] }, { "call": "Every 3 minutes", "key": "Every 3 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:234" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:273" ] }, { "call": "Every 30 minutes", "key": "Every 30 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:133" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166" ] }, { "call": "Every 30 seconds", "key": "Every 30 seconds", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:271" ] }, { "call": "Every 5 minutes", "key": "Every 5 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:235" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:274" ] }, { "call": "Every 6 hours", "key": "Every 6 hours", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:136" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169" ] }, { "call": "Every day", "key": "Every day", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:138" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:171" ] }, { "call": "Every hour", "key": "Every hour", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:134" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:167" ] }, { "call": "Exclude NTP", "key": "Exclude NTP", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:451" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:467" ] }, { "call": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "key": "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:452" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:468" ] }, { "call": "Exclude servers by keyword", "key": "Exclude servers by keyword", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204" ] }, { @@ -723,10 +781,10 @@ "src/netshift/tabs/manager/initController.ts:122", "src/netshift/tabs/manager/initController.ts:132", "src/netshift/tabs/manager/initController.ts:166", - "src/netshift/tabs/manager/initController.ts:197", - "src/netshift/tabs/manager/initController.ts:201", - "src/netshift/tabs/manager/initController.ts:234", - "src/netshift/tabs/manager/initController.ts:238" + "src/netshift/tabs/manager/initController.ts:194", + "src/netshift/tabs/manager/initController.ts:198", + "src/netshift/tabs/manager/initController.ts:231", + "src/netshift/tabs/manager/initController.ts:235" ] }, { @@ -743,7 +801,7 @@ "call": "Fully Routed IPs", "key": "Fully Routed IPs", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:770" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:840" ] }, { @@ -764,14 +822,28 @@ "call": "Global Proxy", "key": "Global Proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:366" + ] + }, + { + "call": "Group by countries", + "key": "Group by countries", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179" + ] + }, + { + "call": "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag", + "key": "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180" ] }, { "call": "How often to automatically update the subscription", "key": "How often to automatically update the subscription", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164" ] }, { @@ -785,7 +857,7 @@ "call": "Include servers by keyword", "key": "Include servers by keyword", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:157" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:192" ] }, { @@ -808,21 +880,21 @@ "call": "Interface Monitoring", "key": "Interface Monitoring", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:232" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:275" ] }, { "call": "Interface Monitoring Delay", "key": "Interface Monitoring Delay", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:264" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:309" ] }, { "call": "Interface monitoring for Bad WAN", "key": "Interface monitoring for Bad WAN", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:233" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:276" ] }, { @@ -1211,7 +1283,7 @@ "call": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "key": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:158" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:193" ] }, { @@ -1240,28 +1312,42 @@ "call": "List Update Frequency", "key": "List Update Frequency", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:323" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:327" + ] + }, + { + "call": "List update schedule, download routing, and routing exclusions", + "key": "List update schedule, download routing, and routing exclusions", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:27" + ] + }, + { + "call": "Lists & Updates", + "key": "Lists & Updates", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:26" ] }, { "call": "Local Domain Lists", "key": "Local Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:678" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744" ] }, { "call": "Local Subnet Lists", "key": "Local Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768" ] }, { "call": "Log Level", "key": "Log Level", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:435" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:554" ] }, { @@ -1285,25 +1371,32 @@ "src/netshift/tabs/dashboard/initController.ts:311" ] }, + { + "call": "Mixed proxy and DNS resolution tuning", + "key": "Mixed proxy and DNS resolution tuning", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:32" + ] + }, { "call": "Mixed Proxy Port", "key": "Mixed Proxy Port", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:810" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:882" ] }, { "call": "Monitored Interfaces", "key": "Monitored Interfaces", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:241" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:285" ] }, { "call": "Must be a number in the range of 50 - 1000", "key": "Must be a number in the range of 50 - 1000", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309" ] }, { @@ -1324,21 +1417,28 @@ "call": "NetShift updated, version:", "key": "NetShift updated, version:", "places": [ - "src/netshift/tabs/manager/initController.ts:226" + "src/netshift/tabs/manager/initController.ts:223" ] }, { "call": "NetShift will not modify your DHCP configuration", "key": "NetShift will not modify your DHCP configuration", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:378" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:458" + ] + }, + { + "call": "Network", + "key": "Network", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:21" ] }, { "call": "Network Interface", "key": "Network Interface", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:340" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384" ] }, { @@ -1385,18 +1485,11 @@ "src/netshift/tabs/diagnostic/diagnostic.store.ts:88" ] }, - { - "call": "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT.", - "key": "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT.", - "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:473" - ] - }, { "call": "Only one section can be global at a time.", "key": "Only one section can be global at a time.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:375" ] }, { @@ -1410,14 +1503,14 @@ "call": "Outbound Config", "key": "Outbound Config", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:30" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:58" ] }, { "call": "Outbound Configuration", "key": "Outbound Configuration", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:68" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:98" ] }, { @@ -1432,7 +1525,7 @@ "call": "Output Network Interface", "key": "Output Network Interface", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:180" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:222" ] }, { @@ -1446,21 +1539,21 @@ "call": "Path must be absolute (start with /)", "key": "Path must be absolute (start with /)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:417" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:535" ] }, { "call": "Path must contain at least one directory (like /tmp/cache.db)", "key": "Path must contain at least one directory (like /tmp/cache.db)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:426" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:544" ] }, { "call": "Path must end with cache.db", "key": "Path must end with cache.db", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:421" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:539" ] }, { @@ -1474,11 +1567,18 @@ "src/netshift/tabs/diagnostic/diagnostic.store.ts:136" ] }, + { + "call": "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT.", + "key": "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:37" + ] + }, { "call": "Proxy Configuration URL", "key": "Proxy Configuration URL", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:37" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:66" ] }, { @@ -1499,28 +1599,28 @@ "call": "Regional options cannot be used together", "key": "Regional options cannot be used together", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513" ] }, { "call": "Remote Domain Lists", "key": "Remote Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:724" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:792" ] }, { "call": "Remote Subnet Lists", "key": "Remote Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:747" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:816" ] }, { "call": "Resolve real IP for routing", "key": "Resolve real IP for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:825" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:898" ] }, { @@ -1534,14 +1634,14 @@ "call": "Route all unmatched traffic through this section's outbound.", "key": "Route all unmatched traffic through this section's outbound.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:367" ] }, { "call": "Route main DNS through proxy/VPN", "key": "Route main DNS through proxy/VPN", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:68" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:104" ] }, { @@ -1558,11 +1658,18 @@ "src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:50" ] }, + { + "call": "Routing", + "key": "Routing", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:26" + ] + }, { "call": "Routing Excluded IPs", "key": "Routing Excluded IPs", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:494" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:384" ] }, { @@ -1611,148 +1718,134 @@ "call": "Russia inside restrictions", "key": "Russia inside restrictions", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:484" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532" ] }, { "call": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "key": "Secret key for authenticating remote access to YACD when WAN access is enabled.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:302" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:433" ] }, { "call": "Sections", "key": "Sections", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:39" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:54" ] }, { "call": "Select a predefined list for routing", "key": "Select a predefined list for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:480" ] }, { "call": "Select between VPN and Proxy connection methods for traffic routing", "key": "Select between VPN and Proxy connection methods for traffic routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:13" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:40" ] }, { "call": "Select DNS protocol to use", "key": "Select DNS protocol to use", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:13" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:46" ] }, { "call": "Select how often the domain or subnet lists are updated automatically", "key": "Select how often the domain or subnet lists are updated automatically", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:324" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:328" ] }, { "call": "Select how to configure the proxy", "key": "Select how to configure the proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:24" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:52" ] }, { "call": "Select network interface for VPN connection", "key": "Select network interface for VPN connection", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:341" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385" ] }, { "call": "Select or enter DNS server address", "key": "Select or enter DNS server address", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:410", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59" ] }, { "call": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "key": "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:400" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:518" ] }, { "call": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "key": "Select path for sing-box config file. Change this ONLY if you know what you are doing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:387" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:504" ] }, { "call": "Select the DNS protocol type for the domain resolver", "key": "Select the DNS protocol type for the domain resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:397" - ] - }, - { - "call": "Select the list type for adding custom domains", - "key": "Select the list type for adding custom domains", - "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:520" - ] - }, - { - "call": "Select the list type for adding custom subnets", - "key": "Select the list type for adding custom subnets", - "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:600" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:443" ] }, { "call": "Select the log level for sing-box", "key": "Select the log level for sing-box", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:436" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:555" ] }, { "call": "Select the network interface from which the traffic will originate", "key": "Select the network interface from which the traffic will originate", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:135" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:175" ] }, { "call": "Select the network interface to which the traffic will originate", "key": "Select the network interface to which the traffic will originate", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:181" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:223" ] }, { "call": "Select the WAN interfaces to be monitored", "key": "Select the WAN interfaces to be monitored", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:242" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:286" ] }, { "call": "Selector", "key": "Selector", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:27" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:55" ] }, { "call": "Selector Proxy Links", "key": "Selector Proxy Links", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216" ] }, { @@ -1766,7 +1859,7 @@ "call": "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct.", "key": "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:69" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:105" ] }, { @@ -1780,7 +1873,7 @@ "call": "Settings", "key": "Settings", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:52" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:67" ] }, { @@ -1809,7 +1902,7 @@ "call": "Sing-box core changed, version:", "key": "Sing-box core changed, version:", "places": [ - "src/netshift/tabs/manager/initController.ts:190" + "src/netshift/tabs/manager/initController.ts:187" ] }, { @@ -1847,47 +1940,54 @@ "src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts:67" ] }, + { + "call": "Source and output interfaces, and Bad WAN interface monitoring", + "key": "Source and output interfaces, and Bad WAN interface monitoring", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:22" + ] + }, { "call": "Source Network Interface", "key": "Source Network Interface", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:134" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:174" ] }, { "call": "Specify a local IP address to be excluded from routing", "key": "Specify a local IP address to be excluded from routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:495" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:385" ] }, { "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:771" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:841" ] }, { "call": "Specify remote URLs to download and use domain lists", "key": "Specify remote URLs to download and use domain lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:725" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793" ] }, { "call": "Specify remote URLs to download and use subnet lists", "key": "Specify remote URLs to download and use subnet lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:748" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:817" ] }, { "call": "Specify the path to the list file located on the router filesystem", "key": "Specify the path to the list file located on the router filesystem", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:679", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:745", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:769" ] }, { @@ -1908,21 +2008,29 @@ "call": "Subscription", "key": "Subscription", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:29" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:21", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57" + ] + }, + { + "call": "Subscription feeds, server filters and URLTest tuning", + "key": "Subscription feeds, server filters and URLTest tuning", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:22" ] }, { "call": "Subscription Update Interval", "key": "Subscription Update Interval", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:130" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163" ] }, { "call": "Subscription URLs", "key": "Subscription URLs", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:91" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122" ] }, { @@ -1950,7 +2058,7 @@ "call": "Switching sing-box core, this may take a few minutes…", "key": "Switching sing-box core, this may take a few minutes…", "places": [ - "src/netshift/tabs/manager/initController.ts:178" + "src/netshift/tabs/manager/initController.ts:177" ] }, { @@ -1964,7 +2072,7 @@ "call": "System information", "key": "System information", "places": [ - "src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts:21" + "src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts:24" ] }, { @@ -1978,64 +2086,57 @@ "call": "Test latency", "key": "Test latency", "places": [ - "src/netshift/tabs/dashboard/partials/renderSections.ts:108" + "src/netshift/tabs/dashboard/partials/renderSections.ts:104" ] }, { "call": "Text List", "key": "Text List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:524", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:604" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:579", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:667" ] }, { "call": "The DNS server used to look up the IP address of an upstream DNS server", "key": "The DNS server used to look up the IP address of an upstream DNS server", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:46" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:81" ] }, { "call": "The interval between connectivity tests", "key": "The interval between connectivity tests", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269" ] }, { "call": "The maximum difference in response times (ms) allowed when comparing servers", "key": "The maximum difference in response times (ms) allowed when comparing servers", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:244" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284" ] }, { "call": "The URL used to test server connectivity", "key": "The URL used to test server connectivity", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:276" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:317" ] }, { "call": "This is a security trade-off: an attacker could intercept the fetch.", "key": "This is a security trade-off: an attacker could intercept the fetch.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121" - ] - }, - { - "call": "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS.", - "key": "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS.", - "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:465" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153" ] }, { "call": "Time in seconds for DNS record caching (default: 60)", "key": "Time in seconds for DNS record caching (default: 60)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:114" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:152" ] }, { @@ -2056,36 +2157,36 @@ "call": "Troubleshooting", "key": "Troubleshooting", "places": [ - "src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:25" + "src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26" ] }, { "call": "TTL must be a positive number", "key": "TTL must be a positive number", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:125" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:163" ] }, { "call": "TTL value cannot be empty", "key": "TTL value cannot be empty", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:120" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:158" ] }, { "call": "UDP (Unprotected DNS)", "key": "UDP (Unprotected DNS)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50" ] }, { "call": "UDP over TCP", "key": "UDP over TCP", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:313" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:355" ] }, { @@ -2142,7 +2243,7 @@ "call": "Updating NetShift, this may take a few minutes; the page will reload…", "key": "Updating NetShift, this may take a few minutes; the page will reload…", "places": [ - "src/netshift/tabs/manager/initController.ts:217" + "src/netshift/tabs/manager/initController.ts:214" ] }, { @@ -2153,6 +2254,13 @@ "src/netshift/tabs/dashboard/initController.ts:271" ] }, + { + "call": "Upstream and bootstrap DNS resolvers, and optional DNS-over-proxy", + "key": "Upstream and bootstrap DNS resolvers, and optional DNS-over-proxy", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17" + ] + }, { "call": "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", "key": "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", @@ -2171,98 +2279,84 @@ "call": "URLTest", "key": "URLTest", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:28" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:56" ] }, { "call": "URLTest Check Interval", "key": "URLTest Check Interval", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:268" ] }, { "call": "URLTest Proxy Links", "key": "URLTest Proxy Links", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:242" ] }, { "call": "URLTest Testing URL", "key": "URLTest Testing URL", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:275" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316" ] }, { "call": "URLTest Tolerance", "key": "URLTest Tolerance", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283" ] }, { "call": "Use only for IP-host panels that serve an invalid or self-signed certificate.", "key": "Use only for IP-host panels that serve an invalid or self-signed certificate.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:151" ] }, { "call": "Use this only when the router has working IPv6 connectivity.", "key": "Use this only when the router has working IPv6 connectivity.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:486" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:494" ] }, { "call": "Use with Exclusion sections to route specific domains directly.", "key": "Use with Exclusion sections to route specific domains directly.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:330" - ] - }, - { - "call": "User Domain List Type", - "key": "User Domain List Type", - "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:519" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373" ] }, { "call": "User Domains", "key": "User Domains", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:531" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587" ] }, { "call": "User Domains List", "key": "User Domains List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:557" - ] - }, - { - "call": "User Subnet List Type", - "key": "User Subnet List Type", - "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:599" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:614" ] }, { "call": "User Subnets", "key": "User Subnets", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675" ] }, { "call": "User Subnets List", "key": "User Subnets List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:637" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702" ] }, { @@ -2294,15 +2388,15 @@ "call": "Validation errors:", "key": "Validation errors:", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:669" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:647", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734" ] }, { "call": "Version", "key": "Version", "places": [ - "src/netshift/tabs/manager/initController.ts:315" + "src/netshift/tabs/manager/initController.ts:312" ] }, { @@ -2317,72 +2411,65 @@ "call": "Visit Wiki", "key": "Visit Wiki", "places": [ - "src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:31" + "src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:32" ] }, { "call": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:38", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:217", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243" ] }, { "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515" ] }, { "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:486" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:534" ] }, { "call": "When enabled, traffic not matching any other section's lists will go through this proxy.", "key": "When enabled, traffic not matching any other section's lists will go through this proxy.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369" ] }, { "call": "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound.", "key": "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:117" ] }, { "call": "YACD Secret Key", "key": "YACD Secret Key", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:301" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:432" ] }, { - "call": "You can select Output Network Interface, by default autodetect", - "key": "You can select Output Network Interface, by default autodetect", + "call": "YACD web dashboard access and remote-access protection", + "key": "YACD web dashboard access and remote-access protection", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:172" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:32" ] }, { - "call": "Группировать по странам", - "key": "Группировать по странам", - "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:145" - ] - }, - { - "call": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", - "key": "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", + "call": "You can select Output Network Interface, by default autodetect", + "key": "You can select Output Network Interface, by default autodetect", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:146" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:213" ] } ] \ No newline at end of file diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index 3c8bcd91..72d86fbc 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 07:23+0300\n" -"PO-Revision-Date: 2026-06-07 07:23+0300\n" +"POT-Creation-Date: 2026-06-07 08:56+0300\n" +"PO-Revision-Date: 2026-06-07 08:56+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -36,35 +36,44 @@ msgstr "" msgid "Active Connections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123 msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573 +msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661 +msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 msgid "Additional marking rules found" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:469 -msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:31 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:36 +msgid "Advanced" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148 msgid "Allow insecure TLS for subscription fetch" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:420 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:314 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:356 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:633 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:657 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:722 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -72,11 +81,11 @@ msgstr "" msgid "Available actions" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:463 -msgid "Block direct connections to known public DNS-over-HTTPS (DoH) servers." +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:480 +msgid "Block direct connections to known public DoH servers (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex) so apps cannot bypass router DNS filtering." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:462 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:479 msgid "Block DoH Servers" msgstr "" @@ -84,7 +93,7 @@ msgstr "" msgid "Bootsrap DNS" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80 msgid "Bootstrap DNS server" msgstr "" @@ -96,11 +105,11 @@ msgstr "" msgid "Browser is using FakeIP correctly" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:399 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:517 msgid "Cache File Path" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:413 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:531 msgid "Cache file path cannot be empty" msgstr "" @@ -145,7 +154,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479 msgid "Community Lists" msgstr "" @@ -153,7 +162,7 @@ msgstr "" msgid "Component Manager" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:386 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:503 msgid "Config File Path" msgstr "" @@ -161,15 +170,23 @@ msgstr "" msgid "Configuration for NetShift service" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:23 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:51 msgid "Configuration Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:12 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:16 +msgid "Connection" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:39 msgid "Connection Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:26 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:17 +msgid "Connection type, transport and DNS resolver for this section" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:54 msgid "Connection URL" msgstr "" @@ -190,19 +207,28 @@ msgstr "" msgid "Currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:98 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:572 +msgid "Custom domains" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:660 +msgid "Custom subnets" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:39 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:31 msgid "Dashboard" msgstr "" -#: src/netshift/tabs/dashboard/partials/renderSections.ts:19 +#: src/netshift/tabs/dashboard/partials/renderSections.ts:20 msgid "Dashboard currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:265 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:310 msgid "Delay in milliseconds before reloading NetShift after interface UP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:272 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:317 msgid "Delay value cannot be empty" msgstr "" @@ -214,7 +240,7 @@ msgstr "" msgid "DHCP has DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:68 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:98 msgid "Diagnostics" msgstr "" @@ -222,52 +248,56 @@ msgstr "" msgid "Disable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:312 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:445 msgid "Disable QUIC" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:313 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:446 msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:522 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665 msgid "Disabled" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149 msgid "Disables TLS certificate verification when downloading the subscription." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 +msgid "DNS" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:88 msgid "DNS on router" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:79 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:116 msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:445 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:442 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 msgid "DNS Protocol Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:113 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:151 msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:409 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:456 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58 msgid "DNS Server" msgstr "" @@ -275,15 +305,19 @@ msgstr "" msgid "DNS server address cannot be empty" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:27 msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:27 +msgid "Domain and subnet lists that decide which traffic uses this section" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431 msgid "Domain Resolver" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:377 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:457 msgid "Dont Touch My DHCP!" msgstr "" @@ -296,25 +330,25 @@ msgstr "" msgid "Download" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:335 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:340 msgid "Download Lists via Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:344 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:350 msgid "Download Lists via specific proxy section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:336 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:345 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:341 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:351 msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:523 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:578 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:666 msgid "Dynamic List" msgstr "" @@ -322,107 +356,107 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:826 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:899 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:483 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:491 msgid "Enable IPv6 Support" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:484 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:492 msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:797 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:868 msgid "Enable Mixed Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:171 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:212 msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:798 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:869 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:280 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:409 msgid "Enable YACD" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:289 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:419 msgid "Enable YACD WAN Access" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:69 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:99 msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:558 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:615 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:233 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:272 msgid "Every 1 minute" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:137 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:170 msgid "Every 12 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:135 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 msgid "Every 3 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:234 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:273 msgid "Every 3 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:133 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 msgid "Every 30 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:271 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:235 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:274 msgid "Every 5 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:136 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169 msgid "Every 6 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:138 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:171 msgid "Every day" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:134 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:167 msgid "Every hour" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:451 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:467 msgid "Exclude NTP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:452 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:468 msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204 msgid "Exclude servers by keyword" msgstr "" @@ -439,10 +473,10 @@ msgstr "" #: src/netshift/tabs/manager/initController.ts:122 #: src/netshift/tabs/manager/initController.ts:132 #: src/netshift/tabs/manager/initController.ts:166 -#: src/netshift/tabs/manager/initController.ts:197 -#: src/netshift/tabs/manager/initController.ts:201 -#: src/netshift/tabs/manager/initController.ts:234 -#: src/netshift/tabs/manager/initController.ts:238 +#: src/netshift/tabs/manager/initController.ts:194 +#: src/netshift/tabs/manager/initController.ts:198 +#: src/netshift/tabs/manager/initController.ts:231 +#: src/netshift/tabs/manager/initController.ts:235 msgid "Failed to execute!" msgstr "" @@ -453,7 +487,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:770 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:840 msgid "Fully Routed IPs" msgstr "" @@ -465,11 +499,19 @@ msgstr "" msgid "Global check" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:366 msgid "Global Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 +msgid "Group by countries" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180 +msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164 msgid "How often to automatically update the subscription" msgstr "" @@ -477,7 +519,7 @@ msgstr "" msgid "HTTP error" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:157 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:192 msgid "Include servers by keyword" msgstr "" @@ -491,15 +533,15 @@ msgstr "" msgid "Installed version is newer than release" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:232 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:275 msgid "Interface Monitoring" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:264 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:309 msgid "Interface Monitoring Delay" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:233 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:276 msgid "Interface monitoring for Bad WAN" msgstr "" @@ -723,7 +765,7 @@ msgstr "" msgid "Issues detected" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:158 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:193 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" @@ -740,19 +782,27 @@ msgstr "" msgid "Latest version is unknown" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:323 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:327 msgid "List Update Frequency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:678 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:27 +msgid "List update schedule, download routing, and routing exclusions" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:26 +msgid "Lists & Updates" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768 msgid "Local Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:435 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:554 msgid "Log Level" msgstr "" @@ -768,15 +818,19 @@ msgstr "" msgid "Memory Usage" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:810 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:32 +msgid "Mixed proxy and DNS resolution tuning" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:882 msgid "Mixed Proxy Port" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:241 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:285 msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -788,15 +842,19 @@ msgstr "" msgid "NetShift Settings" msgstr "" -#: src/netshift/tabs/manager/initController.ts:226 +#: src/netshift/tabs/manager/initController.ts:223 msgid "NetShift updated, version:" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:378 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:458 msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:340 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:21 +msgid "Network" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 msgid "Network Interface" msgstr "" @@ -829,11 +887,7 @@ msgstr "" msgid "Not running" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:473 -msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:375 msgid "Only one section can be global at a time." msgstr "" @@ -841,11 +895,11 @@ msgstr "" msgid "Operation timed out" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:30 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:58 msgid "Outbound Config" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:68 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:98 msgid "Outbound Configuration" msgstr "" @@ -854,7 +908,7 @@ msgstr "" msgid "Outdated" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:180 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:222 msgid "Output Network Interface" msgstr "" @@ -862,15 +916,15 @@ msgstr "" msgid "Path cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:417 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:535 msgid "Path must be absolute (start with /)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:426 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:544 msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:421 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:539 msgid "Path must end with cache.db" msgstr "" @@ -882,7 +936,11 @@ msgstr "" msgid "Pending" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:37 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:37 +msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:66 msgid "Proxy Configuration URL" msgstr "" @@ -894,19 +952,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:724 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:792 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:747 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:816 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:825 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:898 msgid "Resolve real IP for routing" msgstr "" @@ -914,11 +972,11 @@ msgstr "" msgid "Restart NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:367 msgid "Route all unmatched traffic through this section's outbound." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:68 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:104 msgid "Route main DNS through proxy/VPN" msgstr "" @@ -930,7 +988,11 @@ msgstr "" msgid "Router DNS is routed through sing-box" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:494 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:26 +msgid "Routing" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:384 msgid "Routing Excluded IPs" msgstr "" @@ -958,88 +1020,80 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:484 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532 msgid "Russia inside restrictions" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:302 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:433 msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:39 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:54 msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:480 msgid "Select a predefined list for routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:13 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:40 msgid "Select between VPN and Proxy connection methods for traffic routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:13 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:46 msgid "Select DNS protocol to use" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:328 msgid "Select how often the domain or subnet lists are updated automatically" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:24 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:52 msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:341 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:410 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59 msgid "Select or enter DNS server address" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:400 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:518 msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:387 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:504 msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:397 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:443 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:520 -msgid "Select the list type for adding custom domains" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:600 -msgid "Select the list type for adding custom subnets" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:436 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:555 msgid "Select the log level for sing-box" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:135 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:175 msgid "Select the network interface from which the traffic will originate" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:181 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:223 msgid "Select the network interface to which the traffic will originate" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:242 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:286 msgid "Select the WAN interfaces to be monitored" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:27 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:55 msgid "Selector" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216 msgid "Selector Proxy Links" msgstr "" @@ -1047,7 +1101,7 @@ msgstr "" msgid "Self-update failed" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:69 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:105 msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." msgstr "" @@ -1055,7 +1109,7 @@ msgstr "" msgid "Services info" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:52 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:67 msgid "Settings" msgstr "" @@ -1072,7 +1126,7 @@ msgstr "" msgid "Sing-box autostart disabled" msgstr "" -#: src/netshift/tabs/manager/initController.ts:190 +#: src/netshift/tabs/manager/initController.ts:187 msgid "Sing-box core changed, version:" msgstr "" @@ -1096,28 +1150,32 @@ msgstr "" msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:134 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:22 +msgid "Source and output interfaces, and Bad WAN interface monitoring" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:174 msgid "Source Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:495 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:385 msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:771 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:841 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:725 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:748 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:817 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:679 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:745 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:769 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1129,15 +1187,20 @@ msgstr "" msgid "Stop NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:29 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:21 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57 msgid "Subscription" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:130 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:22 +msgid "Subscription feeds, server filters and URLTest tuning" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163 msgid "Subscription Update Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:91 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122 msgid "Subscription URLs" msgstr "" @@ -1153,7 +1216,7 @@ msgstr "" msgid "Switch to stable" msgstr "" -#: src/netshift/tabs/manager/initController.ts:178 +#: src/netshift/tabs/manager/initController.ts:177 msgid "Switching sing-box core, this may take a few minutes…" msgstr "" @@ -1161,7 +1224,7 @@ msgstr "" msgid "System info" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts:21 +#: src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts:24 msgid "System information" msgstr "" @@ -1169,40 +1232,36 @@ msgstr "" msgid "Table exist" msgstr "" -#: src/netshift/tabs/dashboard/partials/renderSections.ts:108 +#: src/netshift/tabs/dashboard/partials/renderSections.ts:104 msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:524 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:604 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:579 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:667 msgid "Text List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:46 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:81 msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:244 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:276 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:317 msgid "The URL used to test server connectivity" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153 msgid "This is a security trade-off: an attacker could intercept the fetch." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:465 -msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:114 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:152 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" @@ -1214,24 +1273,24 @@ msgstr "" msgid "Traffic Total" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:25 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26 msgid "Troubleshooting" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:125 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:163 msgid "TTL must be a positive number" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:120 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:158 msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:313 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:355 msgid "UDP over TCP" msgstr "" @@ -1270,7 +1329,7 @@ msgstr "" msgid "Update NetShift" msgstr "" -#: src/netshift/tabs/manager/initController.ts:217 +#: src/netshift/tabs/manager/initController.ts:214 msgid "Updating NetShift, this may take a few minutes; the page will reload…" msgstr "" @@ -1279,6 +1338,10 @@ msgstr "" msgid "Uplink" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 +msgid "Upstream and bootstrap DNS resolvers, and optional DNS-over-proxy" +msgstr "" + #: src/validators/validateProxyUrl.ts:42 msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" @@ -1287,59 +1350,51 @@ msgstr "" msgid "URL must use one of the following protocols:" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:28 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:56 msgid "URLTest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:268 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:242 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:275 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283 msgid "URLTest Tolerance" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:151 msgid "Use only for IP-host panels that serve an invalid or self-signed certificate." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:486 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:494 msgid "Use this only when the router has working IPv6 connectivity." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:330 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373 msgid "Use with Exclusion sections to route specific domains directly." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:519 -msgid "User Domain List Type" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:531 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:557 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:614 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:599 -msgid "User Subnet List Type" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:637 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702 msgid "User Subnets List" msgstr "" @@ -1365,12 +1420,12 @@ msgstr "" msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:669 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:647 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734 msgid "Validation errors:" msgstr "" -#: src/netshift/tabs/manager/initController.ts:315 +#: src/netshift/tabs/manager/initController.ts:312 msgid "Version" msgstr "" @@ -1379,44 +1434,40 @@ msgstr "" msgid "View logs" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:31 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:32 msgid "Visit Wiki" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:38 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:217 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:486 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:534 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369 msgid "When enabled, traffic not matching any other section's lists will go through this proxy." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:117 msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:301 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:432 msgid "YACD Secret Key" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:172 -msgid "You can select Output Network Interface, by default autodetect" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:145 -msgid "Группировать по странам" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:32 +msgid "YACD web dashboard access and remote-access protection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:146 -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:213 +msgid "You can select Output Network Interface, by default autodetect" msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 4377ec3a..c2068c4b 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 10:23+0300\n" -"PO-Revision-Date: 2026-06-07 10:23+0300\n" +"POT-Creation-Date: 2026-06-07 11:56+0300\n" +"PO-Revision-Date: 2026-06-07 11:56+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -35,11 +35,17 @@ msgstr "Активные соединения" msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." msgstr "Добавьте один или несколько URL подписок для получения конфигураций прокси. Все источники загружаются и объединяются." +msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" +msgstr "Добавьте свои домены: выберите Динамический список (по одному в строке) или Текстовый список (свободный ввод), либо Отключено, чтобы пропустить" + +msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" +msgstr "Добавьте свои подсети или IP: выберите Динамический список (по одному в строке) или Текстовый список (свободный ввод), либо Отключено, чтобы пропустить" + msgid "Additional marking rules found" msgstr "Найдены дополнительные правила маркировки" -msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." -msgstr "Затрагивает публичные DoH-серверы Cloudflare, Google, Quad9, OpenDNS, AdGuard и Yandex." +msgid "Advanced" +msgstr "Дополнительно" msgid "Allow insecure TLS for subscription fetch" msgstr "Разрешить небезопасный TLS при загрузке подписки" @@ -59,8 +65,8 @@ msgstr "Необходимо указать хотя бы одну действ msgid "Available actions" msgstr "Доступные действия" -msgid "Block direct connections to known public DNS-over-HTTPS (DoH) servers." -msgstr "Блокирует прямые подключения к известным публичным серверам DNS-over-HTTPS (DoH)." +msgid "Block direct connections to known public DoH servers (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex) so apps cannot bypass router DNS filtering." +msgstr "Блокировать прямые подключения к известным публичным DoH-серверам (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex), чтобы приложения не могли обойти DNS-фильтрацию роутера." msgid "Block DoH Servers" msgstr "Блокировать DoH-серверы" @@ -122,9 +128,15 @@ msgstr "Конфигурация службы NetShift" msgid "Configuration Type" msgstr "Тип конфигурации" +msgid "Connection" +msgstr "Подключение" + msgid "Connection Type" msgstr "Тип подключения" +msgid "Connection type, transport and DNS resolver for this section" +msgstr "Тип подключения, транспорт и DNS-резолвер для этой секции" + msgid "Connection URL" msgstr "URL подключения" @@ -140,6 +152,12 @@ msgstr "Истекло время ожидания переключения яд msgid "Currently unavailable" msgstr "Временно недоступно" +msgid "Custom domains" +msgstr "Свои домены" + +msgid "Custom subnets" +msgstr "Свои подсети" + msgid "Dashboard" msgstr "Дашборд" @@ -176,6 +194,9 @@ msgstr "Отключено" msgid "Disables TLS certificate verification when downloading the subscription." msgstr "Отключает проверку TLS-сертификата при загрузке подписки." +msgid "DNS" +msgstr "DNS" + msgid "DNS on router" msgstr "DNS на роутере" @@ -203,6 +224,9 @@ msgstr "Адрес DNS-сервера не может быть пустым" msgid "Do not panic, everything can be fixed, just..." msgstr "Не паникуйте, всё можно исправить, просто..." +msgid "Domain and subnet lists that decide which traffic uses this section" +msgstr "Списки доменов и подсетей, определяющие, какой трафик идёт через эту секцию" + msgid "Domain Resolver" msgstr "Резолвер доменов" @@ -332,6 +356,12 @@ msgstr "Глобальная проверка" msgid "Global Proxy" msgstr "Глобальный прокси" +msgid "Group by countries" +msgstr "Группировать по странам" + +msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" +msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" + msgid "How often to automatically update the subscription" msgstr "Как часто автоматически обновлять подписку" @@ -533,6 +563,12 @@ msgstr "Последняя версия неизвестна" msgid "List Update Frequency" msgstr "Частота обновления списков" +msgid "List update schedule, download routing, and routing exclusions" +msgstr "Расписание обновления списков, маршрутизация загрузок и исключения из маршрутизации" + +msgid "Lists & Updates" +msgstr "Списки и обновления" + msgid "Local Domain Lists" msgstr "Локальные списки доменов" @@ -551,6 +587,9 @@ msgstr "Основной DNS через outbound" msgid "Memory Usage" msgstr "Использование памяти" +msgid "Mixed proxy and DNS resolution tuning" +msgstr "Настройка смешанного прокси и разрешения DNS" + msgid "Mixed Proxy Port" msgstr "Порт смешанного прокси" @@ -572,6 +611,9 @@ msgstr "NetShift обновлён, версия:" msgid "NetShift will not modify your DHCP configuration" msgstr "NetShift не будет изменять вашу конфигурацию DHCP" +msgid "Network" +msgstr "Сеть" + msgid "Network Interface" msgstr "Сетевой интерфейс" @@ -590,9 +632,6 @@ msgstr "Не отвечает" msgid "Not running" msgstr "Не запущено" -msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." -msgstr "Примечание: если тип вышестоящего DNS установлен в «DoH», включайте это только после переключения на UDP или DoT." - msgid "Only one section can be global at a time." msgstr "Только одна секция может быть глобальной одновременно." @@ -626,6 +665,9 @@ msgstr "Путь должен заканчиваться на cache.db" msgid "Pending" msgstr "Ожидает запуска" +msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT." +msgstr "Переключатели протоколов, пути к файлам и журналирование. Включайте блокировку DoH только после переключения вышестоящего DNS на UDP или DoT." + msgid "Proxy Configuration URL" msgstr "URL конфигурации прокси" @@ -662,6 +704,9 @@ msgstr "DNS роутера не проходит через sing-box" msgid "Router DNS is routed through sing-box" msgstr "DNS роутера проходит через sing-box" +msgid "Routing" +msgstr "Маршрутизация" + msgid "Routing Excluded IPs" msgstr "Исключённые из маршрутизации IP-адреса" @@ -722,12 +767,6 @@ msgstr "Выберите путь к файлу конфигурации sing-bo msgid "Select the DNS protocol type for the domain resolver" msgstr "Выберите тип протокола DNS для резолвера доменов" -msgid "Select the list type for adding custom domains" -msgstr "Выберите тип списка для добавления пользовательских доменов" - -msgid "Select the list type for adding custom subnets" -msgstr "Выберите тип списка для добавления пользовательских подсетей" - msgid "Select the log level for sing-box" msgstr "Выберите уровень логов для sing-box" @@ -785,6 +824,9 @@ msgstr "Сервис sing-box существует" msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "Версия Sing-box совместима (новее 1.12.4)" +msgid "Source and output interfaces, and Bad WAN interface monitoring" +msgstr "Входящий и исходящий интерфейсы, а также мониторинг интерфейсов Bad WAN" + msgid "Source Network Interface" msgstr "Сетевой интерфейс источника" @@ -812,6 +854,9 @@ msgstr "Остановить NetShift" msgid "Subscription" msgstr "Подписка" +msgid "Subscription feeds, server filters and URLTest tuning" +msgstr "Источники подписок, фильтры серверов и настройка URLTest" + msgid "Subscription Update Interval" msgstr "Интервал обновления подписки" @@ -860,9 +905,6 @@ msgstr "URL-адрес, используемый для проверки под msgid "This is a security trade-off: an attacker could intercept the fetch." msgstr "Это компромисс в безопасности: злоумышленник может перехватить загрузку." -msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." -msgstr "Это не позволяет приложениям обходить DNS-фильтрацию роутера за счёт использования собственного шифрованного DNS." - msgid "Time in seconds for DNS record caching (default: 60)" msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)" @@ -908,6 +950,9 @@ msgstr "Обновление NetShift, это может занять неско msgid "Uplink" msgstr "Исходящий" +msgid "Upstream and bootstrap DNS resolvers, and optional DNS-over-proxy" +msgstr "Вышестоящий и начальный (bootstrap) DNS-резолверы и опциональный DNS через прокси" + msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://" @@ -938,18 +983,12 @@ msgstr "Используйте это только если на роутере msgid "Use with Exclusion sections to route specific domains directly." msgstr "Используйте вместе с секциями исключений для прямой маршрутизации определённых доменов." -msgid "User Domain List Type" -msgstr "Тип пользовательского списка доменов" - msgid "User Domains" msgstr "Пользовательские домены" msgid "User Domains List" msgstr "Список пользовательских доменов" -msgid "User Subnet List Type" -msgstr "Тип пользовательского списка подсетей" - msgid "User Subnets" msgstr "Пользовательские подсети" @@ -989,11 +1028,8 @@ msgstr "Какая секция прокси/VPN обслуживает DNS. О msgid "YACD Secret Key" msgstr "Секретный ключ YACD" +msgid "YACD web dashboard access and remote-access protection" +msgstr "Доступ к веб-панели YACD и защита удалённого доступа" + msgid "You can select Output Network Interface, by default autodetect" msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически." - -msgid "Группировать по странам" -msgstr "Группировать по странам" - -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" -msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" diff --git a/fe-app-netshift/src/helpers/showToast.ts b/fe-app-netshift/src/helpers/showToast.ts index 92dcdd98..b32c1075 100644 --- a/fe-app-netshift/src/helpers/showToast.ts +++ b/fe-app-netshift/src/helpers/showToast.ts @@ -1,6 +1,6 @@ export function showToast( message: string, - type: 'success' | 'error', + type: 'success' | 'error' | 'warning' | 'info', duration: number = 3000, ) { let container = document.querySelector('.toast-container'); diff --git a/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderSections.ts b/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderSections.ts index eeb90871..f204ee68 100644 --- a/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderSections.ts +++ b/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderSections.ts @@ -1,3 +1,4 @@ +import { renderButton } from '../../../../partials'; import { NetShift } from '../../../types'; interface IRenderSectionsProps { @@ -13,7 +14,7 @@ function renderFailedState() { return E( 'div', { - class: 'pdk_dashboard-page__outbound-section centered', + class: 'card pdk_dashboard-page__outbound-section centered', style: 'height: 127px', }, E('span', {}, [E('span', {}, _('Dashboard currently unavailable'))]), @@ -23,7 +24,7 @@ function renderFailedState() { function renderLoadingState() { return E('div', { id: 'dashboard-sections-grid-skeleton', - class: 'pdk_dashboard-page__outbound-section skeleton', + class: 'card pdk_dashboard-page__outbound-section skeleton', style: 'height: 127px', }); } @@ -64,7 +65,7 @@ export function renderDefaultState({ return E( 'div', { - class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? 'pdk_dashboard-page__outbound-grid__item--active' : ''} ${section.withTagSelect ? 'pdk_dashboard-page__outbound-grid__item--selectable' : ''}`, + class: `card pdk_dashboard-page__outbound-grid__item ${outbound.selected ? 'pdk_dashboard-page__outbound-grid__item--active' : ''} ${section.withTagSelect ? 'pdk_dashboard-page__outbound-grid__item--selectable' : ''}`, click: () => section.withTagSelect && onChooseOutbound(section.code, outbound.code), @@ -87,7 +88,7 @@ export function renderDefaultState({ ); } - return E('div', { class: 'pdk_dashboard-page__outbound-section' }, [ + return E('div', { class: 'card pdk_dashboard-page__outbound-section' }, [ // Title with test latency E('div', { class: 'pdk_dashboard-page__outbound-section__title-section' }, [ E( @@ -99,14 +100,11 @@ export function renderDefaultState({ ), latencyFetching ? E('div', { class: 'skeleton', style: 'width: 99px; height: 28px' }) - : E( - 'button', - { - class: 'btn dashboard-sections-grid-item-test-latency', - click: () => testLatency(), - }, - _('Test latency'), - ), + : renderButton({ + text: _('Test latency'), + onClick: () => testLatency(), + classNames: ['dashboard-sections-grid-item-test-latency'], + }), ]), E( 'div', diff --git a/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderWidget.ts b/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderWidget.ts index 25216c09..36e26ba5 100644 --- a/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderWidget.ts +++ b/fe-app-netshift/src/netshift/tabs/dashboard/partials/renderWidget.ts @@ -17,7 +17,7 @@ function renderFailedState() { { id: '', style: 'height: 78px', - class: 'pdk_dashboard-page__widgets-section__item centered', + class: 'card pdk_dashboard-page__widgets-section__item centered', }, _('Currently unavailable'), ); @@ -29,14 +29,14 @@ function renderLoadingState() { { id: '', style: 'height: 78px', - class: 'pdk_dashboard-page__widgets-section__item skeleton', + class: 'card pdk_dashboard-page__widgets-section__item skeleton', }, '', ); } function renderDefaultState({ title, items }: IRenderWidgetProps) { - return E('div', { class: 'pdk_dashboard-page__widgets-section__item' }, [ + return E('div', { class: 'card pdk_dashboard-page__widgets-section__item' }, [ E( 'b', { class: 'pdk_dashboard-page__widgets-section__item__title' }, diff --git a/fe-app-netshift/src/netshift/tabs/dashboard/styles.ts b/fe-app-netshift/src/netshift/tabs/dashboard/styles.ts index 4b26b71e..9fb09214 100644 --- a/fe-app-netshift/src/netshift/tabs/dashboard/styles.ts +++ b/fe-app-netshift/src/netshift/tabs/dashboard/styles.ts @@ -27,9 +27,6 @@ export const styles = ` } .pdk_dashboard-page__widgets-section__item { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; } .pdk_dashboard-page__widgets-section__item__title {} @@ -50,9 +47,6 @@ export const styles = ` .pdk_dashboard-page__outbound-section { margin-top: 10px; - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; } .pdk_dashboard-page__outbound-section__title-section { @@ -74,9 +68,6 @@ export const styles = ` } .pdk_dashboard-page__outbound-grid__item { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; transition: border 0.2s ease; } diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts index 8abbadce..2e9cf66f 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts @@ -39,7 +39,7 @@ export function renderAvailableActions({ viewLogs, showSingBoxConfig, }: IRenderAvailableActionsProps) { - return E('div', { class: 'pdk_diagnostic-page__right-bar__actions' }, [ + return E('div', { class: 'card pdk_diagnostic-page__right-bar__actions' }, [ E('b', {}, _('Available actions')), ...insertIf(restart.visible, [ renderButton({ diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderCheckSection.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderCheckSection.ts index a54da42b..019c0cbb 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderCheckSection.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderCheckSection.ts @@ -56,7 +56,7 @@ function renderLoadingState(props: IRenderCheckSectionProps) { return E( 'div', - { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--loading' }, + { class: 'card pdk_diagnostic_alert pdk_diagnostic_alert--loading' }, [ iconWrap, E('div', { class: 'pdk_diagnostic_alert__content' }, [ @@ -79,7 +79,7 @@ function renderWarningState(props: IRenderCheckSectionProps) { return E( 'div', - { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--warning' }, + { class: 'card pdk_diagnostic_alert pdk_diagnostic_alert--warning' }, [ iconWrap, E('div', { class: 'pdk_diagnostic_alert__content' }, [ @@ -102,7 +102,7 @@ function renderErrorState(props: IRenderCheckSectionProps) { return E( 'div', - { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--error' }, + { class: 'card pdk_diagnostic_alert pdk_diagnostic_alert--error' }, [ iconWrap, E('div', { class: 'pdk_diagnostic_alert__content' }, [ @@ -125,7 +125,7 @@ function renderSuccessState(props: IRenderCheckSectionProps) { return E( 'div', - { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--success' }, + { class: 'card pdk_diagnostic_alert pdk_diagnostic_alert--success' }, [ iconWrap, E('div', { class: 'pdk_diagnostic_alert__content' }, [ @@ -148,7 +148,7 @@ function renderSkippedState(props: IRenderCheckSectionProps) { return E( 'div', - { class: 'pdk_diagnostic_alert pdk_diagnostic_alert--skipped' }, + { class: 'card pdk_diagnostic_alert pdk_diagnostic_alert--skipped' }, [ iconWrap, E('div', { class: 'pdk_diagnostic_alert__content' }, [ diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts index f686b8a6..78a50637 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts @@ -14,36 +14,40 @@ interface IRenderSystemInfoProps { } export function renderSystemInfo({ items }: IRenderSystemInfoProps) { - return E('div', { class: 'pdk_diagnostic-page__right-bar__system-info' }, [ - E( - 'b', - { class: 'pdk_diagnostic-page__right-bar__system-info__title' }, - _('System information'), - ), - ...items.map((item) => { - const tagClass = [ - 'pdk_diagnostic-page__right-bar__system-info__row__tag', - ...insertIf(item.tag?.kind === 'warning', [ - 'pdk_diagnostic-page__right-bar__system-info__row__tag--warning', - ]), - ...insertIf(item.tag?.kind === 'success', [ - 'pdk_diagnostic-page__right-bar__system-info__row__tag--success', - ]), - ] - .filter(Boolean) - .join(' '); - - return E( - 'div', - { class: 'pdk_diagnostic-page__right-bar__system-info__row' }, - [ - E('b', {}, item.key), - E('div', {}, [ - E('span', {}, item.value), - E('span', { class: tagClass }, item?.tag?.label), + return E( + 'div', + { class: 'card pdk_diagnostic-page__right-bar__system-info' }, + [ + E( + 'b', + { class: 'pdk_diagnostic-page__right-bar__system-info__title' }, + _('System information'), + ), + ...items.map((item) => { + const tagClass = [ + 'pdk_diagnostic-page__right-bar__system-info__row__tag', + ...insertIf(item.tag?.kind === 'warning', [ + 'pdk_diagnostic-page__right-bar__system-info__row__tag--warning', + ]), + ...insertIf(item.tag?.kind === 'success', [ + 'pdk_diagnostic-page__right-bar__system-info__row__tag--success', ]), - ], - ); - }), - ]); + ] + .filter(Boolean) + .join(' '); + + return E( + 'div', + { class: 'pdk_diagnostic-page__right-bar__system-info__row' }, + [ + E('b', {}, item.key), + E('div', {}, [ + E('span', {}, item.value), + E('span', { class: tagClass }, item?.tag?.label), + ]), + ], + ); + }), + ], + ); } diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts index adb8801a..0abb6e8d 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts @@ -9,6 +9,7 @@ export function renderWikiDisclaimer(kind: 'default' | 'error' | 'warning') { iconWrap.appendChild(renderBookOpenTextIcon24()); const className = [ + 'card', 'pdk_diagnostic-page__right-bar__wiki', ...insertIf(kind === 'error', [ 'pdk_diagnostic-page__right-bar__wiki--error', diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/styles.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/styles.ts index 6d258aab..a8f78aa2 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/styles.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/styles.ts @@ -29,10 +29,6 @@ export const styles = ` } .pdk_diagnostic-page__right-bar__wiki { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - display: grid; grid-template-columns: auto; grid-row-gap: 10px; @@ -54,21 +50,12 @@ export const styles = ` .pdk_diagnostic-page__right-bar__wiki__texts {} .pdk_diagnostic-page__right-bar__actions { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - display: grid; grid-template-columns: auto; grid-row-gap: 10px; - } .pdk_diagnostic-page__right-bar__system-info { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - display: grid; grid-template-columns: auto; grid-row-gap: 10px; @@ -120,14 +107,10 @@ export const styles = ` } .pdk_diagnostic_alert { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - display: grid; grid-template-columns: 24px 1fr; grid-column-gap: 10px; align-items: center; - padding: 10px; } .pdk_diagnostic_alert--loading { diff --git a/fe-app-netshift/src/netshift/tabs/manager/initController.ts b/fe-app-netshift/src/netshift/tabs/manager/initController.ts index 37f1b7ad..683634d5 100644 --- a/fe-app-netshift/src/netshift/tabs/manager/initController.ts +++ b/fe-app-netshift/src/netshift/tabs/manager/initController.ts @@ -174,10 +174,7 @@ async function runSingBoxMutation( button: ManagerActionDescriptor, ) { setActionLoading(button.loadingKey, true); - showToast( - _('Switching sing-box core, this may take a few minutes…'), - 'success', - ); + showToast(_('Switching sing-box core, this may take a few minutes…'), 'info'); try { const result = await NetShiftShellMethods.singBoxComponentAction( @@ -215,7 +212,7 @@ async function runNetshiftSelfUpdate(button: ManagerActionDescriptor) { // Warning-style toast: self-update is long and ends in a page reload. showToast( _('Updating NetShift, this may take a few minutes; the page will reload…'), - 'success', + 'warning', 6000, ); @@ -306,7 +303,7 @@ function renderComponentCard(card: ManagerCardDescriptor) { ); } - return E('div', { class: 'pdk_manager-page__component' }, [ + return E('div', { class: 'card pdk_manager-page__component' }, [ E('div', { class: 'pdk_manager-page__component__header' }, headerChildren), E('div', { class: 'pdk_manager-page__component__version' }, [ E( diff --git a/fe-app-netshift/src/netshift/tabs/manager/styles.ts b/fe-app-netshift/src/netshift/tabs/manager/styles.ts index 8156d9c5..558c74a0 100644 --- a/fe-app-netshift/src/netshift/tabs/manager/styles.ts +++ b/fe-app-netshift/src/netshift/tabs/manager/styles.ts @@ -25,10 +25,6 @@ export const styles = ` } .pdk_manager-page__component { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - min-width: 0; display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; diff --git a/fe-app-netshift/src/styles.ts b/fe-app-netshift/src/styles.ts index efb12925..4cee85c2 100644 --- a/fe-app-netshift/src/styles.ts +++ b/fe-app-netshift/src/styles.ts @@ -3,6 +3,38 @@ import { DashboardTab, DiagnosticTab, ManagerTab } from './netshift'; import { PartialStyles } from './partials'; export const GlobalStyles = ` +/* + * NetShift design tokens (Stage 1 foundation — task-024). + * Each token layers over the LuCI theme var (with a hardcoded fallback) so + * themes still win. Reused by the custom tabs and the form redesigns + * (task-025/026). Keep these names stable. + */ +:root, +.cbi-map { + --ns-card-border: var(--background-color-low, lightgray); + --ns-card-border-width: 2px; + --ns-card-radius: 4px; + --ns-gap: 10px; + --ns-card-padding: var(--ns-gap); + --ns-success: var(--success-color-medium, #28a745); + --ns-warning: var(--warn-color-medium, #f0ad4e); + --ns-error: var(--error-color-medium, #dc3545); + --ns-info: var(--primary-color-high, #2196f3); +} + +/* + * Shared card primitive. Mirrors the Manager component card look + * (2px solid border, 4px radius, 10px padding, overflow-safe min-width:0). + * Defined BEFORE the per-tab styles so colored-border modifiers + * (e.g. .pdk_diagnostic_alert--warning) still win via source order. + */ +.card { + border: var(--ns-card-border-width) solid var(--ns-card-border); + border-radius: var(--ns-card-radius); + padding: var(--ns-card-padding); + min-width: 0; +} + ${DashboardTab.styles} ${DiagnosticTab.styles} ${ManagerTab.styles} @@ -24,6 +56,42 @@ ${PartialStyles} margin-bottom: -32px; } +/* + * Sections (connection) form — native CBI option-group tabs styled as a + * card (task-025). Reuses task-024's --ns-* tokens. The tab strip + * (ul.cbi-tabmenu) sits on top; each tab pane (.cbi-section-node-tabbed) + * reads as the card body. depends()-driven auto-hide of tabs is unaffected. + */ +#cbi-netshift-section .cbi-section-node-tabbed { + border: var(--ns-card-border-width) solid var(--ns-card-border); + border-radius: var(--ns-card-radius); + padding: var(--ns-card-padding); + min-width: 0; +} + +#cbi-netshift-section ul.cbi-tabmenu { + margin-bottom: var(--ns-gap); +} + +/* + * Settings form — native CBI option-group tabs styled as a card (task-026). + * Reuses task-024's --ns-* tokens and mirrors the #cbi-netshift-section + * pattern above. The tab strip (ul.cbi-tabmenu) sits on top; each tab pane + * (.cbi-section-node-tabbed) reads as the card body. depends()-driven + * auto-hide of tabs is unaffected. The existing + * #cbi-netshift-settings > h3 hide rule above stays valid. + */ +#cbi-netshift-settings .cbi-section-node-tabbed { + border: var(--ns-card-border-width) solid var(--ns-card-border); + border-radius: var(--ns-card-radius); + padding: var(--ns-card-padding); + min-width: 0; +} + +#cbi-netshift-settings ul.cbi-tabmenu { + margin-bottom: var(--ns-gap); +} + /* Centered class helper */ .centered { display: flex; @@ -99,11 +167,19 @@ ${PartialStyles} } .toast-success { - background-color: #28a745; + background-color: var(--ns-success, #28a745); } .toast-error { - background-color: #dc3545; + background-color: var(--ns-error, #dc3545); +} + +.toast-warning { + background-color: var(--ns-warning, #f0ad4e); +} + +.toast-info { + background-color: var(--ns-info, #2196f3); } .toast.visible { diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index c3c12ab1..d653f238 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -2201,1784 +2201,1772 @@ var SocketManager = class _SocketManager { }; var socket = SocketManager.getInstance(); -// src/netshift/tabs/dashboard/partials/renderSections.ts -function renderFailedState() { - return E( - "div", - { - class: "pdk_dashboard-page__outbound-section centered", - style: "height: 127px" - }, - E("span", {}, [E("span", {}, _("Dashboard currently unavailable"))]) - ); +// src/partials/button/styles.ts +var styles = ` +.pdk-partial-button { + text-align: center; } -function renderLoadingState() { - return E("div", { - id: "dashboard-sections-grid-skeleton", - class: "pdk_dashboard-page__outbound-section skeleton", - style: "height: 127px" - }); + +.pdk-partial-button--with-icon { + display: flex; + align-items: center; + justify-content: center; } -function renderDefaultState({ - section, - onChooseOutbound, - onTestLatency, - latencyFetching -}) { - function testLatency() { - if (section.withTagSelect) { - return onTestLatency(section.code); - } - if (section.outbounds.length) { - return onTestLatency(section.outbounds[0].code); - } - } - function renderOutbound(outbound) { - function getLatencyClass() { - if (!outbound.latency) { - return "pdk_dashboard-page__outbound-grid__item__latency--empty"; - } - if (outbound.latency < 800) { - return "pdk_dashboard-page__outbound-grid__item__latency--green"; - } - if (outbound.latency < 1500) { - return "pdk_dashboard-page__outbound-grid__item__latency--yellow"; - } - return "pdk_dashboard-page__outbound-grid__item__latency--red"; - } - return E( - "div", - { - class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? "pdk_dashboard-page__outbound-grid__item--active" : ""} ${section.withTagSelect ? "pdk_dashboard-page__outbound-grid__item--selectable" : ""}`, - click: () => section.withTagSelect && onChooseOutbound(section.code, outbound.code) - }, - [ - E("b", {}, outbound.displayName), - E("div", { class: "pdk_dashboard-page__outbound-grid__item__footer" }, [ - E( - "div", - { class: "pdk_dashboard-page__outbound-grid__item__type" }, - outbound.type - ), - E( - "div", - { class: getLatencyClass() }, - outbound.latency ? `${outbound.latency}ms` : "N/A" - ) - ]) - ] - ); - } - return E("div", { class: "pdk_dashboard-page__outbound-section" }, [ - // Title with test latency - E("div", { class: "pdk_dashboard-page__outbound-section__title-section" }, [ - E( - "div", - { - class: "pdk_dashboard-page__outbound-section__title-section__title" - }, - section.displayName - ), - latencyFetching ? E("div", { class: "skeleton", style: "width: 99px; height: 28px" }) : E( - "button", - { - class: "btn dashboard-sections-grid-item-test-latency", - click: () => testLatency() - }, - _("Test latency") - ) - ]), - E( - "div", - { class: "pdk_dashboard-page__outbound-grid" }, - section.outbounds.map((outbound) => renderOutbound(outbound)) - ) - ]); + +.pdk-partial-button--loading { } -function renderSections(props) { - if (props.failed) { - return renderFailedState(); - } - if (props.loading) { - return renderLoadingState(); - } - return renderDefaultState(props); + +.pdk-partial-button--disabled { } -// src/netshift/tabs/dashboard/partials/renderWidget.ts -function renderFailedState2() { - return E( - "div", - { - id: "", - style: "height: 78px", - class: "pdk_dashboard-page__widgets-section__item centered" - }, - _("Currently unavailable") - ); +.pdk-partial-button__icon { + margin-right: 5px; } -function renderLoadingState2() { - return E( - "div", - { - id: "", - style: "height: 78px", - class: "pdk_dashboard-page__widgets-section__item skeleton" - }, - "" - ); + +.pdk-partial-button__icon { + display: flex; + align-items: center; + justify-content: center; } -function renderDefaultState2({ title, items }) { - return E("div", { class: "pdk_dashboard-page__widgets-section__item" }, [ - E( - "b", - { class: "pdk_dashboard-page__widgets-section__item__title" }, - title - ), - ...items.map( - (item) => E( - "div", - { - class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ""}` - }, - [ - E( - "span", - { class: "pdk_dashboard-page__widgets-section__item__row__key" }, - `${item.key}: ` - ), - E( - "span", - { class: "pdk_dashboard-page__widgets-section__item__row__value" }, - item.value - ) - ] - ) - ) - ]); + +.pdk-partial-button__icon svg { + width: 16px; + height: 16px; } -function renderWidget(props) { - if (props.loading) { - return renderLoadingState2(); - } - if (props.failed) { - return renderFailedState2(); - } - return renderDefaultState2(props); +`; + +// src/partials/modal/styles.ts +var styles2 = ` + +.pdk-partial-modal__body {} + +.pdk-partial-modal__content { + max-height: 70vh; + overflow: scroll; + border-radius: 4px; } -// src/netshift/tabs/dashboard/render.ts -function render() { - return E( - "div", - { - id: "dashboard-status", - class: "pdk_dashboard-page" - }, - [ - // Widgets section - E("div", { class: "pdk_dashboard-page__widgets-section" }, [ - E( - "div", - { id: "dashboard-widget-traffic" }, - renderWidget({ loading: true, failed: false, title: "", items: [] }) - ), - E( - "div", - { id: "dashboard-widget-traffic-total" }, - renderWidget({ loading: true, failed: false, title: "", items: [] }) - ), - E( - "div", - { id: "dashboard-widget-system-info" }, - renderWidget({ loading: true, failed: false, title: "", items: [] }) - ), - E( - "div", - { id: "dashboard-widget-service-info" }, - renderWidget({ loading: true, failed: false, title: "", items: [] }) - ) - ]), - // All outbounds - E( - "div", - { id: "dashboard-sections-grid" }, - renderSections({ - loading: true, - failed: false, - section: { - code: "", - displayName: "", - outbounds: [], - withTagSelect: false - }, - onTestLatency: () => { - }, - onChooseOutbound: () => { - }, - latencyFetching: false - }) - ) - ] - ); +.pdk-partial-modal__footer { + display: flex; + justify-content: flex-end; } -// src/helpers/prettyBytes.ts -function prettyBytes(n) { - const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - if (n < 1e3) { - return n + " B"; - } - const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1); - n = Number((n / Math.pow(1e3, exponent)).toPrecision(3)); - const unit = UNITS[exponent]; - return n + " " + unit; +.pdk-partial-modal__footer button { + margin-left: 10px; } +`; -// src/netshift/fetchers/fetchServicesInfo.ts -async function fetchServicesInfo() { - const [netshift, singbox] = await Promise.all([ - NetShiftShellMethods.getStatus(), - NetShiftShellMethods.getSingBoxStatus() - ]); - if (!netshift.success || !singbox.success) { - store.set({ - servicesInfoWidget: { - loading: false, - failed: true, - data: { singbox: 0, netshift: 0 } - } - }); - } - if (netshift.success && singbox.success) { - store.set({ - servicesInfoWidget: { - loading: false, - failed: false, - data: { - singbox: singbox.data.running, - netshift: netshift.data.enabled - } - } - }); - } -} - -// src/netshift/tabs/dashboard/initController.ts -async function fetchDashboardSections() { - const prev = store.get().sectionsWidget; - store.set({ - sectionsWidget: { - ...prev, - failed: false - } - }); - const { data, success } = await CustomNetShiftMethods.getDashboardSections(); - if (!success) { - logger.error("[DASHBOARD]", "fetchDashboardSections: failed to fetch"); - } - store.set({ - sectionsWidget: { - latencyFetching: false, - loading: false, - failed: !success, - data - } - }); -} -async function connectToClashSockets() { - const clashApiSecret = await getClashApiSecret(); - socket.subscribe( - `${getClashWsUrl()}/traffic?token=${clashApiSecret}`, - (msg) => { - const parsedMsg = JSON.parse(msg); - store.set({ - bandwidthWidget: { - loading: false, - failed: false, - data: { up: parsedMsg.up, down: parsedMsg.down } - } - }); - }, - (_err) => { - logger.error( - "[DASHBOARD]", - "connectToClashSockets - traffic: failed to connect to", - getClashWsUrl() - ); - store.set({ - bandwidthWidget: { - loading: false, - failed: true, - data: { up: 0, down: 0 } - } - }); - } - ); - socket.subscribe( - `${getClashWsUrl()}/connections?token=${clashApiSecret}`, - (msg) => { - const parsedMsg = JSON.parse(msg); - store.set({ - trafficTotalWidget: { - loading: false, - failed: false, - data: { - downloadTotal: parsedMsg.downloadTotal, - uploadTotal: parsedMsg.uploadTotal - } - }, - systemInfoWidget: { - loading: false, - failed: false, - data: { - connections: parsedMsg.connections?.length, - memory: parsedMsg.memory - } - } - }); +// src/icons/renderLoaderCircleIcon24.ts +function renderLoaderCircleIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-loader-circle rotate" }, - (_err) => { - logger.error( - "[DASHBOARD]", - "connectToClashSockets - connections: failed to connect to", - getClashWsUrl() - ); - store.set({ - trafficTotalWidget: { - loading: false, - failed: true, - data: { downloadTotal: 0, uploadTotal: 0 } - }, - systemInfoWidget: { - loading: false, - failed: true, - data: { - connections: 0, - memory: 0 - } - } - }); - } + [ + svgEl("path", { + d: "M21 12a9 9 0 1 1-6.219-8.56" + }), + svgEl("animateTransform", { + attributeName: "transform", + attributeType: "XML", + type: "rotate", + from: "0 12 12", + to: "360 12 12", + dur: "1s", + repeatCount: "indefinite" + }) + ] ); } -async function handleChooseOutbound(selector, tag) { - await NetShiftShellMethods.setClashApiGroupProxy(selector, tag); - await fetchDashboardSections(); -} -async function handleTestGroupLatency(tag) { - store.set({ - sectionsWidget: { - ...store.get().sectionsWidget, - latencyFetching: true - } - }); - await NetShiftShellMethods.getClashApiGroupLatency(tag); - await fetchDashboardSections(); - store.set({ - sectionsWidget: { - ...store.get().sectionsWidget, - latencyFetching: false - } - }); -} -async function handleTestProxyLatency(tag) { - store.set({ - sectionsWidget: { - ...store.get().sectionsWidget, - latencyFetching: true - } - }); - await NetShiftShellMethods.getClashApiProxyLatency(tag); - await fetchDashboardSections(); - store.set({ - sectionsWidget: { - ...store.get().sectionsWidget, - latencyFetching: false - } - }); -} -async function renderSectionsWidget() { - logger.debug("[DASHBOARD]", "renderSectionsWidget"); - const sectionsWidget = store.get().sectionsWidget; - const container = document.getElementById("dashboard-sections-grid"); - if (sectionsWidget.loading || sectionsWidget.failed) { - const renderedWidget = renderSections({ - loading: sectionsWidget.loading, - failed: sectionsWidget.failed, - section: { - code: "", - displayName: "", - outbounds: [], - withTagSelect: false - }, - onTestLatency: () => { - }, - onChooseOutbound: () => { - }, - latencyFetching: sectionsWidget.latencyFetching - }); - return preserveScrollForPage(() => { - container.replaceChildren(renderedWidget); - }); - } - const renderedWidgets = sectionsWidget.data.map( - (section) => renderSections({ - loading: sectionsWidget.loading, - failed: sectionsWidget.failed, - section, - latencyFetching: sectionsWidget.latencyFetching, - onTestLatency: (tag) => { - if (section.withTagSelect) { - return handleTestGroupLatency(tag); - } - return handleTestProxyLatency(tag); - }, - onChooseOutbound: (selector, tag) => { - handleChooseOutbound(selector, tag); - } - }) + +// src/icons/renderCircleAlertIcon24.ts +function renderCircleAlertIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-alert-icon lucide-circle-alert" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("line", { + x1: "12", + y1: "8", + x2: "12", + y2: "12" + }), + svgEl("line", { + x1: "12", + y1: "16", + x2: "12.01", + y2: "16" + }) + ] ); - return preserveScrollForPage(() => { - container.replaceChildren(...renderedWidgets); - }); } -async function renderBandwidthWidget() { - logger.debug("[DASHBOARD]", "renderBandwidthWidget"); - const traffic = store.get().bandwidthWidget; - const container = document.getElementById("dashboard-widget-traffic"); - if (traffic.loading || traffic.failed) { - const renderedWidget2 = renderWidget({ - loading: traffic.loading, - failed: traffic.failed, - title: "", - items: [] - }); - return container.replaceChildren(renderedWidget2); - } - const renderedWidget = renderWidget({ - loading: traffic.loading, - failed: traffic.failed, - title: _("Traffic"), - items: [ - { key: _("Uplink"), value: `${prettyBytes(traffic.data.up)}/s` }, - { key: _("Downlink"), value: `${prettyBytes(traffic.data.down)}/s` } + +// src/icons/renderCircleCheckIcon24.ts +function renderCircleCheckIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-check-icon lucide-circle-check" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("path", { + d: "M9 12l2 2 4-4" + }) ] - }); - container.replaceChildren(renderedWidget); + ); } -async function renderTrafficTotalWidget() { - logger.debug("[DASHBOARD]", "renderTrafficTotalWidget"); - const trafficTotalWidget = store.get().trafficTotalWidget; - const container = document.getElementById("dashboard-widget-traffic-total"); - if (trafficTotalWidget.loading || trafficTotalWidget.failed) { - const renderedWidget2 = renderWidget({ - loading: trafficTotalWidget.loading, - failed: trafficTotalWidget.failed, - title: "", - items: [] - }); - return container.replaceChildren(renderedWidget2); - } - const renderedWidget = renderWidget({ - loading: trafficTotalWidget.loading, - failed: trafficTotalWidget.failed, - title: _("Traffic Total"), - items: [ - { - key: _("Uplink"), - value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)) - }, - { - key: _("Downlink"), - value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)) - } + +// src/icons/renderCircleSlashIcon24.ts +function renderCircleSlashIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-slash-icon lucide-circle-slash" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("line", { + x1: "9", + y1: "15", + x2: "15", + y2: "9" + }) ] - }); - container.replaceChildren(renderedWidget); + ); } -async function renderSystemInfoWidget() { - logger.debug("[DASHBOARD]", "renderSystemInfoWidget"); - const systemInfoWidget = store.get().systemInfoWidget; - const container = document.getElementById("dashboard-widget-system-info"); - if (systemInfoWidget.loading || systemInfoWidget.failed) { - const renderedWidget2 = renderWidget({ - loading: systemInfoWidget.loading, - failed: systemInfoWidget.failed, - title: "", - items: [] - }); - return container.replaceChildren(renderedWidget2); - } - const renderedWidget = renderWidget({ - loading: systemInfoWidget.loading, - failed: systemInfoWidget.failed, - title: _("System info"), - items: [ - { - key: _("Active Connections"), - value: String(systemInfoWidget.data.connections) - }, - { - key: _("Memory Usage"), - value: String(prettyBytes(systemInfoWidget.data.memory)) - } + +// src/icons/renderCircleXIcon24.ts +function renderCircleXIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-x-icon lucide-circle-x" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("path", { + d: "M15 9L9 15" + }), + svgEl("path", { + d: "M9 9L15 15" + }) ] - }); - container.replaceChildren(renderedWidget); + ); } -async function renderServicesInfoWidget() { - logger.debug("[DASHBOARD]", "renderServicesInfoWidget"); - const servicesInfoWidget = store.get().servicesInfoWidget; - const container = document.getElementById("dashboard-widget-service-info"); - if (servicesInfoWidget.loading || servicesInfoWidget.failed) { - const renderedWidget2 = renderWidget({ - loading: servicesInfoWidget.loading, - failed: servicesInfoWidget.failed, - title: "", - items: [] - }); - return container.replaceChildren(renderedWidget2); - } - const renderedWidget = renderWidget({ - loading: servicesInfoWidget.loading, - failed: servicesInfoWidget.failed, - title: _("Services info"), - items: [ - { - key: _("NetShift"), - value: servicesInfoWidget.data.netshift ? _("\u2714 Enabled") : _("\u2718 Disabled"), - attributes: { - class: servicesInfoWidget.data.netshift ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" - } - }, - { - key: _("Sing-box"), - value: servicesInfoWidget.data.singbox ? _("\u2714 Running") : _("\u2718 Stopped"), - attributes: { - class: servicesInfoWidget.data.singbox ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" - } - } + +// src/icons/renderCheckIcon24.ts +function renderCheckIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-check-icon lucide-check" + }, + [ + svgEl("path", { + d: "M20 6 9 17l-5-5" + }) ] - }); - container.replaceChildren(renderedWidget); + ); } -async function onStoreUpdate(next, prev, diff) { - if (diff.sectionsWidget) { - renderSectionsWidget(); - } - if (diff.bandwidthWidget) { - renderBandwidthWidget(); - } - if (diff.trafficTotalWidget) { - renderTrafficTotalWidget(); - } - if (diff.systemInfoWidget) { - renderSystemInfoWidget(); - } - if (diff.servicesInfoWidget) { - renderServicesInfoWidget(); - } + +// src/icons/renderXIcon24.ts +function renderXIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-x-icon lucide-x" + }, + [svgEl("path", { d: "M18 6 6 18" }), svgEl("path", { d: "m6 6 12 12" })] + ); } -async function onPageMount() { - onPageUnmount(); - store.subscribe(onStoreUpdate); - await fetchDashboardSections(); - await fetchServicesInfo(); - await connectToClashSockets(); + +// src/icons/renderTriangleAlertIcon24.ts +function renderTriangleAlertIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-triangle-alert-icon lucide-triangle-alert" + }, + [ + svgEl("path", { + d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" + }), + svgEl("path", { d: "M12 9v4" }), + svgEl("path", { d: "M12 17h.01" }) + ] + ); } -function onPageUnmount() { - store.unsubscribe(onStoreUpdate); - store.reset([ - "bandwidthWidget", - "trafficTotalWidget", - "systemInfoWidget", - "servicesInfoWidget", - "sectionsWidget" - ]); - socket.resetAll(); + +// src/icons/renderPauseIcon24.ts +function renderPauseIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-pause-icon lucide-pause" + }, + [ + svgEl("rect", { + x: "14", + y: "3", + width: "5", + height: "18", + rx: "1" + }), + svgEl("rect", { + x: "5", + y: "3", + width: "5", + height: "18", + rx: "1" + }) + ] + ); } -function registerLifecycleListeners() { - store.subscribe((next, prev, diff) => { - if (diff.tabService && next.tabService.current !== prev.tabService.current) { - logger.debug( - "[DASHBOARD]", - "active tab diff event, active tab:", - diff.tabService.current - ); - const isDashboardVisible = next.tabService.current === "dashboard"; - if (isDashboardVisible) { - logger.debug( - "[DASHBOARD]", - "registerLifecycleListeners", - "onPageMount" - ); - return onPageMount(); - } - if (!isDashboardVisible) { - logger.debug( - "[DASHBOARD]", - "registerLifecycleListeners", - "onPageUnmount" - ); - return onPageUnmount(); - } - } - }); + +// src/icons/renderPlayIcon24.ts +function renderPlayIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-play-icon lucide-play" + }, + [ + svgEl("path", { + d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" + }) + ] + ); } -async function initController() { - onMount("dashboard-status").then(() => { - logger.debug("[DASHBOARD]", "initController", "onMount"); - onPageMount(); - registerLifecycleListeners(); - }); + +// src/icons/renderRotateCcwIcon24.ts +function renderRotateCcwIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-rotate-ccw-icon lucide-rotate-ccw" + }, + [ + svgEl("path", { + d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" + }), + svgEl("path", { + d: "M3 3v5h5" + }) + ] + ); } -// src/netshift/tabs/dashboard/styles.ts -var styles = ` -#cbi-netshift-dashboard-_mount_node > div { - width: 100%; +// src/icons/renderCircleStopIcon24.ts +function renderCircleStopIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-stop-icon lucide-circle-stop" + }, + [ + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }), + svgEl("rect", { + x: "9", + y: "9", + width: "6", + height: "6", + rx: "1" + }) + ] + ); } -#cbi-netshift-dashboard > h3 { - display: none; -} - -.pdk_dashboard-page { - width: 100%; - --dashboard-grid-columns: 4; +// src/icons/renderCirclePlayIcon24.ts +function renderCirclePlayIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-play-icon lucide-circle-play" + }, + [ + svgEl("path", { + d: "M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z" + }), + svgEl("circle", { + cx: "12", + cy: "12", + r: "10" + }) + ] + ); } -@media (max-width: 900px) { - .pdk_dashboard-page { - --dashboard-grid-columns: 2; - } +// src/icons/renderCircleCheckBigIcon24.ts +function renderCircleCheckBigIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-circle-check-big-icon lucide-circle-check-big" + }, + [ + svgEl("path", { + d: "M21.801 10A10 10 0 1 1 17 3.335" + }), + svgEl("path", { + d: "m9 11 3 3L22 4" + }) + ] + ); } -.pdk_dashboard-page__widgets-section { - margin-top: 10px; - display: grid; - grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); - grid-gap: 10px; +// src/icons/renderSquareChartGanttIcon24.ts +function renderSquareChartGanttIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt" + }, + [ + svgEl("rect", { + width: "18", + height: "18", + x: "3", + y: "3", + rx: "2" + }), + svgEl("path", { d: "M9 8h7" }), + svgEl("path", { d: "M8 12h6" }), + svgEl("path", { d: "M11 16h5" }) + ] + ); } -.pdk_dashboard-page__widgets-section__item { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; +// src/icons/renderCogIcon24.ts +function renderCogIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-cog-icon lucide-cog" + }, + [ + svgEl("path", { d: "M11 10.27 7 3.34" }), + svgEl("path", { d: "m11 13.73-4 6.93" }), + svgEl("path", { d: "M12 22v-2" }), + svgEl("path", { d: "M12 2v2" }), + svgEl("path", { d: "M14 12h8" }), + svgEl("path", { d: "m17 20.66-1-1.73" }), + svgEl("path", { d: "m17 3.34-1 1.73" }), + svgEl("path", { d: "M2 12h2" }), + svgEl("path", { d: "m20.66 17-1.73-1" }), + svgEl("path", { d: "m20.66 7-1.73 1" }), + svgEl("path", { d: "m3.34 17 1.73-1" }), + svgEl("path", { d: "m3.34 7 1.73 1" }), + svgEl("circle", { cx: "12", cy: "12", r: "2" }), + svgEl("circle", { cx: "12", cy: "12", r: "8" }) + ] + ); } -.pdk_dashboard-page__widgets-section__item__title {} +// src/icons/renderSearchIcon24.ts +function renderSearchIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-search-icon lucide-search" + }, + [ + svgEl("path", { d: "m21 21-4.34-4.34" }), + svgEl("circle", { cx: "11", cy: "11", r: "8" }) + ] + ); +} -.pdk_dashboard-page__widgets-section__item__row {} +// src/icons/renderBookOpenTextIcon24.ts +function renderBookOpenTextIcon24() { + const NS = "http://www.w3.org/2000/svg"; + return svgEl( + "svg", + { + xmlns: NS, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + class: "lucide lucide-book-open-text-icon lucide-book-open-text" + }, + [ + svgEl("path", { d: "M12 7v14" }), + svgEl("path", { d: "M16 12h2" }), + svgEl("path", { d: "M16 8h2" }), + svgEl("path", { + d: "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" + }), + svgEl("path", { d: "M6 12h2" }), + svgEl("path", { d: "M6 8h2" }) + ] + ); +} -.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value { - color: var(--success-color-medium, green); +// src/partials/button/renderButton.ts +function renderButton({ + classNames = [], + disabled, + loading, + onClick, + text, + icon +}) { + const hasIcon = !!loading || !!icon; + function getWrappedIcon() { + const iconWrap = E("span", { + class: "pdk-partial-button__icon" + }); + if (loading) { + iconWrap.appendChild(renderLoaderCircleIcon24()); + return iconWrap; + } + if (icon) { + iconWrap.appendChild(icon()); + return iconWrap; + } + return iconWrap; + } + function getClass() { + return [ + "btn", + "pdk-partial-button", + ...insertIf(Boolean(disabled), ["pdk-partial-button--disabled"]), + ...insertIf(Boolean(loading), ["pdk-partial-button--loading"]), + ...insertIf(Boolean(hasIcon), ["pdk-partial-button--with-icon"]), + ...classNames + ].filter(Boolean).join(" "); + } + function getDisabled() { + if (loading || disabled) { + return true; + } + return void 0; + } + return E( + "button", + { class: getClass(), disabled: getDisabled(), click: onClick }, + [...insertIf(hasIcon, [getWrappedIcon()]), E("span", {}, text)] + ); } -.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value { - color: var(--error-color-medium, red); +// src/helpers/showToast.ts +function showToast(message, type, duration = 3e3) { + let container = document.querySelector(".toast-container"); + if (!container) { + container = document.createElement("div"); + container.className = "toast-container"; + document.body.appendChild(container); + } + const toast = document.createElement("div"); + toast.className = `toast toast-${type}`; + toast.textContent = message; + container.appendChild(toast); + setTimeout(() => toast.classList.add("visible"), 100); + setTimeout(() => { + toast.classList.remove("visible"); + setTimeout(() => toast.remove(), 300); + }, duration); } -.pdk_dashboard-page__widgets-section__item__row__key {} - -.pdk_dashboard-page__widgets-section__item__row__value {} - -.pdk_dashboard-page__outbound-section { - margin-top: 10px; - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; +// src/helpers/copyToClipboard.ts +function copyToClipboard(text) { + const textarea = document.createElement("textarea"); + textarea.value = text; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand("copy"); + showToast(_("Successfully copied!"), "success"); + } catch (_err) { + showToast(_("Failed to copy!"), "error"); + console.error("copyToClipboard - e", _err); + } + document.body.removeChild(textarea); } -.pdk_dashboard-page__outbound-section__title-section { - display: flex; - align-items: center; - justify-content: space-between; +// src/partials/modal/renderModal.ts +function renderModal(text, name) { + return E( + "div", + { class: "pdk-partial-modal__body" }, + E("div", {}, [ + E("pre", { class: "pdk-partial-modal__content" }, E("code", {}, text)), + E("div", { class: "pdk-partial-modal__footer" }, [ + renderButton({ + classNames: ["cbi-button-apply"], + text: _("Download"), + onClick: () => downloadAsTxt(text, name) + }), + renderButton({ + classNames: ["cbi-button-apply"], + text: _("Copy"), + onClick: () => copyToClipboard(` \`\`\`${name} + ${text} + \`\`\``) + }), + renderButton({ + classNames: ["cbi-button-remove"], + text: _("Close"), + onClick: ui.hideModal + }) + ]) + ]) + ); } -.pdk_dashboard-page__outbound-section__title-section__title { - color: var(--text-color-high); - font-weight: 700; -} +// src/partials/index.ts +var PartialStyles = ` +${styles} +${styles2} +`; -.pdk_dashboard-page__outbound-grid { - margin-top: 5px; - display: grid; - grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); - grid-gap: 10px; +// src/netshift/tabs/dashboard/partials/renderSections.ts +function renderFailedState() { + return E( + "div", + { + class: "card pdk_dashboard-page__outbound-section centered", + style: "height: 127px" + }, + E("span", {}, [E("span", {}, _("Dashboard currently unavailable"))]) + ); } - -.pdk_dashboard-page__outbound-grid__item { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - transition: border 0.2s ease; +function renderLoadingState() { + return E("div", { + id: "dashboard-sections-grid-skeleton", + class: "card pdk_dashboard-page__outbound-section skeleton", + style: "height: 127px" + }); } - -.pdk_dashboard-page__outbound-grid__item--selectable { - cursor: pointer; +function renderDefaultState({ + section, + onChooseOutbound, + onTestLatency, + latencyFetching +}) { + function testLatency() { + if (section.withTagSelect) { + return onTestLatency(section.code); + } + if (section.outbounds.length) { + return onTestLatency(section.outbounds[0].code); + } + } + function renderOutbound(outbound) { + function getLatencyClass() { + if (!outbound.latency) { + return "pdk_dashboard-page__outbound-grid__item__latency--empty"; + } + if (outbound.latency < 800) { + return "pdk_dashboard-page__outbound-grid__item__latency--green"; + } + if (outbound.latency < 1500) { + return "pdk_dashboard-page__outbound-grid__item__latency--yellow"; + } + return "pdk_dashboard-page__outbound-grid__item__latency--red"; + } + return E( + "div", + { + class: `card pdk_dashboard-page__outbound-grid__item ${outbound.selected ? "pdk_dashboard-page__outbound-grid__item--active" : ""} ${section.withTagSelect ? "pdk_dashboard-page__outbound-grid__item--selectable" : ""}`, + click: () => section.withTagSelect && onChooseOutbound(section.code, outbound.code) + }, + [ + E("b", {}, outbound.displayName), + E("div", { class: "pdk_dashboard-page__outbound-grid__item__footer" }, [ + E( + "div", + { class: "pdk_dashboard-page__outbound-grid__item__type" }, + outbound.type + ), + E( + "div", + { class: getLatencyClass() }, + outbound.latency ? `${outbound.latency}ms` : "N/A" + ) + ]) + ] + ); + } + return E("div", { class: "card pdk_dashboard-page__outbound-section" }, [ + // Title with test latency + E("div", { class: "pdk_dashboard-page__outbound-section__title-section" }, [ + E( + "div", + { + class: "pdk_dashboard-page__outbound-section__title-section__title" + }, + section.displayName + ), + latencyFetching ? E("div", { class: "skeleton", style: "width: 99px; height: 28px" }) : renderButton({ + text: _("Test latency"), + onClick: () => testLatency(), + classNames: ["dashboard-sections-grid-item-test-latency"] + }) + ]), + E( + "div", + { class: "pdk_dashboard-page__outbound-grid" }, + section.outbounds.map((outbound) => renderOutbound(outbound)) + ) + ]); } - -.pdk_dashboard-page__outbound-grid__item--selectable:hover { - border-color: var(--primary-color-high, dodgerblue); +function renderSections(props) { + if (props.failed) { + return renderFailedState(); + } + if (props.loading) { + return renderLoadingState(); + } + return renderDefaultState(props); } -.pdk_dashboard-page__outbound-grid__item--active { - border-color: var(--success-color-medium, green); +// src/netshift/tabs/dashboard/partials/renderWidget.ts +function renderFailedState2() { + return E( + "div", + { + id: "", + style: "height: 78px", + class: "card pdk_dashboard-page__widgets-section__item centered" + }, + _("Currently unavailable") + ); } - -.pdk_dashboard-page__outbound-grid__item__footer { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 10px; +function renderLoadingState2() { + return E( + "div", + { + id: "", + style: "height: 78px", + class: "card pdk_dashboard-page__widgets-section__item skeleton" + }, + "" + ); } - -.pdk_dashboard-page__outbound-grid__item__type {} - -.pdk_dashboard-page__outbound-grid__item__latency--empty { - color: var(--primary-color-low, lightgray); +function renderDefaultState2({ title, items }) { + return E("div", { class: "card pdk_dashboard-page__widgets-section__item" }, [ + E( + "b", + { class: "pdk_dashboard-page__widgets-section__item__title" }, + title + ), + ...items.map( + (item) => E( + "div", + { + class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ""}` + }, + [ + E( + "span", + { class: "pdk_dashboard-page__widgets-section__item__row__key" }, + `${item.key}: ` + ), + E( + "span", + { class: "pdk_dashboard-page__widgets-section__item__row__value" }, + item.value + ) + ] + ) + ) + ]); } - -.pdk_dashboard-page__outbound-grid__item__latency--green { - color: var(--success-color-medium, green); +function renderWidget(props) { + if (props.loading) { + return renderLoadingState2(); + } + if (props.failed) { + return renderFailedState2(); + } + return renderDefaultState2(props); } -.pdk_dashboard-page__outbound-grid__item__latency--yellow { - color: var(--warn-color-medium, orange); +// src/netshift/tabs/dashboard/render.ts +function render() { + return E( + "div", + { + id: "dashboard-status", + class: "pdk_dashboard-page" + }, + [ + // Widgets section + E("div", { class: "pdk_dashboard-page__widgets-section" }, [ + E( + "div", + { id: "dashboard-widget-traffic" }, + renderWidget({ loading: true, failed: false, title: "", items: [] }) + ), + E( + "div", + { id: "dashboard-widget-traffic-total" }, + renderWidget({ loading: true, failed: false, title: "", items: [] }) + ), + E( + "div", + { id: "dashboard-widget-system-info" }, + renderWidget({ loading: true, failed: false, title: "", items: [] }) + ), + E( + "div", + { id: "dashboard-widget-service-info" }, + renderWidget({ loading: true, failed: false, title: "", items: [] }) + ) + ]), + // All outbounds + E( + "div", + { id: "dashboard-sections-grid" }, + renderSections({ + loading: true, + failed: false, + section: { + code: "", + displayName: "", + outbounds: [], + withTagSelect: false + }, + onTestLatency: () => { + }, + onChooseOutbound: () => { + }, + latencyFetching: false + }) + ) + ] + ); } -.pdk_dashboard-page__outbound-grid__item__latency--red { - color: var(--error-color-medium, red); +// src/helpers/prettyBytes.ts +function prettyBytes(n) { + const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + if (n < 1e3) { + return n + " B"; + } + const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1); + n = Number((n / Math.pow(1e3, exponent)).toPrecision(3)); + const unit = UNITS[exponent]; + return n + " " + unit; } -`; - -// src/netshift/tabs/dashboard/index.ts -var DashboardTab = { - render, - initController, - styles -}; - -// src/netshift/tabs/diagnostic/renderDiagnostic.ts -function render2() { - return E("div", { id: "diagnostic-status", class: "pdk_diagnostic-page" }, [ - E("div", { class: "pdk_diagnostic-page__left-bar" }, [ - E("div", { id: "pdk_diagnostic-page-run-check" }), - E("div", { - class: "pdk_diagnostic-page__checks", - id: "pdk_diagnostic-page-checks" - }) - ]), - E("div", { class: "pdk_diagnostic-page__right-bar" }, [ - E("div", { id: "pdk_diagnostic-page-wiki" }), - E("div", { id: "pdk_diagnostic-page-actions" }), - E("div", { id: "pdk_diagnostic-page-system-info" }) - ]) +// src/netshift/fetchers/fetchServicesInfo.ts +async function fetchServicesInfo() { + const [netshift, singbox] = await Promise.all([ + NetShiftShellMethods.getStatus(), + NetShiftShellMethods.getSingBoxStatus() ]); + if (!netshift.success || !singbox.success) { + store.set({ + servicesInfoWidget: { + loading: false, + failed: true, + data: { singbox: 0, netshift: 0 } + } + }); + } + if (netshift.success && singbox.success) { + store.set({ + servicesInfoWidget: { + loading: false, + failed: false, + data: { + singbox: singbox.data.running, + netshift: netshift.data.enabled + } + } + }); + } } -// src/netshift/tabs/diagnostic/checks/updateCheckStore.ts -function updateCheckStore(check, minified) { - const diagnosticsChecks = store.get().diagnosticsChecks; - const other = diagnosticsChecks.filter((item) => item.code !== check.code); - const smallCheck = { - ...check, - items: check.items.filter((item) => item.state !== "success") - }; - const targetCheck = minified ? smallCheck : check; +// src/netshift/tabs/dashboard/initController.ts +async function fetchDashboardSections() { + const prev = store.get().sectionsWidget; + store.set({ + sectionsWidget: { + ...prev, + failed: false + } + }); + const { data, success } = await CustomNetShiftMethods.getDashboardSections(); + if (!success) { + logger.error("[DASHBOARD]", "fetchDashboardSections: failed to fetch"); + } + store.set({ + sectionsWidget: { + latencyFetching: false, + loading: false, + failed: !success, + data + } + }); +} +async function connectToClashSockets() { + const clashApiSecret = await getClashApiSecret(); + socket.subscribe( + `${getClashWsUrl()}/traffic?token=${clashApiSecret}`, + (msg) => { + const parsedMsg = JSON.parse(msg); + store.set({ + bandwidthWidget: { + loading: false, + failed: false, + data: { up: parsedMsg.up, down: parsedMsg.down } + } + }); + }, + (_err) => { + logger.error( + "[DASHBOARD]", + "connectToClashSockets - traffic: failed to connect to", + getClashWsUrl() + ); + store.set({ + bandwidthWidget: { + loading: false, + failed: true, + data: { up: 0, down: 0 } + } + }); + } + ); + socket.subscribe( + `${getClashWsUrl()}/connections?token=${clashApiSecret}`, + (msg) => { + const parsedMsg = JSON.parse(msg); + store.set({ + trafficTotalWidget: { + loading: false, + failed: false, + data: { + downloadTotal: parsedMsg.downloadTotal, + uploadTotal: parsedMsg.uploadTotal + } + }, + systemInfoWidget: { + loading: false, + failed: false, + data: { + connections: parsedMsg.connections?.length, + memory: parsedMsg.memory + } + } + }); + }, + (_err) => { + logger.error( + "[DASHBOARD]", + "connectToClashSockets - connections: failed to connect to", + getClashWsUrl() + ); + store.set({ + trafficTotalWidget: { + loading: false, + failed: true, + data: { downloadTotal: 0, uploadTotal: 0 } + }, + systemInfoWidget: { + loading: false, + failed: true, + data: { + connections: 0, + memory: 0 + } + } + }); + } + ); +} +async function handleChooseOutbound(selector, tag) { + await NetShiftShellMethods.setClashApiGroupProxy(selector, tag); + await fetchDashboardSections(); +} +async function handleTestGroupLatency(tag) { + store.set({ + sectionsWidget: { + ...store.get().sectionsWidget, + latencyFetching: true + } + }); + await NetShiftShellMethods.getClashApiGroupLatency(tag); + await fetchDashboardSections(); + store.set({ + sectionsWidget: { + ...store.get().sectionsWidget, + latencyFetching: false + } + }); +} +async function handleTestProxyLatency(tag) { store.set({ - diagnosticsChecks: [...other, targetCheck] + sectionsWidget: { + ...store.get().sectionsWidget, + latencyFetching: true + } }); -} - -// src/netshift/tabs/diagnostic/helpers/getMeta.ts -function getMeta({ allGood, atLeastOneGood }) { - if (allGood) { - return { - state: "success", - description: _("Checks passed") - }; - } - if (atLeastOneGood) { - return { - state: "warning", - description: _("Issues detected") - }; - } - return { - state: "error", - description: _("Checks failed") - }; -} - -// src/netshift/tabs/diagnostic/checks/runDnsCheck.ts -async function runDnsCheck() { - const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS; - updateCheckStore({ - order, - code, - title, - description: _("Checking, please wait"), - state: "loading", - items: [] + await NetShiftShellMethods.getClashApiProxyLatency(tag); + await fetchDashboardSections(); + store.set({ + sectionsWidget: { + ...store.get().sectionsWidget, + latencyFetching: false + } }); - const dnsChecks = await NetShiftShellMethods.checkDNSAvailable(); - if (!dnsChecks.success) { - updateCheckStore({ - order, - code, - title, - description: _("Cannot receive checks result"), - state: "error", - items: [] +} +async function renderSectionsWidget() { + logger.debug("[DASHBOARD]", "renderSectionsWidget"); + const sectionsWidget = store.get().sectionsWidget; + const container = document.getElementById("dashboard-sections-grid"); + if (sectionsWidget.loading || sectionsWidget.failed) { + const renderedWidget = renderSections({ + loading: sectionsWidget.loading, + failed: sectionsWidget.failed, + section: { + code: "", + displayName: "", + outbounds: [], + withTagSelect: false + }, + onTestLatency: () => { + }, + onChooseOutbound: () => { + }, + latencyFetching: sectionsWidget.latencyFetching + }); + return preserveScrollForPage(() => { + container.replaceChildren(renderedWidget); }); - throw new Error("DNS checks failed"); } - const data = dnsChecks.data; - const allGood = Boolean(data.dns_on_router) && Boolean(data.dhcp_config_status) && Boolean(data.bootstrap_dns_status) && Boolean(data.dns_status); - const atLeastOneGood = Boolean(data.dns_on_router) || Boolean(data.dhcp_config_status) || Boolean(data.bootstrap_dns_status) || Boolean(data.dns_status); - const { state, description } = getMeta({ atLeastOneGood, allGood }); - updateCheckStore({ - order, - code, - title, - description, - state, - items: [ - ...insertIf( - data.dns_type === "doh" || data.dns_type === "dot" || !data.bootstrap_dns_status, - [ - { - state: data.bootstrap_dns_status ? "success" : "error", - key: _("Bootsrap DNS"), - value: data.bootstrap_dns_server - } - ] - ), - { - state: data.dns_status ? "success" : "error", - key: _("Main DNS"), - value: `${data.dns_server} [${data.dns_type}]` - }, - ...insertIf( - typeof data.dns_via_outbound_tag === "string" && data.dns_via_outbound_tag.length > 0, - [ - { - state: "success", - key: _("Main DNS via outbound"), - value: data.dns_via_outbound_tag ?? "" - } - ] - ), - { - state: data.dns_on_router ? "success" : "error", - key: _("DNS on router"), - value: "" + const renderedWidgets = sectionsWidget.data.map( + (section) => renderSections({ + loading: sectionsWidget.loading, + failed: sectionsWidget.failed, + section, + latencyFetching: sectionsWidget.latencyFetching, + onTestLatency: (tag) => { + if (section.withTagSelect) { + return handleTestGroupLatency(tag); + } + return handleTestProxyLatency(tag); }, - { - state: data.dhcp_config_status ? "success" : "error", - key: _("DHCP has DNS server"), - value: "" + onChooseOutbound: (selector, tag) => { + handleChooseOutbound(selector, tag); } - ] + }) + ); + return preserveScrollForPage(() => { + container.replaceChildren(...renderedWidgets); }); - if (!atLeastOneGood) { - throw new Error("DNS checks failed"); - } } - -// src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts -async function runSingBoxCheck() { - const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX; - updateCheckStore({ - order, - code, - title, - description: _("Checking, please wait"), - state: "loading", - items: [] - }); - const singBoxChecks = await NetShiftShellMethods.checkSingBox(); - if (!singBoxChecks.success) { - updateCheckStore({ - order, - code, - title, - description: _("Cannot receive checks result"), - state: "error", +async function renderBandwidthWidget() { + logger.debug("[DASHBOARD]", "renderBandwidthWidget"); + const traffic = store.get().bandwidthWidget; + const container = document.getElementById("dashboard-widget-traffic"); + if (traffic.loading || traffic.failed) { + const renderedWidget2 = renderWidget({ + loading: traffic.loading, + failed: traffic.failed, + title: "", items: [] }); - throw new Error("Sing-box checks failed"); + return container.replaceChildren(renderedWidget2); } - const data = singBoxChecks.data; - const allGood = Boolean(data.sing_box_installed) && Boolean(data.sing_box_version_ok) && Boolean(data.sing_box_service_exist) && Boolean(data.sing_box_autostart_disabled) && Boolean(data.sing_box_process_running) && Boolean(data.sing_box_ports_listening); - const atLeastOneGood = Boolean(data.sing_box_installed) || Boolean(data.sing_box_version_ok) || Boolean(data.sing_box_service_exist) || Boolean(data.sing_box_autostart_disabled) || Boolean(data.sing_box_process_running) || Boolean(data.sing_box_ports_listening); - const { state, description } = getMeta({ atLeastOneGood, allGood }); - updateCheckStore({ - order, - code, - title, - description, - state, + const renderedWidget = renderWidget({ + loading: traffic.loading, + failed: traffic.failed, + title: _("Traffic"), items: [ - { - state: data.sing_box_installed ? "success" : "error", - key: _("Sing-box installed"), - value: "" - }, - { - state: data.sing_box_version_ok ? "success" : "error", - key: _("Sing-box version is compatible (newer than 1.12.4)"), - value: "" - }, - { - state: data.sing_box_service_exist ? "success" : "error", - key: _("Sing-box service exist"), - value: "" - }, - { - state: data.sing_box_autostart_disabled ? "success" : "error", - key: _("Sing-box autostart disabled"), - value: "" - }, - { - state: data.sing_box_process_running ? "success" : "error", - key: _("Sing-box process running"), - value: "" - }, - { - state: data.sing_box_ports_listening ? "success" : "error", - key: _("Sing-box listening ports"), - value: "" - } + { key: _("Uplink"), value: `${prettyBytes(traffic.data.up)}/s` }, + { key: _("Downlink"), value: `${prettyBytes(traffic.data.down)}/s` } ] }); - if (!atLeastOneGood || !data.sing_box_process_running) { - throw new Error("Sing-box checks failed"); - } + container.replaceChildren(renderedWidget); } - -// src/netshift/tabs/diagnostic/checks/runNftCheck.ts -async function runNftCheck() { - const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT; - updateCheckStore({ - order, - code, - title, - description: _("Checking, please wait"), - state: "loading", - items: [] - }); - await RemoteFakeIPMethods.getFakeIpCheck(); - await RemoteFakeIPMethods.getIpCheck(); - const nftablesChecks = await NetShiftShellMethods.checkNftRules(); - if (!nftablesChecks.success) { - updateCheckStore({ - order, - code, - title, - description: _("Cannot receive checks result"), - state: "error", +async function renderTrafficTotalWidget() { + logger.debug("[DASHBOARD]", "renderTrafficTotalWidget"); + const trafficTotalWidget = store.get().trafficTotalWidget; + const container = document.getElementById("dashboard-widget-traffic-total"); + if (trafficTotalWidget.loading || trafficTotalWidget.failed) { + const renderedWidget2 = renderWidget({ + loading: trafficTotalWidget.loading, + failed: trafficTotalWidget.failed, + title: "", items: [] }); - throw new Error("Nftables checks failed"); + return container.replaceChildren(renderedWidget2); } - const data = nftablesChecks.data; - const allGood = Boolean(data.table_exist) && Boolean(data.rules_mangle_exist) && Boolean(data.rules_mangle_counters) && Boolean(data.rules_mangle_output_exist) && Boolean(data.rules_proxy_exist) && Boolean(data.rules_proxy_counters) && !data.rules_other_mark_exist; - const atLeastOneGood = Boolean(data.table_exist) || Boolean(data.rules_mangle_exist) || Boolean(data.rules_mangle_counters) || Boolean(data.rules_mangle_output_exist) || Boolean(data.rules_proxy_exist) || Boolean(data.rules_proxy_counters) || !data.rules_other_mark_exist; - const { state, description } = getMeta({ atLeastOneGood, allGood }); - updateCheckStore({ - order, - code, - title, - description, - state, + const renderedWidget = renderWidget({ + loading: trafficTotalWidget.loading, + failed: trafficTotalWidget.failed, + title: _("Traffic Total"), items: [ { - state: data.table_exist ? "success" : "error", - key: _("Table exist"), - value: "" - }, - { - state: data.rules_mangle_exist ? "success" : "error", - key: _("Rules mangle exist"), - value: "" - }, - { - state: data.rules_mangle_counters ? "success" : "error", - key: _("Rules mangle counters"), - value: "" - }, - { - state: data.rules_mangle_output_exist ? "success" : "error", - key: _("Rules mangle output exist"), - value: "" + key: _("Uplink"), + value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)) }, { - state: data.rules_proxy_exist ? "success" : "error", - key: _("Rules proxy exist"), - value: "" - }, + key: _("Downlink"), + value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)) + } + ] + }); + container.replaceChildren(renderedWidget); +} +async function renderSystemInfoWidget() { + logger.debug("[DASHBOARD]", "renderSystemInfoWidget"); + const systemInfoWidget = store.get().systemInfoWidget; + const container = document.getElementById("dashboard-widget-system-info"); + if (systemInfoWidget.loading || systemInfoWidget.failed) { + const renderedWidget2 = renderWidget({ + loading: systemInfoWidget.loading, + failed: systemInfoWidget.failed, + title: "", + items: [] + }); + return container.replaceChildren(renderedWidget2); + } + const renderedWidget = renderWidget({ + loading: systemInfoWidget.loading, + failed: systemInfoWidget.failed, + title: _("System info"), + items: [ { - state: data.rules_proxy_counters ? "success" : "error", - key: _("Rules proxy counters"), - value: "" + key: _("Active Connections"), + value: String(systemInfoWidget.data.connections) }, { - state: !data.rules_other_mark_exist ? "success" : "warning", - key: !data.rules_other_mark_exist ? _("No other marking rules found") : _("Additional marking rules found"), - value: "" + key: _("Memory Usage"), + value: String(prettyBytes(systemInfoWidget.data.memory)) } ] }); - if (!atLeastOneGood) { - throw new Error("Nftables checks failed"); - } + container.replaceChildren(renderedWidget); } - -// src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts -async function runFakeIPCheck() { - const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP; - updateCheckStore({ - order, - code, - title, - description: _("Checking, please wait"), - state: "loading", - items: [] - }); - const routerFakeIPResponse = await NetShiftShellMethods.checkFakeIP(); - const checkFakeIPResponse = await RemoteFakeIPMethods.getFakeIpCheck(); - const checkIPResponse = await RemoteFakeIPMethods.getIpCheck(); - const checks = { - router: routerFakeIPResponse.success && routerFakeIPResponse.data.fakeip, - browserFakeIP: checkFakeIPResponse.success && checkFakeIPResponse.data.fakeip, - differentIP: checkFakeIPResponse.success && checkIPResponse.success && checkFakeIPResponse.data.IP !== checkIPResponse.data.IP - }; - const allGood = checks.router || checks.browserFakeIP || checks.differentIP; - const atLeastOneGood = checks.router && checks.browserFakeIP && checks.differentIP; - const { state, description } = getMeta({ atLeastOneGood, allGood }); - updateCheckStore({ - order, - code, - title, - description, - state, +async function renderServicesInfoWidget() { + logger.debug("[DASHBOARD]", "renderServicesInfoWidget"); + const servicesInfoWidget = store.get().servicesInfoWidget; + const container = document.getElementById("dashboard-widget-service-info"); + if (servicesInfoWidget.loading || servicesInfoWidget.failed) { + const renderedWidget2 = renderWidget({ + loading: servicesInfoWidget.loading, + failed: servicesInfoWidget.failed, + title: "", + items: [] + }); + return container.replaceChildren(renderedWidget2); + } + const renderedWidget = renderWidget({ + loading: servicesInfoWidget.loading, + failed: servicesInfoWidget.failed, + title: _("Services info"), items: [ { - state: checks.router ? "success" : "warning", - key: checks.router ? _("Router DNS is routed through sing-box") : _("Router DNS is not routed through sing-box"), - value: "" + key: _("NetShift"), + value: servicesInfoWidget.data.netshift ? _("\u2714 Enabled") : _("\u2718 Disabled"), + attributes: { + class: servicesInfoWidget.data.netshift ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" + } }, { - state: checks.browserFakeIP ? "success" : "error", - key: checks.browserFakeIP ? _("Browser is using FakeIP correctly") : _("Browser is not using FakeIP"), - value: "" - }, - ...insertIf(checks.browserFakeIP, [ - { - state: checks.differentIP ? "success" : "error", - key: checks.differentIP ? _("Proxy traffic is routed via FakeIP") : _("Proxy traffic is not routed via FakeIP"), - value: "" + key: _("Sing-box"), + value: servicesInfoWidget.data.singbox ? _("\u2714 Running") : _("\u2718 Stopped"), + attributes: { + class: servicesInfoWidget.data.singbox ? "pdk_dashboard-page__widgets-section__item__row--success" : "pdk_dashboard-page__widgets-section__item__row--error" } - ]) + } ] }); + container.replaceChildren(renderedWidget); } - -// src/partials/button/styles.ts -var styles2 = ` -.pdk-partial-button { - text-align: center; +async function onStoreUpdate(next, prev, diff) { + if (diff.sectionsWidget) { + renderSectionsWidget(); + } + if (diff.bandwidthWidget) { + renderBandwidthWidget(); + } + if (diff.trafficTotalWidget) { + renderTrafficTotalWidget(); + } + if (diff.systemInfoWidget) { + renderSystemInfoWidget(); + } + if (diff.servicesInfoWidget) { + renderServicesInfoWidget(); + } } - -.pdk-partial-button--with-icon { - display: flex; - align-items: center; - justify-content: center; +async function onPageMount() { + onPageUnmount(); + store.subscribe(onStoreUpdate); + await fetchDashboardSections(); + await fetchServicesInfo(); + await connectToClashSockets(); } - -.pdk-partial-button--loading { +function onPageUnmount() { + store.unsubscribe(onStoreUpdate); + store.reset([ + "bandwidthWidget", + "trafficTotalWidget", + "systemInfoWidget", + "servicesInfoWidget", + "sectionsWidget" + ]); + socket.resetAll(); } - -.pdk-partial-button--disabled { +function registerLifecycleListeners() { + store.subscribe((next, prev, diff) => { + if (diff.tabService && next.tabService.current !== prev.tabService.current) { + logger.debug( + "[DASHBOARD]", + "active tab diff event, active tab:", + diff.tabService.current + ); + const isDashboardVisible = next.tabService.current === "dashboard"; + if (isDashboardVisible) { + logger.debug( + "[DASHBOARD]", + "registerLifecycleListeners", + "onPageMount" + ); + return onPageMount(); + } + if (!isDashboardVisible) { + logger.debug( + "[DASHBOARD]", + "registerLifecycleListeners", + "onPageUnmount" + ); + return onPageUnmount(); + } + } + }); } - -.pdk-partial-button__icon { - margin-right: 5px; +async function initController() { + onMount("dashboard-status").then(() => { + logger.debug("[DASHBOARD]", "initController", "onMount"); + onPageMount(); + registerLifecycleListeners(); + }); } -.pdk-partial-button__icon { - display: flex; - align-items: center; - justify-content: center; +// src/netshift/tabs/dashboard/styles.ts +var styles3 = ` +#cbi-netshift-dashboard-_mount_node > div { + width: 100%; } -.pdk-partial-button__icon svg { - width: 16px; - height: 16px; +#cbi-netshift-dashboard > h3 { + display: none; } -`; - -// src/partials/modal/styles.ts -var styles3 = ` - -.pdk-partial-modal__body {} - -.pdk-partial-modal__content { - max-height: 70vh; - overflow: scroll; - border-radius: 4px; + +.pdk_dashboard-page { + width: 100%; + --dashboard-grid-columns: 4; } -.pdk-partial-modal__footer { - display: flex; - justify-content: flex-end; +@media (max-width: 900px) { + .pdk_dashboard-page { + --dashboard-grid-columns: 2; + } } -.pdk-partial-modal__footer button { - margin-left: 10px; +.pdk_dashboard-page__widgets-section { + margin-top: 10px; + display: grid; + grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); + grid-gap: 10px; } -`; -// src/icons/renderLoaderCircleIcon24.ts -function renderLoaderCircleIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-loader-circle rotate" - }, - [ - svgEl("path", { - d: "M21 12a9 9 0 1 1-6.219-8.56" - }), - svgEl("animateTransform", { - attributeName: "transform", - attributeType: "XML", - type: "rotate", - from: "0 12 12", - to: "360 12 12", - dur: "1s", - repeatCount: "indefinite" - }) - ] - ); +.pdk_dashboard-page__widgets-section__item { } -// src/icons/renderCircleAlertIcon24.ts -function renderCircleAlertIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - width: "24", - height: "24", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-circle-alert-icon lucide-circle-alert" - }, - [ - svgEl("circle", { - cx: "12", - cy: "12", - r: "10" - }), - svgEl("line", { - x1: "12", - y1: "8", - x2: "12", - y2: "12" - }), - svgEl("line", { - x1: "12", - y1: "16", - x2: "12.01", - y2: "16" - }) - ] - ); -} +.pdk_dashboard-page__widgets-section__item__title {} -// src/icons/renderCircleCheckIcon24.ts -function renderCircleCheckIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - width: "24", - height: "24", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-circle-check-icon lucide-circle-check" - }, - [ - svgEl("circle", { - cx: "12", - cy: "12", - r: "10" - }), - svgEl("path", { - d: "M9 12l2 2 4-4" - }) - ] - ); -} +.pdk_dashboard-page__widgets-section__item__row {} -// src/icons/renderCircleSlashIcon24.ts -function renderCircleSlashIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - width: "24", - height: "24", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-circle-slash-icon lucide-circle-slash" - }, - [ - svgEl("circle", { - cx: "12", - cy: "12", - r: "10" - }), - svgEl("line", { - x1: "9", - y1: "15", - x2: "15", - y2: "9" - }) - ] - ); +.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value { + color: var(--success-color-medium, green); } -// src/icons/renderCircleXIcon24.ts -function renderCircleXIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - width: "24", - height: "24", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-circle-x-icon lucide-circle-x" - }, - [ - svgEl("circle", { - cx: "12", - cy: "12", - r: "10" - }), - svgEl("path", { - d: "M15 9L9 15" - }), - svgEl("path", { - d: "M9 9L15 15" - }) - ] - ); +.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value { + color: var(--error-color-medium, red); } -// src/icons/renderCheckIcon24.ts -function renderCheckIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-check-icon lucide-check" - }, - [ - svgEl("path", { - d: "M20 6 9 17l-5-5" - }) - ] - ); +.pdk_dashboard-page__widgets-section__item__row__key {} + +.pdk_dashboard-page__widgets-section__item__row__value {} + +.pdk_dashboard-page__outbound-section { + margin-top: 10px; } -// src/icons/renderXIcon24.ts -function renderXIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-x-icon lucide-x" - }, - [svgEl("path", { d: "M18 6 6 18" }), svgEl("path", { d: "m6 6 12 12" })] - ); +.pdk_dashboard-page__outbound-section__title-section { + display: flex; + align-items: center; + justify-content: space-between; } -// src/icons/renderTriangleAlertIcon24.ts -function renderTriangleAlertIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-triangle-alert-icon lucide-triangle-alert" - }, - [ - svgEl("path", { - d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" - }), - svgEl("path", { d: "M12 9v4" }), - svgEl("path", { d: "M12 17h.01" }) - ] - ); +.pdk_dashboard-page__outbound-section__title-section__title { + color: var(--text-color-high); + font-weight: 700; } -// src/icons/renderPauseIcon24.ts -function renderPauseIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-pause-icon lucide-pause" - }, - [ - svgEl("rect", { - x: "14", - y: "3", - width: "5", - height: "18", - rx: "1" - }), - svgEl("rect", { - x: "5", - y: "3", - width: "5", - height: "18", - rx: "1" - }) - ] - ); +.pdk_dashboard-page__outbound-grid { + margin-top: 5px; + display: grid; + grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr); + grid-gap: 10px; } -// src/icons/renderPlayIcon24.ts -function renderPlayIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-play-icon lucide-play" - }, - [ - svgEl("path", { - d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" - }) - ] - ); +.pdk_dashboard-page__outbound-grid__item { + transition: border 0.2s ease; } -// src/icons/renderRotateCcwIcon24.ts -function renderRotateCcwIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-rotate-ccw-icon lucide-rotate-ccw" - }, - [ - svgEl("path", { - d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" - }), - svgEl("path", { - d: "M3 3v5h5" - }) - ] - ); +.pdk_dashboard-page__outbound-grid__item--selectable { + cursor: pointer; } -// src/icons/renderCircleStopIcon24.ts -function renderCircleStopIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-circle-stop-icon lucide-circle-stop" - }, - [ - svgEl("circle", { - cx: "12", - cy: "12", - r: "10" - }), - svgEl("rect", { - x: "9", - y: "9", - width: "6", - height: "6", - rx: "1" - }) - ] - ); +.pdk_dashboard-page__outbound-grid__item--selectable:hover { + border-color: var(--primary-color-high, dodgerblue); } -// src/icons/renderCirclePlayIcon24.ts -function renderCirclePlayIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-circle-play-icon lucide-circle-play" - }, - [ - svgEl("path", { - d: "M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z" - }), - svgEl("circle", { - cx: "12", - cy: "12", - r: "10" - }) - ] - ); +.pdk_dashboard-page__outbound-grid__item--active { + border-color: var(--success-color-medium, green); } -// src/icons/renderCircleCheckBigIcon24.ts -function renderCircleCheckBigIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-circle-check-big-icon lucide-circle-check-big" - }, - [ - svgEl("path", { - d: "M21.801 10A10 10 0 1 1 17 3.335" - }), - svgEl("path", { - d: "m9 11 3 3L22 4" - }) - ] - ); +.pdk_dashboard-page__outbound-grid__item__footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 10px; } -// src/icons/renderSquareChartGanttIcon24.ts -function renderSquareChartGanttIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-square-chart-gantt-icon lucide-square-chart-gantt" - }, - [ - svgEl("rect", { - width: "18", - height: "18", - x: "3", - y: "3", - rx: "2" - }), - svgEl("path", { d: "M9 8h7" }), - svgEl("path", { d: "M8 12h6" }), - svgEl("path", { d: "M11 16h5" }) - ] - ); +.pdk_dashboard-page__outbound-grid__item__type {} + +.pdk_dashboard-page__outbound-grid__item__latency--empty { + color: var(--primary-color-low, lightgray); } -// src/icons/renderCogIcon24.ts -function renderCogIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-cog-icon lucide-cog" - }, - [ - svgEl("path", { d: "M11 10.27 7 3.34" }), - svgEl("path", { d: "m11 13.73-4 6.93" }), - svgEl("path", { d: "M12 22v-2" }), - svgEl("path", { d: "M12 2v2" }), - svgEl("path", { d: "M14 12h8" }), - svgEl("path", { d: "m17 20.66-1-1.73" }), - svgEl("path", { d: "m17 3.34-1 1.73" }), - svgEl("path", { d: "M2 12h2" }), - svgEl("path", { d: "m20.66 17-1.73-1" }), - svgEl("path", { d: "m20.66 7-1.73 1" }), - svgEl("path", { d: "m3.34 17 1.73-1" }), - svgEl("path", { d: "m3.34 7 1.73 1" }), - svgEl("circle", { cx: "12", cy: "12", r: "2" }), - svgEl("circle", { cx: "12", cy: "12", r: "8" }) - ] - ); +.pdk_dashboard-page__outbound-grid__item__latency--green { + color: var(--success-color-medium, green); +} + +.pdk_dashboard-page__outbound-grid__item__latency--yellow { + color: var(--warn-color-medium, orange); +} + +.pdk_dashboard-page__outbound-grid__item__latency--red { + color: var(--error-color-medium, red); +} + +`; + +// src/netshift/tabs/dashboard/index.ts +var DashboardTab = { + render, + initController, + styles: styles3 +}; + +// src/netshift/tabs/diagnostic/renderDiagnostic.ts +function render2() { + return E("div", { id: "diagnostic-status", class: "pdk_diagnostic-page" }, [ + E("div", { class: "pdk_diagnostic-page__left-bar" }, [ + E("div", { id: "pdk_diagnostic-page-run-check" }), + E("div", { + class: "pdk_diagnostic-page__checks", + id: "pdk_diagnostic-page-checks" + }) + ]), + E("div", { class: "pdk_diagnostic-page__right-bar" }, [ + E("div", { id: "pdk_diagnostic-page-wiki" }), + E("div", { id: "pdk_diagnostic-page-actions" }), + E("div", { id: "pdk_diagnostic-page-system-info" }) + ]) + ]); } -// src/icons/renderSearchIcon24.ts -function renderSearchIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-search-icon lucide-search" - }, - [ - svgEl("path", { d: "m21 21-4.34-4.34" }), - svgEl("circle", { cx: "11", cy: "11", r: "8" }) - ] - ); +// src/netshift/tabs/diagnostic/checks/updateCheckStore.ts +function updateCheckStore(check, minified) { + const diagnosticsChecks = store.get().diagnosticsChecks; + const other = diagnosticsChecks.filter((item) => item.code !== check.code); + const smallCheck = { + ...check, + items: check.items.filter((item) => item.state !== "success") + }; + const targetCheck = minified ? smallCheck : check; + store.set({ + diagnosticsChecks: [...other, targetCheck] + }); } -// src/icons/renderBookOpenTextIcon24.ts -function renderBookOpenTextIcon24() { - const NS = "http://www.w3.org/2000/svg"; - return svgEl( - "svg", - { - xmlns: NS, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - class: "lucide lucide-book-open-text-icon lucide-book-open-text" - }, - [ - svgEl("path", { d: "M12 7v14" }), - svgEl("path", { d: "M16 12h2" }), - svgEl("path", { d: "M16 8h2" }), - svgEl("path", { - d: "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" - }), - svgEl("path", { d: "M6 12h2" }), - svgEl("path", { d: "M6 8h2" }) - ] - ); +// src/netshift/tabs/diagnostic/helpers/getMeta.ts +function getMeta({ allGood, atLeastOneGood }) { + if (allGood) { + return { + state: "success", + description: _("Checks passed") + }; + } + if (atLeastOneGood) { + return { + state: "warning", + description: _("Issues detected") + }; + } + return { + state: "error", + description: _("Checks failed") + }; } -// src/partials/button/renderButton.ts -function renderButton({ - classNames = [], - disabled, - loading, - onClick, - text, - icon -}) { - const hasIcon = !!loading || !!icon; - function getWrappedIcon() { - const iconWrap = E("span", { - class: "pdk-partial-button__icon" +// src/netshift/tabs/diagnostic/checks/runDnsCheck.ts +async function runDnsCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS; + updateCheckStore({ + order, + code, + title, + description: _("Checking, please wait"), + state: "loading", + items: [] + }); + const dnsChecks = await NetShiftShellMethods.checkDNSAvailable(); + if (!dnsChecks.success) { + updateCheckStore({ + order, + code, + title, + description: _("Cannot receive checks result"), + state: "error", + items: [] }); - if (loading) { - iconWrap.appendChild(renderLoaderCircleIcon24()); - return iconWrap; - } - if (icon) { - iconWrap.appendChild(icon()); - return iconWrap; - } - return iconWrap; + throw new Error("DNS checks failed"); } - function getClass() { - return [ - "btn", - "pdk-partial-button", - ...insertIf(Boolean(disabled), ["pdk-partial-button--disabled"]), - ...insertIf(Boolean(loading), ["pdk-partial-button--loading"]), - ...insertIf(Boolean(hasIcon), ["pdk-partial-button--with-icon"]), - ...classNames - ].filter(Boolean).join(" "); + const data = dnsChecks.data; + const allGood = Boolean(data.dns_on_router) && Boolean(data.dhcp_config_status) && Boolean(data.bootstrap_dns_status) && Boolean(data.dns_status); + const atLeastOneGood = Boolean(data.dns_on_router) || Boolean(data.dhcp_config_status) || Boolean(data.bootstrap_dns_status) || Boolean(data.dns_status); + const { state, description } = getMeta({ atLeastOneGood, allGood }); + updateCheckStore({ + order, + code, + title, + description, + state, + items: [ + ...insertIf( + data.dns_type === "doh" || data.dns_type === "dot" || !data.bootstrap_dns_status, + [ + { + state: data.bootstrap_dns_status ? "success" : "error", + key: _("Bootsrap DNS"), + value: data.bootstrap_dns_server + } + ] + ), + { + state: data.dns_status ? "success" : "error", + key: _("Main DNS"), + value: `${data.dns_server} [${data.dns_type}]` + }, + ...insertIf( + typeof data.dns_via_outbound_tag === "string" && data.dns_via_outbound_tag.length > 0, + [ + { + state: "success", + key: _("Main DNS via outbound"), + value: data.dns_via_outbound_tag ?? "" + } + ] + ), + { + state: data.dns_on_router ? "success" : "error", + key: _("DNS on router"), + value: "" + }, + { + state: data.dhcp_config_status ? "success" : "error", + key: _("DHCP has DNS server"), + value: "" + } + ] + }); + if (!atLeastOneGood) { + throw new Error("DNS checks failed"); } - function getDisabled() { - if (loading || disabled) { - return true; - } - return void 0; +} + +// src/netshift/tabs/diagnostic/checks/runSingBoxCheck.ts +async function runSingBoxCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX; + updateCheckStore({ + order, + code, + title, + description: _("Checking, please wait"), + state: "loading", + items: [] + }); + const singBoxChecks = await NetShiftShellMethods.checkSingBox(); + if (!singBoxChecks.success) { + updateCheckStore({ + order, + code, + title, + description: _("Cannot receive checks result"), + state: "error", + items: [] + }); + throw new Error("Sing-box checks failed"); + } + const data = singBoxChecks.data; + const allGood = Boolean(data.sing_box_installed) && Boolean(data.sing_box_version_ok) && Boolean(data.sing_box_service_exist) && Boolean(data.sing_box_autostart_disabled) && Boolean(data.sing_box_process_running) && Boolean(data.sing_box_ports_listening); + const atLeastOneGood = Boolean(data.sing_box_installed) || Boolean(data.sing_box_version_ok) || Boolean(data.sing_box_service_exist) || Boolean(data.sing_box_autostart_disabled) || Boolean(data.sing_box_process_running) || Boolean(data.sing_box_ports_listening); + const { state, description } = getMeta({ atLeastOneGood, allGood }); + updateCheckStore({ + order, + code, + title, + description, + state, + items: [ + { + state: data.sing_box_installed ? "success" : "error", + key: _("Sing-box installed"), + value: "" + }, + { + state: data.sing_box_version_ok ? "success" : "error", + key: _("Sing-box version is compatible (newer than 1.12.4)"), + value: "" + }, + { + state: data.sing_box_service_exist ? "success" : "error", + key: _("Sing-box service exist"), + value: "" + }, + { + state: data.sing_box_autostart_disabled ? "success" : "error", + key: _("Sing-box autostart disabled"), + value: "" + }, + { + state: data.sing_box_process_running ? "success" : "error", + key: _("Sing-box process running"), + value: "" + }, + { + state: data.sing_box_ports_listening ? "success" : "error", + key: _("Sing-box listening ports"), + value: "" + } + ] + }); + if (!atLeastOneGood || !data.sing_box_process_running) { + throw new Error("Sing-box checks failed"); } - return E( - "button", - { class: getClass(), disabled: getDisabled(), click: onClick }, - [...insertIf(hasIcon, [getWrappedIcon()]), E("span", {}, text)] - ); } -// src/helpers/showToast.ts -function showToast(message, type, duration = 3e3) { - let container = document.querySelector(".toast-container"); - if (!container) { - container = document.createElement("div"); - container.className = "toast-container"; - document.body.appendChild(container); +// src/netshift/tabs/diagnostic/checks/runNftCheck.ts +async function runNftCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT; + updateCheckStore({ + order, + code, + title, + description: _("Checking, please wait"), + state: "loading", + items: [] + }); + await RemoteFakeIPMethods.getFakeIpCheck(); + await RemoteFakeIPMethods.getIpCheck(); + const nftablesChecks = await NetShiftShellMethods.checkNftRules(); + if (!nftablesChecks.success) { + updateCheckStore({ + order, + code, + title, + description: _("Cannot receive checks result"), + state: "error", + items: [] + }); + throw new Error("Nftables checks failed"); } - const toast = document.createElement("div"); - toast.className = `toast toast-${type}`; - toast.textContent = message; - container.appendChild(toast); - setTimeout(() => toast.classList.add("visible"), 100); - setTimeout(() => { - toast.classList.remove("visible"); - setTimeout(() => toast.remove(), 300); - }, duration); -} - -// src/helpers/copyToClipboard.ts -function copyToClipboard(text) { - const textarea = document.createElement("textarea"); - textarea.value = text; - document.body.appendChild(textarea); - textarea.select(); - try { - document.execCommand("copy"); - showToast(_("Successfully copied!"), "success"); - } catch (_err) { - showToast(_("Failed to copy!"), "error"); - console.error("copyToClipboard - e", _err); + const data = nftablesChecks.data; + const allGood = Boolean(data.table_exist) && Boolean(data.rules_mangle_exist) && Boolean(data.rules_mangle_counters) && Boolean(data.rules_mangle_output_exist) && Boolean(data.rules_proxy_exist) && Boolean(data.rules_proxy_counters) && !data.rules_other_mark_exist; + const atLeastOneGood = Boolean(data.table_exist) || Boolean(data.rules_mangle_exist) || Boolean(data.rules_mangle_counters) || Boolean(data.rules_mangle_output_exist) || Boolean(data.rules_proxy_exist) || Boolean(data.rules_proxy_counters) || !data.rules_other_mark_exist; + const { state, description } = getMeta({ atLeastOneGood, allGood }); + updateCheckStore({ + order, + code, + title, + description, + state, + items: [ + { + state: data.table_exist ? "success" : "error", + key: _("Table exist"), + value: "" + }, + { + state: data.rules_mangle_exist ? "success" : "error", + key: _("Rules mangle exist"), + value: "" + }, + { + state: data.rules_mangle_counters ? "success" : "error", + key: _("Rules mangle counters"), + value: "" + }, + { + state: data.rules_mangle_output_exist ? "success" : "error", + key: _("Rules mangle output exist"), + value: "" + }, + { + state: data.rules_proxy_exist ? "success" : "error", + key: _("Rules proxy exist"), + value: "" + }, + { + state: data.rules_proxy_counters ? "success" : "error", + key: _("Rules proxy counters"), + value: "" + }, + { + state: !data.rules_other_mark_exist ? "success" : "warning", + key: !data.rules_other_mark_exist ? _("No other marking rules found") : _("Additional marking rules found"), + value: "" + } + ] + }); + if (!atLeastOneGood) { + throw new Error("Nftables checks failed"); } - document.body.removeChild(textarea); } -// src/partials/modal/renderModal.ts -function renderModal(text, name) { - return E( - "div", - { class: "pdk-partial-modal__body" }, - E("div", {}, [ - E("pre", { class: "pdk-partial-modal__content" }, E("code", {}, text)), - E("div", { class: "pdk-partial-modal__footer" }, [ - renderButton({ - classNames: ["cbi-button-apply"], - text: _("Download"), - onClick: () => downloadAsTxt(text, name) - }), - renderButton({ - classNames: ["cbi-button-apply"], - text: _("Copy"), - onClick: () => copyToClipboard(` \`\`\`${name} - ${text} - \`\`\``) - }), - renderButton({ - classNames: ["cbi-button-remove"], - text: _("Close"), - onClick: ui.hideModal - }) +// src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts +async function runFakeIPCheck() { + const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP; + updateCheckStore({ + order, + code, + title, + description: _("Checking, please wait"), + state: "loading", + items: [] + }); + const routerFakeIPResponse = await NetShiftShellMethods.checkFakeIP(); + const checkFakeIPResponse = await RemoteFakeIPMethods.getFakeIpCheck(); + const checkIPResponse = await RemoteFakeIPMethods.getIpCheck(); + const checks = { + router: routerFakeIPResponse.success && routerFakeIPResponse.data.fakeip, + browserFakeIP: checkFakeIPResponse.success && checkFakeIPResponse.data.fakeip, + differentIP: checkFakeIPResponse.success && checkIPResponse.success && checkFakeIPResponse.data.IP !== checkIPResponse.data.IP + }; + const allGood = checks.router || checks.browserFakeIP || checks.differentIP; + const atLeastOneGood = checks.router && checks.browserFakeIP && checks.differentIP; + const { state, description } = getMeta({ atLeastOneGood, allGood }); + updateCheckStore({ + order, + code, + title, + description, + state, + items: [ + { + state: checks.router ? "success" : "warning", + key: checks.router ? _("Router DNS is routed through sing-box") : _("Router DNS is not routed through sing-box"), + value: "" + }, + { + state: checks.browserFakeIP ? "success" : "error", + key: checks.browserFakeIP ? _("Browser is using FakeIP correctly") : _("Browser is not using FakeIP"), + value: "" + }, + ...insertIf(checks.browserFakeIP, [ + { + state: checks.differentIP ? "success" : "error", + key: checks.differentIP ? _("Proxy traffic is routed via FakeIP") : _("Proxy traffic is not routed via FakeIP"), + value: "" + } ]) - ]) - ); + ] + }); } -// src/partials/index.ts -var PartialStyles = ` -${styles2} -${styles3} -`; - // src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts function renderAvailableActions({ restart, @@ -3990,7 +3978,7 @@ function renderAvailableActions({ viewLogs, showSingBoxConfig }) { - return E("div", { class: "pdk_diagnostic-page__right-bar__actions" }, [ + return E("div", { class: "card pdk_diagnostic-page__right-bar__actions" }, [ E("b", {}, _("Available actions")), ...insertIf(restart.visible, [ renderButton({ @@ -4108,7 +4096,7 @@ function renderLoadingState3(props) { iconWrap.appendChild(renderLoaderCircleIcon24()); return E( "div", - { class: "pdk_diagnostic_alert pdk_diagnostic_alert--loading" }, + { class: "card pdk_diagnostic_alert pdk_diagnostic_alert--loading" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ @@ -4129,7 +4117,7 @@ function renderWarningState(props) { iconWrap.appendChild(renderCircleAlertIcon24()); return E( "div", - { class: "pdk_diagnostic_alert pdk_diagnostic_alert--warning" }, + { class: "card pdk_diagnostic_alert pdk_diagnostic_alert--warning" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ @@ -4150,7 +4138,7 @@ function renderErrorState(props) { iconWrap.appendChild(renderCircleXIcon24()); return E( "div", - { class: "pdk_diagnostic_alert pdk_diagnostic_alert--error" }, + { class: "card pdk_diagnostic_alert pdk_diagnostic_alert--error" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ @@ -4171,7 +4159,7 @@ function renderSuccessState(props) { iconWrap.appendChild(renderCircleCheckIcon24()); return E( "div", - { class: "pdk_diagnostic_alert pdk_diagnostic_alert--success" }, + { class: "card pdk_diagnostic_alert pdk_diagnostic_alert--success" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ @@ -4192,7 +4180,7 @@ function renderSkippedState(props) { iconWrap.appendChild(renderCircleSlashIcon24()); return E( "div", - { class: "pdk_diagnostic_alert pdk_diagnostic_alert--skipped" }, + { class: "card pdk_diagnostic_alert pdk_diagnostic_alert--skipped" }, [ iconWrap, E("div", { class: "pdk_diagnostic_alert__content" }, [ @@ -4245,35 +4233,39 @@ function renderRunAction({ // src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts function renderSystemInfo({ items }) { - return E("div", { class: "pdk_diagnostic-page__right-bar__system-info" }, [ - E( - "b", - { class: "pdk_diagnostic-page__right-bar__system-info__title" }, - _("System information") - ), - ...items.map((item) => { - const tagClass = [ - "pdk_diagnostic-page__right-bar__system-info__row__tag", - ...insertIf(item.tag?.kind === "warning", [ - "pdk_diagnostic-page__right-bar__system-info__row__tag--warning" - ]), - ...insertIf(item.tag?.kind === "success", [ - "pdk_diagnostic-page__right-bar__system-info__row__tag--success" - ]) - ].filter(Boolean).join(" "); - return E( - "div", - { class: "pdk_diagnostic-page__right-bar__system-info__row" }, - [ - E("b", {}, item.key), - E("div", {}, [ - E("span", {}, item.value), - E("span", { class: tagClass }, item?.tag?.label) + return E( + "div", + { class: "card pdk_diagnostic-page__right-bar__system-info" }, + [ + E( + "b", + { class: "pdk_diagnostic-page__right-bar__system-info__title" }, + _("System information") + ), + ...items.map((item) => { + const tagClass = [ + "pdk_diagnostic-page__right-bar__system-info__row__tag", + ...insertIf(item.tag?.kind === "warning", [ + "pdk_diagnostic-page__right-bar__system-info__row__tag--warning" + ]), + ...insertIf(item.tag?.kind === "success", [ + "pdk_diagnostic-page__right-bar__system-info__row__tag--success" ]) - ] - ); - }) - ]); + ].filter(Boolean).join(" "); + return E( + "div", + { class: "pdk_diagnostic-page__right-bar__system-info__row" }, + [ + E("b", {}, item.key), + E("div", {}, [ + E("span", {}, item.value), + E("span", { class: tagClass }, item?.tag?.label) + ]) + ] + ); + }) + ] + ); } // src/helpers/normalizeCompiledVersion.ts @@ -4291,6 +4283,7 @@ function renderWikiDisclaimer(kind) { }); iconWrap.appendChild(renderBookOpenTextIcon24()); const className = [ + "card", "pdk_diagnostic-page__right-bar__wiki", ...insertIf(kind === "error", [ "pdk_diagnostic-page__right-bar__wiki--error" @@ -4952,10 +4945,6 @@ var styles4 = ` } .pdk_diagnostic-page__right-bar__wiki { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - display: grid; grid-template-columns: auto; grid-row-gap: 10px; @@ -4977,21 +4966,12 @@ var styles4 = ` .pdk_diagnostic-page__right-bar__wiki__texts {} .pdk_diagnostic-page__right-bar__actions { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - display: grid; grid-template-columns: auto; grid-row-gap: 10px; - } .pdk_diagnostic-page__right-bar__system-info { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - display: grid; grid-template-columns: auto; grid-row-gap: 10px; @@ -5043,14 +5023,10 @@ var styles4 = ` } .pdk_diagnostic_alert { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - display: grid; grid-template-columns: 24px 1fr; grid-column-gap: 10px; align-items: center; - padding: 10px; } .pdk_diagnostic_alert--loading { @@ -5390,10 +5366,7 @@ async function runNetshiftCheck(button) { } async function runSingBoxMutation(component, button) { setActionLoading(button.loadingKey, true); - showToast( - _("Switching sing-box core, this may take a few minutes\u2026"), - "success" - ); + showToast(_("Switching sing-box core, this may take a few minutes\u2026"), "info"); try { const result = await NetShiftShellMethods.singBoxComponentAction( button.backendAction === "install_stable" ? "install_stable" : "install_extended" @@ -5423,7 +5396,7 @@ async function runNetshiftSelfUpdate(button) { setActionLoading(button.loadingKey, true); showToast( _("Updating NetShift, this may take a few minutes; the page will reload\u2026"), - "success", + "warning", 6e3 ); try { @@ -5490,7 +5463,7 @@ function renderComponentCard(card) { E("div", { class: "pdk_manager-page__component__status" }, [tag]) ); } - return E("div", { class: "pdk_manager-page__component" }, [ + return E("div", { class: "card pdk_manager-page__component" }, [ E("div", { class: "pdk_manager-page__component__header" }, headerChildren), E("div", { class: "pdk_manager-page__component__version" }, [ E( @@ -5616,10 +5589,6 @@ var styles5 = ` } .pdk_manager-page__component { - border: 2px var(--background-color-low, lightgray) solid; - border-radius: 4px; - padding: 10px; - min-width: 0; display: grid; grid-template-columns: 1fr; grid-row-gap: 10px; @@ -5708,6 +5677,38 @@ var ManagerTab = { // src/styles.ts var GlobalStyles = ` +/* + * NetShift design tokens (Stage 1 foundation \u2014 task-024). + * Each token layers over the LuCI theme var (with a hardcoded fallback) so + * themes still win. Reused by the custom tabs and the form redesigns + * (task-025/026). Keep these names stable. + */ +:root, +.cbi-map { + --ns-card-border: var(--background-color-low, lightgray); + --ns-card-border-width: 2px; + --ns-card-radius: 4px; + --ns-gap: 10px; + --ns-card-padding: var(--ns-gap); + --ns-success: var(--success-color-medium, #28a745); + --ns-warning: var(--warn-color-medium, #f0ad4e); + --ns-error: var(--error-color-medium, #dc3545); + --ns-info: var(--primary-color-high, #2196f3); +} + +/* + * Shared card primitive. Mirrors the Manager component card look + * (2px solid border, 4px radius, 10px padding, overflow-safe min-width:0). + * Defined BEFORE the per-tab styles so colored-border modifiers + * (e.g. .pdk_diagnostic_alert--warning) still win via source order. + */ +.card { + border: var(--ns-card-border-width) solid var(--ns-card-border); + border-radius: var(--ns-card-radius); + padding: var(--ns-card-padding); + min-width: 0; +} + ${DashboardTab.styles} ${DiagnosticTab.styles} ${ManagerTab.styles} @@ -5729,6 +5730,42 @@ ${PartialStyles} margin-bottom: -32px; } +/* + * Sections (connection) form \u2014 native CBI option-group tabs styled as a + * card (task-025). Reuses task-024's --ns-* tokens. The tab strip + * (ul.cbi-tabmenu) sits on top; each tab pane (.cbi-section-node-tabbed) + * reads as the card body. depends()-driven auto-hide of tabs is unaffected. + */ +#cbi-netshift-section .cbi-section-node-tabbed { + border: var(--ns-card-border-width) solid var(--ns-card-border); + border-radius: var(--ns-card-radius); + padding: var(--ns-card-padding); + min-width: 0; +} + +#cbi-netshift-section ul.cbi-tabmenu { + margin-bottom: var(--ns-gap); +} + +/* + * Settings form \u2014 native CBI option-group tabs styled as a card (task-026). + * Reuses task-024's --ns-* tokens and mirrors the #cbi-netshift-section + * pattern above. The tab strip (ul.cbi-tabmenu) sits on top; each tab pane + * (.cbi-section-node-tabbed) reads as the card body. depends()-driven + * auto-hide of tabs is unaffected. The existing + * #cbi-netshift-settings > h3 hide rule above stays valid. + */ +#cbi-netshift-settings .cbi-section-node-tabbed { + border: var(--ns-card-border-width) solid var(--ns-card-border); + border-radius: var(--ns-card-radius); + padding: var(--ns-card-padding); + min-width: 0; +} + +#cbi-netshift-settings ul.cbi-tabmenu { + margin-bottom: var(--ns-gap); +} + /* Centered class helper */ .centered { display: flex; @@ -5804,11 +5841,19 @@ ${PartialStyles} } .toast-success { - background-color: #28a745; + background-color: var(--ns-success, #28a745); } .toast-error { - background-color: #dc3545; + background-color: var(--ns-error, #dc3545); +} + +.toast-warning { + background-color: var(--ns-warning, #f0ad4e); +} + +.toast-info { + background-color: var(--ns-info, #2196f3); } .toast.visible { diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js index ca791b7f..59e3bb0a 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js @@ -32,6 +32,21 @@ const EntryPoint = { // Enable tab views netshiftMap.tabbed = true; + // Dashboard tab (first / landing tab) + const dashboardSection = netshiftMap.section( + form.TypedSection, + "dashboard", + _("Dashboard"), + ); + dashboardSection.anonymous = true; + dashboardSection.addremove = false; + dashboardSection.cfgsections = function () { + return ["dashboard"]; + }; + + // Render dashboard content + dashboard.createDashboardContent(dashboardSection); + // Sections tab const sectionsSection = netshiftMap.section( form.TypedSection, @@ -61,21 +76,6 @@ const EntryPoint = { // Render settings content settings.createSettingsContent(settingsSection); - // Diagnostic tab - const diagnosticSection = netshiftMap.section( - form.TypedSection, - "diagnostic", - _("Diagnostics"), - ); - diagnosticSection.anonymous = true; - diagnosticSection.addremove = false; - diagnosticSection.cfgsections = function () { - return ["diagnostic"]; - }; - - // Render diagnostic content - diagnostic.createDiagnosticContent(diagnosticSection); - // Component Manager tab const managerSection = netshiftMap.section( form.TypedSection, @@ -91,20 +91,20 @@ const EntryPoint = { // Render Component Manager content manager.createManagerContent(managerSection); - // Dashboard tab - const dashboardSection = netshiftMap.section( + // Diagnostic tab + const diagnosticSection = netshiftMap.section( form.TypedSection, - "dashboard", - _("Dashboard"), + "diagnostic", + _("Diagnostics"), ); - dashboardSection.anonymous = true; - dashboardSection.addremove = false; - dashboardSection.cfgsections = function () { - return ["dashboard"]; + diagnosticSection.anonymous = true; + diagnosticSection.addremove = false; + diagnosticSection.cfgsections = function () { + return ["diagnostic"]; }; - // Render dashboard content - dashboard.createDashboardContent(dashboardSection); + // Render diagnostic content + diagnostic.createDiagnosticContent(diagnosticSection); // Inject core service main.coreService(); diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index 232da717..8d2fa2f3 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -6,7 +6,34 @@ "require view.netshift.main as main"; function createSectionContent(section) { - let o = section.option( + // Group the 36 connection options into 4 native CBI option-group tabs. + // HARD RULE: once a section has tab(), every option MUST be added via + // taboption() — any leftover section.option(...) renders nothing. + // depends() works across tabs; a tab whose options are all depends-hidden + // auto-hides from the strip (desired, e.g. Subscription for proxy/url). + section.tab( + "connection", + _("Connection"), + _("Connection type, transport and DNS resolver for this section"), + ); + section.tab( + "subscription", + _("Subscription"), + _("Subscription feeds, server filters and URLTest tuning"), + ); + section.tab( + "routing", + _("Routing"), + _("Domain and subnet lists that decide which traffic uses this section"), + ); + section.tab( + "advanced", + _("Advanced"), + _("Mixed proxy and DNS resolution tuning"), + ); + + let o = section.taboption( + "connection", form.ListValue, "connection_type", _("Connection Type"), @@ -17,7 +44,8 @@ function createSectionContent(section) { o.value("block", "Block"); o.value("exclusion", "Exclusion"); - o = section.option( + o = section.taboption( + "connection", form.ListValue, "proxy_config_type", _("Configuration Type"), @@ -31,7 +59,8 @@ function createSectionContent(section) { o.default = "url"; o.depends("connection_type", "proxy"); - o = section.option( + o = section.taboption( + "connection", form.TextValue, "proxy_string", _("Proxy Configuration URL"), @@ -62,7 +91,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "connection", form.TextValue, "outbound_json", _("Outbound Configuration"), @@ -85,7 +115,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "subscription", form.DynamicList, "subscription_url", _("Subscription URLs"), @@ -110,7 +141,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "subscription", form.Flag, "subscription_insecure", _("Allow insecure TLS for subscription fetch"), @@ -124,7 +156,8 @@ function createSectionContent(section) { o.rmempty = false; o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); - o = section.option( + o = section.taboption( + "subscription", form.ListValue, "subscription_update_interval", _("Subscription Update Interval"), @@ -139,19 +172,21 @@ function createSectionContent(section) { o.default = "1h"; o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); - o = section.option( + o = section.taboption( + "subscription", form.Flag, "subscription_group_by_countries", - _("Группировать по странам"), + _("Group by countries"), _( - "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы", + "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag", ), ); o.default = "0"; o.rmempty = false; o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); - o = section.option( + o = section.taboption( + "subscription", form.DynamicList, "subscription_filter_include_keywords", _("Include servers by keyword"), @@ -162,7 +197,8 @@ function createSectionContent(section) { o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o.rmempty = true; - o = section.option( + o = section.taboption( + "subscription", form.DynamicList, "subscription_filter_exclude_keywords", _("Exclude servers by keyword"), @@ -173,7 +209,8 @@ function createSectionContent(section) { o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); o.rmempty = true; - o = section.option( + o = section.taboption( + "connection", form.DynamicList, "selector_proxy_links", _("Selector Proxy Links"), @@ -198,7 +235,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "subscription", form.DynamicList, "urltest_proxy_links", _("URLTest Proxy Links"), @@ -223,7 +261,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "subscription", form.ListValue, "urltest_check_interval", _("URLTest Check Interval"), @@ -237,7 +276,8 @@ function createSectionContent(section) { o.depends({ connection_type: "proxy", proxy_config_type: "urltest" }); o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); - o = section.option( + o = section.taboption( + "subscription", form.Value, "urltest_tolerance", _("URLTest Tolerance"), @@ -269,7 +309,8 @@ function createSectionContent(section) { return _("Must be a number in the range of 50 - 1000"); }; - o = section.option( + o = section.taboption( + "subscription", form.Value, "urltest_testing_url", _("URLTest Testing URL"), @@ -307,7 +348,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "connection", form.Flag, "enable_udp_over_tcp", _("UDP over TCP"), @@ -317,7 +359,8 @@ function createSectionContent(section) { o.depends("connection_type", "proxy"); o.rmempty = false; - o = section.option( + o = section.taboption( + "connection", form.Flag, "global_proxy", _("Global Proxy"), @@ -334,7 +377,8 @@ function createSectionContent(section) { o.default = "0"; o.rmempty = false; - o = section.option( + o = section.taboption( + "connection", widgets.DeviceSelect, "interface", _("Network Interface"), @@ -380,7 +424,8 @@ function createSectionContent(section) { return !isWireless; }; - o = section.option( + o = section.taboption( + "connection", form.Flag, "domain_resolver_enabled", _("Domain Resolver"), @@ -390,7 +435,8 @@ function createSectionContent(section) { o.rmempty = false; o.depends("connection_type", "vpn"); - o = section.option( + o = section.taboption( + "connection", form.ListValue, "domain_resolver_dns_type", _("DNS Protocol Type"), @@ -403,7 +449,8 @@ function createSectionContent(section) { o.rmempty = false; o.depends("domain_resolver_enabled", "1"); - o = section.option( + o = section.taboption( + "connection", form.Value, "domain_resolver_dns_server", _("DNS Server"), @@ -425,7 +472,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "routing", form.DynamicList, "community_lists", _("Community Lists"), @@ -513,11 +561,18 @@ function createSectionContent(section) { } }; - o = section.option( + // --- Custom domains group (mode selector + the matching input below) --- + // Three UCI keys kept (user_domain_list_type, user_domains, + // user_domains_text); depends() shows only the input for the chosen mode, + // so the trio reads as one "list-or-text" control. + o = section.taboption( + "routing", form.ListValue, "user_domain_list_type", - _("User Domain List Type"), - _("Select the list type for adding custom domains"), + _("Custom domains"), + _( + "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", + ), ); o.value("disabled", _("Disabled")); o.value("dynamic", _("Dynamic List")); @@ -525,7 +580,8 @@ function createSectionContent(section) { o.default = "disabled"; o.rmempty = false; - o = section.option( + o = section.taboption( + "routing", form.DynamicList, "user_domains", _("User Domains"), @@ -551,7 +607,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "routing", form.TextValue, "user_domains_text", _("User Domains List"), @@ -593,11 +650,17 @@ function createSectionContent(section) { return true; }; - o = section.option( + // --- Custom subnets group (mode selector + the matching input below) --- + // Same pattern as the domains group; keeps user_subnet_list_type, + // user_subnets and user_subnets_text as separate UCI keys. + o = section.taboption( + "routing", form.ListValue, "user_subnet_list_type", - _("User Subnet List Type"), - _("Select the list type for adding custom subnets"), + _("Custom subnets"), + _( + "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", + ), ); o.value("disabled", _("Disabled")); o.value("dynamic", _("Dynamic List")); @@ -605,7 +668,8 @@ function createSectionContent(section) { o.default = "disabled"; o.rmempty = false; - o = section.option( + o = section.taboption( + "routing", form.DynamicList, "user_subnets", _("User Subnets"), @@ -631,7 +695,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "routing", form.TextValue, "user_subnets_text", _("User Subnets List"), @@ -672,7 +737,8 @@ function createSectionContent(section) { return true; }; - o = section.option( + o = section.taboption( + "routing", form.DynamicList, "local_domain_lists", _("Local Domain Lists"), @@ -695,7 +761,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "routing", form.DynamicList, "local_subnet_lists", _("Local Subnet Lists"), @@ -718,7 +785,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "routing", form.DynamicList, "remote_domain_lists", _("Remote Domain Lists"), @@ -741,7 +809,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "routing", form.DynamicList, "remote_subnet_lists", _("Remote Subnet Lists"), @@ -764,7 +833,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "routing", form.DynamicList, "fully_routed_ips", _("Fully Routed IPs"), @@ -791,7 +861,8 @@ function createSectionContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "advanced", form.Flag, "mixed_proxy_enabled", _("Enable Mixed Proxy"), @@ -804,7 +875,8 @@ function createSectionContent(section) { o.depends("connection_type", "proxy"); o.depends("connection_type", "vpn"); - o = section.option( + o = section.taboption( + "advanced", form.Value, "mixed_proxy_port", _("Mixed Proxy Port"), @@ -819,7 +891,8 @@ function createSectionContent(section) { o.rmempty = true; o.depends("mixed_proxy_enabled", "1"); - o = section.option( + o = section.taboption( + "advanced", form.Flag, "resolve_real_ip_for_routing", _("Resolve real IP for routing"), diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js index 4a262874..0c40f085 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js @@ -6,7 +6,40 @@ "require view.netshift.main as main"; function createSettingsContent(section) { - let o = section.option( + // Group the 27 settings options into 5 native CBI option-group tabs. + // HARD RULE: once a section has tab(), every option MUST be added via + // taboption() — any leftover section.option(...) renders nothing. + // depends() works across tabs; a tab whose options are all depends-hidden + // auto-hides from the strip. + section.tab( + "dns", + _("DNS"), + _("Upstream and bootstrap DNS resolvers, and optional DNS-over-proxy"), + ); + section.tab( + "network", + _("Network"), + _("Source and output interfaces, and Bad WAN interface monitoring"), + ); + section.tab( + "lists", + _("Lists & Updates"), + _("List update schedule, download routing, and routing exclusions"), + ); + section.tab( + "yacd", + _("Dashboard"), + _("YACD web dashboard access and remote-access protection"), + ); + section.tab( + "advanced", + _("Advanced"), + _("Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT."), + ); + + // --- DNS tab --- + let o = section.taboption( + "dns", form.ListValue, "dns_type", _("DNS Protocol Type"), @@ -18,7 +51,8 @@ function createSettingsContent(section) { o.default = "udp"; o.rmempty = false; - o = section.option( + o = section.taboption( + "dns", form.Value, "dns_server", _("DNS Server"), @@ -39,7 +73,8 @@ function createSettingsContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "dns", form.Value, "bootstrap_dns_server", _("Bootstrap DNS server"), @@ -62,7 +97,8 @@ function createSettingsContent(section) { return validation.message; }; - o = section.option( + o = section.taboption( + "dns", form.Flag, "dns_via_outbound", _("Route main DNS through proxy/VPN"), @@ -73,7 +109,8 @@ function createSettingsContent(section) { o.default = "0"; o.rmempty = false; - o = section.option( + o = section.taboption( + "dns", form.ListValue, "dns_outbound_section", _("DNS outbound section"), @@ -107,7 +144,8 @@ function createSettingsContent(section) { return Promise.resolve(); }; - o = section.option( + o = section.taboption( + "dns", form.Value, "dns_rewrite_ttl", _("DNS Rewrite TTL"), @@ -128,7 +166,9 @@ function createSettingsContent(section) { return true; }; - o = section.option( + // --- Network tab --- + o = section.taboption( + "network", widgets.DeviceSelect, "source_network_interfaces", _("Source Network Interface"), @@ -165,7 +205,8 @@ function createSettingsContent(section) { return !isWireless; }; - o = section.option( + o = section.taboption( + "network", form.Flag, "enable_output_network_interface", _("Enable Output Network Interface"), @@ -174,7 +215,8 @@ function createSettingsContent(section) { o.default = "0"; o.rmempty = false; - o = section.option( + o = section.taboption( + "network", widgets.DeviceSelect, "output_network_interface", _("Output Network Interface"), @@ -226,7 +268,8 @@ function createSettingsContent(section) { return !isWireless; }; - o = section.option( + o = section.taboption( + "network", form.Flag, "enable_badwan_interface_monitoring", _("Interface Monitoring"), @@ -235,7 +278,8 @@ function createSettingsContent(section) { o.default = "0"; o.rmempty = false; - o = section.option( + o = section.taboption( + "network", widgets.NetworkSelect, "badwan_monitored_interfaces", _("Monitored Interfaces"), @@ -258,7 +302,8 @@ function createSettingsContent(section) { return true; }; - o = section.option( + o = section.taboption( + "network", form.Value, "badwan_reload_delay", _("Interface Monitoring Delay"), @@ -274,50 +319,9 @@ function createSettingsContent(section) { return true; }; - o = section.option( - form.Flag, - "enable_yacd", - _("Enable YACD"), - `<a href="${main.getClashUIUrl()}" target="_blank">${main.getClashUIUrl()}</a>`, - ); - o.default = "0"; - o.rmempty = false; - - o = section.option( - form.Flag, - "enable_yacd_wan_access", - _("Enable YACD WAN Access"), - _( - "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", - ), - ); - o.depends("enable_yacd", "1"); - o.default = "0"; - o.rmempty = false; - - o = section.option( - form.Value, - "yacd_secret_key", - _("YACD Secret Key"), - _( - "Secret key for authenticating remote access to YACD when WAN access is enabled.", - ), - ); - o.depends("enable_yacd_wan_access", "1"); - o.rmempty = false; - - o = section.option( - form.Flag, - "disable_quic", - _("Disable QUIC"), - _( - "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", - ), - ); - o.default = "0"; - o.rmempty = false; - - o = section.option( + // --- Lists & Updates tab --- + o = section.taboption( + "lists", form.ListValue, "update_interval", _("List Update Frequency"), @@ -329,7 +333,8 @@ function createSettingsContent(section) { o.default = "1d"; o.rmempty = false; - o = section.option( + o = section.taboption( + "lists", form.Flag, "download_lists_via_proxy", _("Download Lists via Proxy/VPN"), @@ -338,7 +343,8 @@ function createSettingsContent(section) { o.default = "0"; o.rmempty = false; - o = section.option( + o = section.taboption( + "lists", form.ListValue, "download_lists_via_proxy_section", _("Download Lists via specific proxy section"), @@ -371,7 +377,81 @@ function createSettingsContent(section) { return Promise.resolve(); }; - o = section.option( + o = section.taboption( + "lists", + form.DynamicList, + "routing_excluded_ips", + _("Routing Excluded IPs"), + _("Specify a local IP address to be excluded from routing"), + ); + o.placeholder = "IP"; + o.rmempty = true; + o.validate = function (section_id, value) { + // Optional + if (!value || value.length === 0) { + return true; + } + + const validation = main.validateIP(value); + + if (validation.valid) { + return true; + } + + return validation.message; + }; + + // --- Dashboard / YACD tab --- + o = section.taboption( + "yacd", + form.Flag, + "enable_yacd", + _("Enable YACD"), + `<a href="${main.getClashUIUrl()}" target="_blank">${main.getClashUIUrl()}</a>`, + ); + o.default = "0"; + o.rmempty = false; + + o = section.taboption( + "yacd", + form.Flag, + "enable_yacd_wan_access", + _("Enable YACD WAN Access"), + _( + "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.", + ), + ); + o.depends("enable_yacd", "1"); + o.default = "0"; + o.rmempty = false; + + o = section.taboption( + "yacd", + form.Value, + "yacd_secret_key", + _("YACD Secret Key"), + _( + "Secret key for authenticating remote access to YACD when WAN access is enabled.", + ), + ); + o.depends("enable_yacd_wan_access", "1"); + o.rmempty = false; + + // --- Advanced tab --- + o = section.taboption( + "advanced", + form.Flag, + "disable_quic", + _("Disable QUIC"), + _( + "Disable the QUIC protocol to improve compatibility or fix issues with video streaming", + ), + ); + o.default = "0"; + o.rmempty = false; + + o = section.taboption( + "advanced", form.Flag, "dont_touch_dhcp", _("Dont Touch My DHCP!"), @@ -380,7 +460,44 @@ function createSettingsContent(section) { o.default = "0"; o.rmempty = false; - o = section.option( + o = section.taboption( + "advanced", + form.Flag, + "exclude_ntp", + _("Exclude NTP"), + _( + "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", + ), + ); + o.default = "0"; + o.rmempty = false; + + o = section.taboption( + "advanced", + form.Flag, + "block_doh", + _("Block DoH Servers"), + _( + "Block direct connections to known public DoH servers (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex) so apps cannot bypass router DNS filtering.", + ), + ); + o.default = "0"; + o.rmempty = false; + + o = section.taboption( + "advanced", + form.Flag, + "enable_ipv6", + _("Enable IPv6 Support"), + _("Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support.") + + " " + + _("Use this only when the router has working IPv6 connectivity."), + ); + o.default = "0"; + o.rmempty = false; + + o = section.taboption( + "advanced", form.ListValue, "config_path", _("Config File Path"), @@ -393,7 +510,8 @@ function createSettingsContent(section) { o.default = "/etc/sing-box/config.json"; o.rmempty = false; - o = section.option( + o = section.taboption( + "advanced", form.Value, "cache_path", _("Cache File Path"), @@ -429,7 +547,8 @@ function createSettingsContent(section) { return true; }; - o = section.option( + o = section.taboption( + "advanced", form.ListValue, "log_level", _("Log Level"), @@ -444,72 +563,6 @@ function createSettingsContent(section) { o.value("panic", "Panic"); o.default = "warn"; o.rmempty = false; - - o = section.option( - form.Flag, - "exclude_ntp", - _("Exclude NTP"), - _( - "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN", - ), - ); - o.default = "0"; - o.rmempty = false; - - o = section.option( - form.Flag, - "block_doh", - _("Block DoH Servers"), - _("Block direct connections to known public DNS-over-HTTPS (DoH) servers.") + - " " + - _( - "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS.", - ) + - " " + - _( - "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers.", - ) + - " " + - _( - "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT.", - ), - ); - o.default = "0"; - o.rmempty = false; - - o = section.option( - form.Flag, - "enable_ipv6", - _("Enable IPv6 Support"), - _("Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support.") + - " " + - _("Use this only when the router has working IPv6 connectivity."), - ); - o.default = "0"; - o.rmempty = false; - - o = section.option( - form.DynamicList, - "routing_excluded_ips", - _("Routing Excluded IPs"), - _("Specify a local IP address to be excluded from routing"), - ); - o.placeholder = "IP"; - o.rmempty = true; - o.validate = function (section_id, value) { - // Optional - if (!value || value.length === 0) { - return true; - } - - const validation = main.validateIP(value); - - if (validation.valid) { - return true; - } - - return validation.message; - }; } const EntryPoint = { diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 4377ec3a..c2068c4b 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 10:23+0300\n" -"PO-Revision-Date: 2026-06-07 10:23+0300\n" +"POT-Creation-Date: 2026-06-07 11:56+0300\n" +"PO-Revision-Date: 2026-06-07 11:56+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -35,11 +35,17 @@ msgstr "Активные соединения" msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." msgstr "Добавьте один или несколько URL подписок для получения конфигураций прокси. Все источники загружаются и объединяются." +msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" +msgstr "Добавьте свои домены: выберите Динамический список (по одному в строке) или Текстовый список (свободный ввод), либо Отключено, чтобы пропустить" + +msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" +msgstr "Добавьте свои подсети или IP: выберите Динамический список (по одному в строке) или Текстовый список (свободный ввод), либо Отключено, чтобы пропустить" + msgid "Additional marking rules found" msgstr "Найдены дополнительные правила маркировки" -msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." -msgstr "Затрагивает публичные DoH-серверы Cloudflare, Google, Quad9, OpenDNS, AdGuard и Yandex." +msgid "Advanced" +msgstr "Дополнительно" msgid "Allow insecure TLS for subscription fetch" msgstr "Разрешить небезопасный TLS при загрузке подписки" @@ -59,8 +65,8 @@ msgstr "Необходимо указать хотя бы одну действ msgid "Available actions" msgstr "Доступные действия" -msgid "Block direct connections to known public DNS-over-HTTPS (DoH) servers." -msgstr "Блокирует прямые подключения к известным публичным серверам DNS-over-HTTPS (DoH)." +msgid "Block direct connections to known public DoH servers (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex) so apps cannot bypass router DNS filtering." +msgstr "Блокировать прямые подключения к известным публичным DoH-серверам (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex), чтобы приложения не могли обойти DNS-фильтрацию роутера." msgid "Block DoH Servers" msgstr "Блокировать DoH-серверы" @@ -122,9 +128,15 @@ msgstr "Конфигурация службы NetShift" msgid "Configuration Type" msgstr "Тип конфигурации" +msgid "Connection" +msgstr "Подключение" + msgid "Connection Type" msgstr "Тип подключения" +msgid "Connection type, transport and DNS resolver for this section" +msgstr "Тип подключения, транспорт и DNS-резолвер для этой секции" + msgid "Connection URL" msgstr "URL подключения" @@ -140,6 +152,12 @@ msgstr "Истекло время ожидания переключения яд msgid "Currently unavailable" msgstr "Временно недоступно" +msgid "Custom domains" +msgstr "Свои домены" + +msgid "Custom subnets" +msgstr "Свои подсети" + msgid "Dashboard" msgstr "Дашборд" @@ -176,6 +194,9 @@ msgstr "Отключено" msgid "Disables TLS certificate verification when downloading the subscription." msgstr "Отключает проверку TLS-сертификата при загрузке подписки." +msgid "DNS" +msgstr "DNS" + msgid "DNS on router" msgstr "DNS на роутере" @@ -203,6 +224,9 @@ msgstr "Адрес DNS-сервера не может быть пустым" msgid "Do not panic, everything can be fixed, just..." msgstr "Не паникуйте, всё можно исправить, просто..." +msgid "Domain and subnet lists that decide which traffic uses this section" +msgstr "Списки доменов и подсетей, определяющие, какой трафик идёт через эту секцию" + msgid "Domain Resolver" msgstr "Резолвер доменов" @@ -332,6 +356,12 @@ msgstr "Глобальная проверка" msgid "Global Proxy" msgstr "Глобальный прокси" +msgid "Group by countries" +msgstr "Группировать по странам" + +msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" +msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" + msgid "How often to automatically update the subscription" msgstr "Как часто автоматически обновлять подписку" @@ -533,6 +563,12 @@ msgstr "Последняя версия неизвестна" msgid "List Update Frequency" msgstr "Частота обновления списков" +msgid "List update schedule, download routing, and routing exclusions" +msgstr "Расписание обновления списков, маршрутизация загрузок и исключения из маршрутизации" + +msgid "Lists & Updates" +msgstr "Списки и обновления" + msgid "Local Domain Lists" msgstr "Локальные списки доменов" @@ -551,6 +587,9 @@ msgstr "Основной DNS через outbound" msgid "Memory Usage" msgstr "Использование памяти" +msgid "Mixed proxy and DNS resolution tuning" +msgstr "Настройка смешанного прокси и разрешения DNS" + msgid "Mixed Proxy Port" msgstr "Порт смешанного прокси" @@ -572,6 +611,9 @@ msgstr "NetShift обновлён, версия:" msgid "NetShift will not modify your DHCP configuration" msgstr "NetShift не будет изменять вашу конфигурацию DHCP" +msgid "Network" +msgstr "Сеть" + msgid "Network Interface" msgstr "Сетевой интерфейс" @@ -590,9 +632,6 @@ msgstr "Не отвечает" msgid "Not running" msgstr "Не запущено" -msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." -msgstr "Примечание: если тип вышестоящего DNS установлен в «DoH», включайте это только после переключения на UDP или DoT." - msgid "Only one section can be global at a time." msgstr "Только одна секция может быть глобальной одновременно." @@ -626,6 +665,9 @@ msgstr "Путь должен заканчиваться на cache.db" msgid "Pending" msgstr "Ожидает запуска" +msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT." +msgstr "Переключатели протоколов, пути к файлам и журналирование. Включайте блокировку DoH только после переключения вышестоящего DNS на UDP или DoT." + msgid "Proxy Configuration URL" msgstr "URL конфигурации прокси" @@ -662,6 +704,9 @@ msgstr "DNS роутера не проходит через sing-box" msgid "Router DNS is routed through sing-box" msgstr "DNS роутера проходит через sing-box" +msgid "Routing" +msgstr "Маршрутизация" + msgid "Routing Excluded IPs" msgstr "Исключённые из маршрутизации IP-адреса" @@ -722,12 +767,6 @@ msgstr "Выберите путь к файлу конфигурации sing-bo msgid "Select the DNS protocol type for the domain resolver" msgstr "Выберите тип протокола DNS для резолвера доменов" -msgid "Select the list type for adding custom domains" -msgstr "Выберите тип списка для добавления пользовательских доменов" - -msgid "Select the list type for adding custom subnets" -msgstr "Выберите тип списка для добавления пользовательских подсетей" - msgid "Select the log level for sing-box" msgstr "Выберите уровень логов для sing-box" @@ -785,6 +824,9 @@ msgstr "Сервис sing-box существует" msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "Версия Sing-box совместима (новее 1.12.4)" +msgid "Source and output interfaces, and Bad WAN interface monitoring" +msgstr "Входящий и исходящий интерфейсы, а также мониторинг интерфейсов Bad WAN" + msgid "Source Network Interface" msgstr "Сетевой интерфейс источника" @@ -812,6 +854,9 @@ msgstr "Остановить NetShift" msgid "Subscription" msgstr "Подписка" +msgid "Subscription feeds, server filters and URLTest tuning" +msgstr "Источники подписок, фильтры серверов и настройка URLTest" + msgid "Subscription Update Interval" msgstr "Интервал обновления подписки" @@ -860,9 +905,6 @@ msgstr "URL-адрес, используемый для проверки под msgid "This is a security trade-off: an attacker could intercept the fetch." msgstr "Это компромисс в безопасности: злоумышленник может перехватить загрузку." -msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." -msgstr "Это не позволяет приложениям обходить DNS-фильтрацию роутера за счёт использования собственного шифрованного DNS." - msgid "Time in seconds for DNS record caching (default: 60)" msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)" @@ -908,6 +950,9 @@ msgstr "Обновление NetShift, это может занять неско msgid "Uplink" msgstr "Исходящий" +msgid "Upstream and bootstrap DNS resolvers, and optional DNS-over-proxy" +msgstr "Вышестоящий и начальный (bootstrap) DNS-резолверы и опциональный DNS через прокси" + msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://" @@ -938,18 +983,12 @@ msgstr "Используйте это только если на роутере msgid "Use with Exclusion sections to route specific domains directly." msgstr "Используйте вместе с секциями исключений для прямой маршрутизации определённых доменов." -msgid "User Domain List Type" -msgstr "Тип пользовательского списка доменов" - msgid "User Domains" msgstr "Пользовательские домены" msgid "User Domains List" msgstr "Список пользовательских доменов" -msgid "User Subnet List Type" -msgstr "Тип пользовательского списка подсетей" - msgid "User Subnets" msgstr "Пользовательские подсети" @@ -989,11 +1028,8 @@ msgstr "Какая секция прокси/VPN обслуживает DNS. О msgid "YACD Secret Key" msgstr "Секретный ключ YACD" +msgid "YACD web dashboard access and remote-access protection" +msgstr "Доступ к веб-панели YACD и защита удалённого доступа" + msgid "You can select Output Network Interface, by default autodetect" msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически." - -msgid "Группировать по странам" -msgstr "Группировать по странам" - -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" -msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index 3c8bcd91..72d86fbc 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 07:23+0300\n" -"PO-Revision-Date: 2026-06-07 07:23+0300\n" +"POT-Creation-Date: 2026-06-07 08:56+0300\n" +"PO-Revision-Date: 2026-06-07 08:56+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -36,35 +36,44 @@ msgstr "" msgid "Active Connections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:92 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:123 msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573 +msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661 +msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runNftCheck.ts:99 msgid "Additional marking rules found" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:469 -msgid "Affects Cloudflare, Google, Quad9, OpenDNS, AdGuard, and Yandex public DoH servers." +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:31 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:36 +msgid "Advanced" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:116 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148 msgid "Allow insecure TLS for subscription fetch" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:290 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:420 msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:314 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:356 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:576 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:633 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:657 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:722 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -72,11 +81,11 @@ msgstr "" msgid "Available actions" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:463 -msgid "Block direct connections to known public DNS-over-HTTPS (DoH) servers." +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:480 +msgid "Block direct connections to known public DoH servers (Cloudflare, Google, Quad9, OpenDNS, AdGuard, Yandex) so apps cannot bypass router DNS filtering." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:462 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:479 msgid "Block DoH Servers" msgstr "" @@ -84,7 +93,7 @@ msgstr "" msgid "Bootsrap DNS" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80 msgid "Bootstrap DNS server" msgstr "" @@ -96,11 +105,11 @@ msgstr "" msgid "Browser is using FakeIP correctly" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:399 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:517 msgid "Cache File Path" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:413 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:531 msgid "Cache file path cannot be empty" msgstr "" @@ -145,7 +154,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479 msgid "Community Lists" msgstr "" @@ -153,7 +162,7 @@ msgstr "" msgid "Component Manager" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:386 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:503 msgid "Config File Path" msgstr "" @@ -161,15 +170,23 @@ msgstr "" msgid "Configuration for NetShift service" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:23 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:51 msgid "Configuration Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:12 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:16 +msgid "Connection" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:39 msgid "Connection Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:26 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:17 +msgid "Connection type, transport and DNS resolver for this section" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:54 msgid "Connection URL" msgstr "" @@ -190,19 +207,28 @@ msgstr "" msgid "Currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:98 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:572 +msgid "Custom domains" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:660 +msgid "Custom subnets" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:39 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:31 msgid "Dashboard" msgstr "" -#: src/netshift/tabs/dashboard/partials/renderSections.ts:19 +#: src/netshift/tabs/dashboard/partials/renderSections.ts:20 msgid "Dashboard currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:265 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:310 msgid "Delay in milliseconds before reloading NetShift after interface UP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:272 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:317 msgid "Delay value cannot be empty" msgstr "" @@ -214,7 +240,7 @@ msgstr "" msgid "DHCP has DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:68 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:98 msgid "Diagnostics" msgstr "" @@ -222,52 +248,56 @@ msgstr "" msgid "Disable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:312 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:445 msgid "Disable QUIC" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:313 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:446 msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:522 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665 msgid "Disabled" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:117 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149 msgid "Disables TLS certificate verification when downloading the subscription." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 +msgid "DNS" +msgstr "" + #: src/netshift/tabs/diagnostic/checks/runDnsCheck.ts:88 msgid "DNS on router" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:79 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:116 msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:15 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:445 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:16 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:396 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:12 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:442 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 msgid "DNS Protocol Type" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:113 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:151 msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:409 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:24 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:456 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58 msgid "DNS Server" msgstr "" @@ -275,15 +305,19 @@ msgstr "" msgid "DNS server address cannot be empty" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:27 msgid "Do not panic, everything can be fixed, just..." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:386 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:27 +msgid "Domain and subnet lists that decide which traffic uses this section" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431 msgid "Domain Resolver" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:377 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:457 msgid "Dont Touch My DHCP!" msgstr "" @@ -296,25 +330,25 @@ msgstr "" msgid "Download" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:335 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:340 msgid "Download Lists via Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:344 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:350 msgid "Download Lists via specific proxy section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:336 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:345 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:341 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:351 msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:523 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:578 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:666 msgid "Dynamic List" msgstr "" @@ -322,107 +356,107 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:387 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:826 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:899 msgid "Enable DNS resolve to get real IP when routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:483 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:491 msgid "Enable IPv6 Support" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:484 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:492 msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:797 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:868 msgid "Enable Mixed Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:171 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:212 msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:798 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:869 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:280 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:409 msgid "Enable YACD" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:289 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:419 msgid "Enable YACD WAN Access" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:69 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:99 msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:558 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:615 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:233 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:272 msgid "Every 1 minute" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:137 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:170 msgid "Every 12 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:135 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 msgid "Every 3 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:234 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:273 msgid "Every 3 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:133 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 msgid "Every 30 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:271 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:235 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:274 msgid "Every 5 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:136 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169 msgid "Every 6 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:138 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:171 msgid "Every day" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:134 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:167 msgid "Every hour" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:451 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:467 msgid "Exclude NTP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:452 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:468 msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204 msgid "Exclude servers by keyword" msgstr "" @@ -439,10 +473,10 @@ msgstr "" #: src/netshift/tabs/manager/initController.ts:122 #: src/netshift/tabs/manager/initController.ts:132 #: src/netshift/tabs/manager/initController.ts:166 -#: src/netshift/tabs/manager/initController.ts:197 -#: src/netshift/tabs/manager/initController.ts:201 -#: src/netshift/tabs/manager/initController.ts:234 -#: src/netshift/tabs/manager/initController.ts:238 +#: src/netshift/tabs/manager/initController.ts:194 +#: src/netshift/tabs/manager/initController.ts:198 +#: src/netshift/tabs/manager/initController.ts:231 +#: src/netshift/tabs/manager/initController.ts:235 msgid "Failed to execute!" msgstr "" @@ -453,7 +487,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:770 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:840 msgid "Fully Routed IPs" msgstr "" @@ -465,11 +499,19 @@ msgstr "" msgid "Global check" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:323 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:366 msgid "Global Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:131 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 +msgid "Group by countries" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180 +msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164 msgid "How often to automatically update the subscription" msgstr "" @@ -477,7 +519,7 @@ msgstr "" msgid "HTTP error" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:157 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:192 msgid "Include servers by keyword" msgstr "" @@ -491,15 +533,15 @@ msgstr "" msgid "Installed version is newer than release" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:232 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:275 msgid "Interface Monitoring" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:264 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:309 msgid "Interface Monitoring Delay" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:233 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:276 msgid "Interface monitoring for Bad WAN" msgstr "" @@ -723,7 +765,7 @@ msgstr "" msgid "Issues detected" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:158 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:193 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" @@ -740,19 +782,27 @@ msgstr "" msgid "Latest version is unknown" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:323 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:327 msgid "List Update Frequency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:678 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:27 +msgid "List update schedule, download routing, and routing exclusions" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:26 +msgid "Lists & Updates" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768 msgid "Local Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:435 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:554 msgid "Log Level" msgstr "" @@ -768,15 +818,19 @@ msgstr "" msgid "Memory Usage" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:810 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:32 +msgid "Mixed proxy and DNS resolution tuning" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:882 msgid "Mixed Proxy Port" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:241 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:285 msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -788,15 +842,19 @@ msgstr "" msgid "NetShift Settings" msgstr "" -#: src/netshift/tabs/manager/initController.ts:226 +#: src/netshift/tabs/manager/initController.ts:223 msgid "NetShift updated, version:" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:378 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:458 msgid "NetShift will not modify your DHCP configuration" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:340 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:21 +msgid "Network" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 msgid "Network Interface" msgstr "" @@ -829,11 +887,7 @@ msgstr "" msgid "Not running" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:473 -msgid "Note: if your upstream DNS type is set to 'DoH', enable this only after switching to UDP or DoT." -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:375 msgid "Only one section can be global at a time." msgstr "" @@ -841,11 +895,11 @@ msgstr "" msgid "Operation timed out" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:30 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:58 msgid "Outbound Config" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:68 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:98 msgid "Outbound Configuration" msgstr "" @@ -854,7 +908,7 @@ msgstr "" msgid "Outdated" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:180 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:222 msgid "Output Network Interface" msgstr "" @@ -862,15 +916,15 @@ msgstr "" msgid "Path cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:417 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:535 msgid "Path must be absolute (start with /)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:426 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:544 msgid "Path must contain at least one directory (like /tmp/cache.db)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:421 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:539 msgid "Path must end with cache.db" msgstr "" @@ -882,7 +936,11 @@ msgstr "" msgid "Pending" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:37 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:37 +msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:66 msgid "Proxy Configuration URL" msgstr "" @@ -894,19 +952,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:724 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:792 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:747 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:816 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:825 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:898 msgid "Resolve real IP for routing" msgstr "" @@ -914,11 +972,11 @@ msgstr "" msgid "Restart NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:367 msgid "Route all unmatched traffic through this section's outbound." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:68 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:104 msgid "Route main DNS through proxy/VPN" msgstr "" @@ -930,7 +988,11 @@ msgstr "" msgid "Router DNS is routed through sing-box" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:494 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:26 +msgid "Routing" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:384 msgid "Routing Excluded IPs" msgstr "" @@ -958,88 +1020,80 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:484 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532 msgid "Russia inside restrictions" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:302 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:433 msgid "Secret key for authenticating remote access to YACD when WAN access is enabled." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:39 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:54 msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:480 msgid "Select a predefined list for routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:13 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:40 msgid "Select between VPN and Proxy connection methods for traffic routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:13 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:46 msgid "Select DNS protocol to use" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:328 msgid "Select how often the domain or subnet lists are updated automatically" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:24 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:52 msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:341 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:410 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:25 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59 msgid "Select or enter DNS server address" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:400 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:518 msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:387 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:504 msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:397 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:443 msgid "Select the DNS protocol type for the domain resolver" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:520 -msgid "Select the list type for adding custom domains" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:600 -msgid "Select the list type for adding custom subnets" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:436 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:555 msgid "Select the log level for sing-box" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:135 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:175 msgid "Select the network interface from which the traffic will originate" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:181 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:223 msgid "Select the network interface to which the traffic will originate" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:242 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:286 msgid "Select the WAN interfaces to be monitored" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:27 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:55 msgid "Selector" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216 msgid "Selector Proxy Links" msgstr "" @@ -1047,7 +1101,7 @@ msgstr "" msgid "Self-update failed" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:69 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:105 msgid "Send upstream DNS queries through a proxy/VPN outbound instead of directly. Bootstrap DNS always stays direct." msgstr "" @@ -1055,7 +1109,7 @@ msgstr "" msgid "Services info" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:52 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/netshift.js:67 msgid "Settings" msgstr "" @@ -1072,7 +1126,7 @@ msgstr "" msgid "Sing-box autostart disabled" msgstr "" -#: src/netshift/tabs/manager/initController.ts:190 +#: src/netshift/tabs/manager/initController.ts:187 msgid "Sing-box core changed, version:" msgstr "" @@ -1096,28 +1150,32 @@ msgstr "" msgid "Sing-box version is compatible (newer than 1.12.4)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:134 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:22 +msgid "Source and output interfaces, and Bad WAN interface monitoring" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:174 msgid "Source Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:495 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:385 msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:771 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:841 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:725 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:748 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:817 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:679 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:745 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:769 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1129,15 +1187,20 @@ msgstr "" msgid "Stop NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:29 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:21 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57 msgid "Subscription" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:130 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:22 +msgid "Subscription feeds, server filters and URLTest tuning" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163 msgid "Subscription Update Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:91 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:122 msgid "Subscription URLs" msgstr "" @@ -1153,7 +1216,7 @@ msgstr "" msgid "Switch to stable" msgstr "" -#: src/netshift/tabs/manager/initController.ts:178 +#: src/netshift/tabs/manager/initController.ts:177 msgid "Switching sing-box core, this may take a few minutes…" msgstr "" @@ -1161,7 +1224,7 @@ msgstr "" msgid "System info" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts:21 +#: src/netshift/tabs/diagnostic/partials/renderSystemInfo.ts:24 msgid "System information" msgstr "" @@ -1169,40 +1232,36 @@ msgstr "" msgid "Table exist" msgstr "" -#: src/netshift/tabs/dashboard/partials/renderSections.ts:108 +#: src/netshift/tabs/dashboard/partials/renderSections.ts:104 msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:524 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:604 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:579 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:667 msgid "Text List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:46 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:81 msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:230 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:244 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:276 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:317 msgid "The URL used to test server connectivity" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:121 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153 msgid "This is a security trade-off: an attacker could intercept the fetch." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:465 -msgid "This prevents applications from bypassing the router's DNS filtering by using their own encrypted DNS." -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:114 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:152 msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" @@ -1214,24 +1273,24 @@ msgstr "" msgid "Traffic Total" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:25 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:26 msgid "Troubleshooting" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:125 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:163 msgid "TTL must be a positive number" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:120 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:158 msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:313 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:355 msgid "UDP over TCP" msgstr "" @@ -1270,7 +1329,7 @@ msgstr "" msgid "Update NetShift" msgstr "" -#: src/netshift/tabs/manager/initController.ts:217 +#: src/netshift/tabs/manager/initController.ts:214 msgid "Updating NetShift, this may take a few minutes; the page will reload…" msgstr "" @@ -1279,6 +1338,10 @@ msgstr "" msgid "Uplink" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:17 +msgid "Upstream and bootstrap DNS resolvers, and optional DNS-over-proxy" +msgstr "" + #: src/validators/validateProxyUrl.ts:42 msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" @@ -1287,59 +1350,51 @@ msgstr "" msgid "URL must use one of the following protocols:" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:28 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:56 msgid "URLTest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:229 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:268 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:242 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:275 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283 msgid "URLTest Tolerance" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:119 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:151 msgid "Use only for IP-host panels that serve an invalid or self-signed certificate." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:486 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:494 msgid "Use this only when the router has working IPv6 connectivity." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:330 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373 msgid "Use with Exclusion sections to route specific domains directly." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:519 -msgid "User Domain List Type" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:531 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:557 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:614 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:599 -msgid "User Subnet List Type" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:637 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702 msgid "User Subnets List" msgstr "" @@ -1365,12 +1420,12 @@ msgstr "" msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:590 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:669 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:647 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734 msgid "Validation errors:" msgstr "" -#: src/netshift/tabs/manager/initController.ts:315 +#: src/netshift/tabs/manager/initController.ts:312 msgid "Version" msgstr "" @@ -1379,44 +1434,40 @@ msgstr "" msgid "View logs" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:31 +#: src/netshift/tabs/diagnostic/partials/renderWikiDisclaimer.ts:32 msgid "Visit Wiki" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:38 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:217 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:467 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:486 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:534 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:326 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369 msgid "When enabled, traffic not matching any other section's lists will go through this proxy." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:80 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:117 msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:301 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:432 msgid "YACD Secret Key" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:172 -msgid "You can select Output Network Interface, by default autodetect" -msgstr "" - -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:145 -msgid "Группировать по странам" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:32 +msgid "YACD web dashboard access and remote-access protection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:146 -msgid "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:213 +msgid "You can select Output Network Interface, by default autodetect" msgstr "" From 76ac754acd92f4fdc1e73e26892992f9332aaa95 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Sun, 7 Jun 2026 18:58:08 +0300 Subject: [PATCH 61/75] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20v=20?= =?UTF-8?q?=D0=B8=D0=B7=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B9=20=D0=BD=D0=B0=20ipk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile-ipk | 4 +- .../memory/architect-orchestrator.md | 125 ++++++++++++++ .../memory/shell-backend-developer.md | 48 ++++++ docs/agent-rules/packaging.md | 30 +++- netshift/files/usr/lib/updater.sh | 150 ++++++++++++++--- tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 155 +++++++++++++++++- 7 files changed, 480 insertions(+), 34 deletions(-) diff --git a/Dockerfile-ipk b/Dockerfile-ipk index 9b5c38a4..878e5baa 100644 --- a/Dockerfile-ipk +++ b/Dockerfile-ipk @@ -1,11 +1,11 @@ FROM itdoginfo/openwrt-sdk-ipk:24.10.6 ARG NETSHIFT_VERSION +ENV NETSHIFT_VERSION=${NETSHIFT_VERSION} COPY ./netshift /builder/package/feeds/utilities/netshift COPY ./luci-app-netshift /builder/package/feeds/luci/luci-app-netshift -RUN export NETSHIFT_VERSION="v${NETSHIFT_VERSION}" && \ - make defconfig && \ +RUN make defconfig && \ make package/netshift/compile V=s -j4 && \ make package/luci-app-netshift/compile V=s -j4 diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index af007b82..c721ad98 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -540,3 +540,128 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> NOT a task-022 defect. When a smoke category uses this pattern, trust the ✓/✗ marks, not just the "Results: N passed" line; a real gating test needs `done < tmpfile`. + +## FIRST apk / OpenWrt 25 HARDWARE TEST (2026-06-07, router 192.168.1.101) + +- TEST BOX: ssh root@192.168.1.101 (blank pw), OpenWrt **25.12.4**, target + sunxi/cortexa7 (arm_cortex-a7_neon-vfpv4, armv7l), **apk-tools 3.0.5**, Orange + Pi One, small overlay (98.8M total, ~60M free baseline). It is a SPARE box + BEHIND the main gw (its default route = 192.168.1.1, 0 dhcp leases), reached + DIRECTLY over LAN — so netshift kill-switch can't lock me out; rescue + `/etc/init.d/netshift stop` always reachable. NO base64/openssl/xxd/od on a + bare box -> push files with raw `cat > /tmp/f` over ssh (ssh stdin is 8-bit + clean); base64-over-ssh FAILS (busybox base64 absent until coreutils-base64 + dep installs). sha256sum to verify transfer. +- BUILD: `docker build -f Dockerfile-apk --build-arg NETSHIFT_VERSION=<v> -t + netshift-apk:test .`; extract via `docker create`+`docker cp + /builder/bin/packages/x86_64/{utilities,luci}/.`. Packages are PKGARCH=all + (noarch) so x86_64 SDK output installs on armv7 fine. +- **BUG #1 (REAL, apk-only, HIGH): apk `mkpkg` REJECTS version strings with a + dash in the upstream part.** `apk version --check` rejects `0.8.5-rc1-r1` and + `0.8.5-test-904fd64-r1` (rc=1) but accepts `0.8.5-r1`, `0.07062026-r1`, + `0.8.5_rc1-r1`. apk treats `-` as the `-rN` release separator. So a + NETSHIFT_VERSION with a dash (an RC tag `0.8.5-rc1`, or a `git describe` + `0.8.5-11-gHASH`) makes the apk build DIE at `package/.../netshift failed to + build` while the IPK build tolerates it (why it never showed on the 24.10/ipk + main router). build.yml's normal `git describe --tags --exact-match || + 0.$(date)` gives clean numerics so prod releases are usually safe — but ANY + dashed tag breaks apk-only. FIX direction (packaging task): sanitize dashes in + the apk Dockerfile/Makefile version (`-`→`_` or `.`) OR document the + no-dash-tag constraint. Dockerfile-apk passes version RAW (the known ipk-vs-apk + v-prefix asymmetry is nearby). +- INSTALL + RUNTIME CHAIN ON OWRT25/apk = WORKS. `apk update` then `apk add + --allow-untrusted /tmp/.../netshift-*.apk` pulled all deps (sing-box 1.12.17, + jq 1.8.1, bind-dig, coreutils-base64, kmod-nft-tproxy). Version stamp applied + in BOTH constants.sh (0.8.5) and luci main.js. Fresh install with EMPTY + proxy_string correctly ABORTS ("Outbound section not found") — expected, not a + bug. After setting a valid test SS proxy + (`ss://<b64userinfo>@192.0.2.10:8388#test`) + restart: sing-box running, nft + `NetShiftTable` with correct marks (0x00100000/0x00200000 -> tproxy + 127.0.0.1:1602), rt_tables `105 netshift`, dnsmasq -> 127.0.0.42, Clash API + :9090 listening, `sing-box check` rc=0, `global_check` renders perfectly + (UTF-8 emoji intact — apk packaging preserves encoding). get_system_info JSON + correct, fetched `latest: 0.8.6` from GitHub. +- ASYNC CORE-SWITCH path WORKS on apk: `component_action_async sing_box + install_extended` returns instantly with job_id (no rpcd 30s brick — task-007 + fix holds on apk). NOTE: `component_action_status` JSON has keys + success/running/exit_code/message — NO "status" key (don't poll-grep for + "status"). +- **BUG #2 (minor, cosmetic): check_update_stable shows apk `-r` suffix.** + returns current_version "1.12.17" vs latest_version "1.12.17-r1" (status + correctly "latest" — the ${v%%-*} semver compare works, only the DISPLAY + string carries apk's -r1). UI would show "latest: 1.12.17-r1". Polish for apk. +- **FINDING #3 (GOOD — graceful failure validated): extended install FAILED + SAFELY on the small overlay.** Download (armv7 v1.13.12-extended-2.4.0) ok to + tmpfs, but extraction ran out of overlay space; updater rolled back cleanly: + stock core stayed executable & running, no leftover /tmp/netshift-sbext.*, no + brick. The extract logic (updater.sh ~1016-1027) already rm's the live binary + then streams the new member straight onto the final path (never 2 binaries on + overlay) + tmpfs backup/restore — well-designed. The HISTORIC brick did NOT + recur. Real limitation: the EXTENDED binary (~50MB+) simply can't fit a + ~12-60M overlay (Orange Pi One class) — matches the documented ≥25MB + requirement; extended is not installable on small-flash devices. Failure msg + even guesses the cause correctly. +- **FINDING #5 (the one to chase): after the FAILED extended-install rollback, + stock `sing-box version/help/check` SEGFAULT (rc 139)** while the already- + RUNNING daemon kept working. ROOT: the rollback `mv backup -> /usr/bin/sing-box` + restored a binary, but the on-disk file ended up being a PRE-EXISTING stale + `Jun-4` binary (the box had an old sing-box before my test); the live daemon's + /proc/PID/exe showed `-> /usr/bin/sing-box (deleted)`. So the segfault is an + ARTIFACT of (failed extended rollback) × (pre-existing stale binary) × (100%- + full overlay during the op), NOT a clean repro of our code. BUT worth a guarded + re-test on a CLEAN box with adequate overlay: confirm a normal apk + install/reinstall of stock sing-box 1.12.17 on armv7/OWRT25 does NOT segfault + on `version`/`check` (if it does, that breaks sing_box_save_config validation). +- OVERLAY 100% during the failed extract caused transient `uci: I/O error` + + `crontab: can't create root.new` on the first stop; `du` of overlay upper was + only 21M while `df` said 100% (loop/squashfs accounting + fs fragmentation). + Stopping netshift freed it back to 61%, then clean. +- CLEANUP (always do this): `/etc/init.d/netshift stop` -> `apk del + luci-i18n-netshift-ru luci-app-netshift` then `apk del netshift` (apk + reverse-dep purge also removed sing-box/jq/kmods, 146->128 pkgs). prerm + correctly removed `105 netshift` from rt_tables. /etc/config/netshift is a + conffile (survives removal — rm by hand if you want a pristine box). Final + state restored: no pkgs/table/rt/proc, internet OK, overlay 40%, dnsmasq + noresolv=0. Box returned to baseline. +- Minor: a fresh-box `restart` logs `crontab: can't open 'root'` (no root + crontab spool yet) — cosmetic, cron line still created later. + +## Finding #5 RESOLVED + task-027 backup-integrity fix (2026-06-07) + +- RE-VERIFIED #5 on a CLEAN box: freshly apk-installed stock sing-box 1.12.17 + (the REAL 40119352-byte / 40MB armv7 binary) runs version/check/help all rc=0, + NO segfault. So #5 (segfault) was NOT our bug — it was a corrupt-restore + ARTIFACT: last session's failed extended-install rollback had `mv`'d a + TRUNCATED 12.7MB backup onto /usr/bin/sing-box (real binary is 40MB) under a + 100%-full overlay, and that truncated file segfaulted. KEY TELL: binary SIZE + mismatch (12.7M vs 40M) + the live daemon's /proc/PID/exe showed "(deleted)". +- BUT re-reading updater.sh exposed a REAL latent bug behind the artifact: the + core-swap backup `cp -p <bin> <tmpfs-backup> 2>/dev/null` checked only cp's exit + code, with NO size/integrity check. busybox cp can truncate under tmpfs ENOSPC + and silently pass the guard, so the rollback `mv backup -> /usr/bin/sing-box` + could restore a CORRUPT (segfaulting) binary — the safety net handing the router + a broken core. Same pattern in BOTH the extended path (~997-1014, rollbacks + ~1019-1060) AND the stable path (~1138-1155 + updates_stable_rollback). +- FIX = task-027 (shell-backend-developer, code-reviewer APPROVED, gates green): + two busybox-safe helpers `updates_verify_copy src dst` (0 iff dst size == src + size via `wc -c < file`) and `updates_backup_is_complete backup expected_size` + (vs a size STASHED at backup time, NOT the live path which a half-written + extract may have clobbered). Gate all 4 backup-cp sites (abort BEFORE touching + the live binary -> working core intact); guard all 4 restore sites (extended + x3 + updates_stable_rollback, now takes $3/$4 sizes, both call sites threaded) + to REFUSE restoring a truncated/missing backup (honest failure JSON + error log + instead of installing a broken core). NO exit 1 (async worker -> would skip JSON + + epilogue); used the updates_log+echo+return 1 pattern. +test_backup_integrity + (alias backupguard, 10 assertions). shellcheck clean; smoke all = 120 passed/0 + (110 -> +10). +- BUSYBOX SIZE-COMPARE IDIOM (reusable, reviewer-flagged): end size guards with + `[ -n "$x" ] && [ "$x" = "$y" ]` — the `-n` short-circuit makes an empty `wc -c` + output fail SAFE (refuse restore) instead of `[: arg`. Use `wc -c < file` + (redirect, no leading-whitespace) NOT `wc -c file` (arg form has whitespace). +- ON-HARDWARE PROOF (router 192.168.1.101): pushed the fixed updater.sh, sourced + the real functions, made a real 40MB source + a head-c-truncated 12.7MB backup + (the exact size that originally segfaulted) -> all 7 assertions PASS incl. + rollback-refuses-truncated (live NOT clobbered) and rollback-restores-complete. + PERF note: `dd bs=1 count=12M` to truncate is GLACIAL on armv7 (timed out); + use `head -c N` instead. Box cleaned to baseline after (apk del sing-box; no + pkgs/table/binary, internet OK, overlay 40%). diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 5e051737..85d3837b 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -729,3 +729,51 @@ findings; keep under ~200 lines. unchanged due to the documented piped-while subshell counter quirk). UTF-8 intact. New constant `TMP_SUBSCRIPTION_MERGE_FOLDER`; UCI example documents the `list subscription_url` form. NO sacred constant/port/mark/path changed. + +## task-027: core-swap backup integrity (truncated-rollback anti-segfault) + +- Root cause (hardware, OpenWrt 25, armv7, tiny overlay): the tmpfs core backup + used `cp -p ... 2>/dev/null` checking ONLY cp's rc. busybox `cp` can TRUNCATE + under tmpfs ENOSPC and still return 0, so a partial backup passed the "Failed + to backup current sing-box binary" guard; then the rollback `mv backup -> + /usr/bin/sing-box` restored a 12.7 MB stub in place of the real ~40 MB core → + segfault (rc 139) on every invocation. The rollback IS the safety net, so a + corrupt backup is strictly worse than not rolling back. +- Two new helpers at the top of `updater.sh` (next to `updates_log`): + `updates_verify_copy src dst` (0 iff dst exists and `wc -c < dst` == `wc -c < + src`; absent src → 0 = nothing to back up) and `updates_backup_is_complete + backup expected_size` (0 iff backup exists and `wc -c` == the stashed size). + Size-match only — guarding TRUNCATION not bit-rot; do NOT md5/sha a 40 MB + binary on slow armv7. busybox-safe `wc -c < file` (NOT GNU stat). +- **Stash the expected size at backup time.** After each backup `cp` succeeds + + verifies, record `backup_binary_size="$(wc -c < "$backup_binary")"` (and + `backup_cronet_size`) into NEW locals. Rollbacks compare the backup's current + size against that stash (not against the live `/usr/bin/sing-box`, which the + half-written extract may have already clobbered). For `updates_stable_rollback` + the sizes are passed as NEW args $3/$4 — both call sites updated. +- Backup-cp gates (4): extended binary, extended libcronet, stable binary, + stable libcronet — each `if ! cp -p ... || ! updates_verify_copy ...; then` + inside the EXISTING abort path that fires BEFORE the live binary is touched → + healthy box = no-op pass, working core left intact on failure. +- Rollback restore guards (4 sites): extended extract-fail, extended cronet-fail, + extended validation-fail, and shared `updates_stable_rollback`. Pattern: `if + updates_backup_is_complete "$backup" "$size"; then mv -f ...; else updates_log + "...backup is corrupt/incomplete; NOT restoring to avoid installing a broken + core" "error"; fi`. NEVER overwrite live from a truncated backup. +- CRITICAL: this runs inside the `component_action` async worker → NO `exit 1` + (would kill the worker, skip JSON emission + restore epilogue). The corrupt- + backup branches just `updates_log "..." "error"` and fall through to the + existing `echo "{...success:false...}"; return 1` honest-failure path. Did NOT + touch download/extract streaming (rm-then-stream), connectivity self-heal, + async machinery, or any constant. +- Smoke: NEW top-level `test_backup_integrity` (alias `backupguard`, 10 + assertions). Driver sources REAL updater.sh, silences log, re-pins + `UPDATES_SING_BOX_BIN`/`UPDATES_LIBCRONET_LIB` to /tmp temp files (so the + container's real binary is untouched), builds 64-byte src + complete + 10-byte + truncated fixtures via `dd`, then drives the 3 helpers + `updates_stable_rollback` + with truncated vs complete backups (asserts live marker survives the truncated + rollback AND the complete one overwrites it). Parsed in the CURRENT shell + (`while read < "$out"`, no pipe) so PASS counts are EXACT (avoids the + piped-while subshell counter quirk). Registered all 5 points (all)/case alias/ + usage line/docker-compose comment). shellcheck -S error clean (bin+libs+ + install.sh); `smoke-tests all` = 120 passed / 0 failed (110 baseline + 10 new). diff --git a/docs/agent-rules/packaging.md b/docs/agent-rules/packaging.md index 9814bda5..8df87e21 100644 --- a/docs/agent-rules/packaging.md +++ b/docs/agent-rules/packaging.md @@ -68,18 +68,34 @@ best-effort). It uses the same `PKG_VERSION` expression and updated, `luci-base` installed, feed dirs created; the apk one also runs `./setup.sh`). -### KNOWN INCONSISTENCY — respect it, do not "fix" blindly +### Version passing — symmetric, both RAW (no `v` prefix) -The two release Dockerfiles pass the version differently: +Both release Dockerfiles now pass the version **raw**, with no `v` prefix: -- `Dockerfile-ipk`: `RUN export NETSHIFT_VERSION="v${NETSHIFT_VERSION}" && ...` - — it **prepends `v`**. +- `Dockerfile-ipk`: `ENV NETSHIFT_VERSION=${NETSHIFT_VERSION}` — **raw, no + `v`**. - `Dockerfile-apk`: `ENV NETSHIFT_VERSION=${NETSHIFT_VERSION}` — **raw, no `v`**. -This asymmetry is intentional/load-bearing for the current artifact names. Do -not normalize one to match the other without verifying the whole release flow -(§4) and `install.sh` matching (§6). +> Historical note (task-028): `Dockerfile-ipk` used to **prepend `v`** +> (`RUN export NETSHIFT_VERSION="v${NETSHIFT_VERSION}" && ...`) while the apk +> file passed it raw. That asymmetry stamped a leading `v` into the ipk +> package/control version and the runtime `constants.sh` `NETSHIFT_VERSION`, +> so on OWRT24/ipk the installed version (`v0.8.6`) never matched the no-`v` +> GitHub tag (`0.8.6`) and the LuCI UI falsely reported "outdated". The `v` +> prepend was removed (ipk normalized to the apk shape) after verifying the +> whole release flow (§4) and `install.sh` matching (§6): the `_`→`-` rename, +> the 3-package filter, the i18n `-${VERSION}` naming, and the release tag all +> derive from the git tag, and `install.sh` matches assets by **name prefix** +> (the `v` lived in the version segment, not the name prefix) — so dropping it +> does not affect either. Keep both Dockerfiles passing the version raw. +> +> Residual fragility (out of scope, on record): the UI version-equality check +> in `fe-app-netshift` does **not** normalize a leading `v`. If a future +> release is tagged **with** a `v` (e.g. `v0.8.7`), `netshift_latest_version` +> would carry `v` while the installed (no-`v`) version would not, re-triggering +> the false-"outdated" mismatch. **Tag releases WITHOUT a `v`** to keep +> ipk / apk / tag all consistent. --- diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 812fa29e..32701819 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -27,6 +27,42 @@ updates_log() { log "Updater: $message" "$level" } +# Verify that a backup copy is byte-complete, guarding the core-swap rollback +# against a TRUNCATED backup written under tmpfs ENOSPC (busybox `cp` does not +# reliably return non-zero on a partial write). Returns 0 iff $dst exists and +# its byte size equals $src's size. A size match is sufficient here: this is a +# same-machine copy of the same file and we are guarding truncation, not bit-rot +# — do NOT md5/sha a ~40 MB binary on a slow armv7 router. If $src is absent +# there is nothing to back up, so verification trivially succeeds (0). +updates_verify_copy() { + local src="$1" + local dst="$2" + local ssz dsz + + [ -f "$src" ] || return 0 + [ -f "$dst" ] || return 1 + ssz="$(wc -c < "$src" 2>/dev/null)" || return 1 + dsz="$(wc -c < "$dst" 2>/dev/null)" || return 1 + [ -n "$ssz" ] && [ "$ssz" = "$dsz" ] +} + +# Verify that a stashed backup is still byte-complete BEFORE a rollback restores +# it over the live path. Compares the backup's current byte size against the +# expected source size recorded at backup time. Returns 0 iff $backup exists and +# its size equals $expected_size. Refusing to restore a truncated backup is +# safer than installing a segfaulting core as the "safe" fallback. +updates_backup_is_complete() { + local backup="$1" + local expected_size="$2" + local bsz + + [ -n "$backup" ] || return 1 + [ -f "$backup" ] || return 1 + [ -n "$expected_size" ] || return 1 + bsz="$(wc -c < "$backup" 2>/dev/null)" || return 1 + [ -n "$bsz" ] && [ "$bsz" = "$expected_size" ] +} + # ── Async component-action job state (jq, atomic) ─────────────────── # # The UI starts long-running component actions (e.g. switching the sing-box @@ -918,6 +954,7 @@ _updates_install_sing_box_extended_core() { local tmp_dir archive releases tag rel asset_url local binary_path cronet_path local backup_binary="" backup_cronet="" new_version + local backup_binary_size="" backup_cronet_size="" # Interruption-tolerant heal: a run killed mid-flight (e.g. the old rpcd 30s # timeout) could leave a non-executable /usr/bin/sing-box behind. Such a @@ -996,21 +1033,29 @@ _updates_install_sing_box_extended_core() { # no room for a second copy of the binary. if [ -e /usr/bin/sing-box ]; then backup_binary="$tmp_dir/sing-box.backup" - if ! cp -p /usr/bin/sing-box "$backup_binary" 2>/dev/null; then + # Gate on a byte-complete backup, not just cp's exit code: a partial + # write under tmpfs ENOSPC could otherwise pass and later be restored as + # a truncated, segfaulting "safe" fallback. Abort here — the live binary + # has NOT been touched yet, so the working core is left intact. + if ! cp -p /usr/bin/sing-box "$backup_binary" 2>/dev/null || + ! updates_verify_copy /usr/bin/sing-box "$backup_binary"; then rm -rf "$tmp_dir" updates_log "Failed to backup current sing-box binary" "error" echo "{\"success\":false,\"message\":\"Failed to backup current sing-box binary\"}" return 1 fi + backup_binary_size="$(wc -c < "$backup_binary" 2>/dev/null)" fi if [ -n "$cronet_path" ] && [ -e /usr/lib/libcronet.so ]; then backup_cronet="$tmp_dir/libcronet.so.backup" - if ! cp -p /usr/lib/libcronet.so "$backup_cronet" 2>/dev/null; then + if ! cp -p /usr/lib/libcronet.so "$backup_cronet" 2>/dev/null || + ! updates_verify_copy /usr/lib/libcronet.so "$backup_cronet"; then rm -rf "$tmp_dir" updates_log "Failed to backup current libcronet.so" "error" echo "{\"success\":false,\"message\":\"Failed to backup current libcronet.so\"}" return 1 fi + backup_cronet_size="$(wc -c < "$backup_cronet" 2>/dev/null)" fi # Free overlay space by removing the live binary BEFORE extracting, then @@ -1019,7 +1064,16 @@ _updates_install_sing_box_extended_core() { rm -f /usr/bin/sing-box if ! tar -xzf "$archive" -O "$binary_path" > /usr/bin/sing-box 2>/dev/null || [ ! -s /usr/bin/sing-box ]; then rm -f /usr/bin/sing-box - [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box + # Only restore from a backup that is still byte-complete — restoring a + # truncated backup would install a segfaulting core as the "safe" + # fallback (worse than leaving the path absent). + if [ -n "$backup_binary" ]; then + if updates_backup_is_complete "$backup_binary" "$backup_binary_size"; then + mv -f "$backup_binary" /usr/bin/sing-box + else + updates_log "Rollback: sing-box backup is corrupt/incomplete; NOT restoring to avoid installing a broken core" "error" + fi + fi rm -rf "$tmp_dir" updates_log "Failed to extract sing-box-extended binary (out of space on overlay?)" "error" echo "{\"success\":false,\"message\":\"Failed to extract sing-box-extended binary (not enough free space on the router?)\"}" @@ -1031,8 +1085,20 @@ _updates_install_sing_box_extended_core() { rm -f /usr/lib/libcronet.so if ! tar -xzf "$archive" -O "$cronet_path" > /usr/lib/libcronet.so 2>/dev/null || [ ! -s /usr/lib/libcronet.so ]; then rm -f /usr/bin/sing-box /usr/lib/libcronet.so - [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box - [ -n "$backup_cronet" ] && mv -f "$backup_cronet" /usr/lib/libcronet.so + if [ -n "$backup_binary" ]; then + if updates_backup_is_complete "$backup_binary" "$backup_binary_size"; then + mv -f "$backup_binary" /usr/bin/sing-box + else + updates_log "Rollback: sing-box backup is corrupt/incomplete; NOT restoring to avoid installing a broken core" "error" + fi + fi + if [ -n "$backup_cronet" ]; then + if updates_backup_is_complete "$backup_cronet" "$backup_cronet_size"; then + mv -f "$backup_cronet" /usr/lib/libcronet.so + else + updates_log "Rollback: libcronet.so backup is corrupt/incomplete; NOT restoring" "error" + fi + fi rm -rf "$tmp_dir" updates_log "Failed to extract libcronet.so" "error" echo "{\"success\":false,\"message\":\"Failed to extract libcronet.so\"}" @@ -1049,9 +1115,21 @@ _updates_install_sing_box_extended_core() { *extended*) ;; *) rm -f /usr/bin/sing-box - [ -n "$backup_binary" ] && mv -f "$backup_binary" /usr/bin/sing-box + if [ -n "$backup_binary" ]; then + if updates_backup_is_complete "$backup_binary" "$backup_binary_size"; then + mv -f "$backup_binary" /usr/bin/sing-box + else + updates_log "Rollback: sing-box backup is corrupt/incomplete; NOT restoring to avoid installing a broken core" "error" + fi + fi [ -n "$cronet_path" ] && rm -f /usr/lib/libcronet.so - [ -n "$backup_cronet" ] && mv -f "$backup_cronet" /usr/lib/libcronet.so + if [ -n "$backup_cronet" ]; then + if updates_backup_is_complete "$backup_cronet" "$backup_cronet_size"; then + mv -f "$backup_cronet" /usr/lib/libcronet.so + else + updates_log "Rollback: libcronet.so backup is corrupt/incomplete; NOT restoring" "error" + fi + fi rm -rf "$tmp_dir" updates_log "Installed sing-box failed extended validation; previous binary restored" "error" echo "{\"success\":false,\"message\":\"Installed sing-box failed extended validation; previous binary restored\"}" @@ -1122,6 +1200,7 @@ updates_install_sing_box_stable() { _updates_install_sing_box_stable_core() { local new_version installed=1 local tmp_dir backup_binary="" backup_cronet="" + local backup_binary_size="" backup_cronet_size="" # Remove stale temp dirs from an interrupted earlier run (tmpfs is small). rm -rf /tmp/netshift-sbstable.* 2>/dev/null @@ -1137,21 +1216,29 @@ _updates_install_sing_box_stable_core() { # touches anything, so a failed install can be rolled back to a working core. if [ -e "$UPDATES_SING_BOX_BIN" ]; then backup_binary="$tmp_dir/sing-box.backup" - if ! cp -p "$UPDATES_SING_BOX_BIN" "$backup_binary" 2>/dev/null; then + # Gate on a byte-complete backup, not just cp's exit code (busybox cp can + # truncate under tmpfs ENOSPC and still return 0). Abort here — the + # package manager has not touched the binary yet, so the working core + # stays intact. + if ! cp -p "$UPDATES_SING_BOX_BIN" "$backup_binary" 2>/dev/null || + ! updates_verify_copy "$UPDATES_SING_BOX_BIN" "$backup_binary"; then rm -rf "$tmp_dir" updates_log "Failed to backup current sing-box binary" "error" echo "{\"success\":false,\"message\":\"Failed to backup current sing-box binary\"}" return 1 fi + backup_binary_size="$(wc -c < "$backup_binary" 2>/dev/null)" fi if [ -e "$UPDATES_LIBCRONET_LIB" ]; then backup_cronet="$tmp_dir/libcronet.so.backup" - if ! cp -p "$UPDATES_LIBCRONET_LIB" "$backup_cronet" 2>/dev/null; then + if ! cp -p "$UPDATES_LIBCRONET_LIB" "$backup_cronet" 2>/dev/null || + ! updates_verify_copy "$UPDATES_LIBCRONET_LIB" "$backup_cronet"; then rm -rf "$tmp_dir" updates_log "Failed to backup current libcronet.so" "error" echo "{\"success\":false,\"message\":\"Failed to backup current libcronet.so\"}" return 1 fi + backup_cronet_size="$(wc -c < "$backup_cronet" 2>/dev/null)" fi if command -v apk >/dev/null 2>&1; then @@ -1179,7 +1266,7 @@ _updates_install_sing_box_stable_core() { if [ "$installed" -eq 0 ]; then # Package install failed (it may have already removed/half-replaced the # binary). Restore the tmpfs backup so a working core remains. - updates_stable_rollback "$backup_binary" "$backup_cronet" + updates_stable_rollback "$backup_binary" "$backup_cronet" "$backup_binary_size" "$backup_cronet_size" rm -rf "$tmp_dir" updates_log "Failed to install stable sing-box via package manager; previous binary restored" "error" echo "{\"success\":false,\"message\":\"Failed to install stable sing-box (package manager error); previous binary restored\"}" @@ -1193,7 +1280,7 @@ _updates_install_sing_box_stable_core() { # longer be an "extended" build. If it still is, the install did not land — # restore the backup so the router keeps a known-good core. if is_sing_box_extended "$new_version"; then - updates_stable_rollback "$backup_binary" "$backup_cronet" + updates_stable_rollback "$backup_binary" "$backup_cronet" "$backup_binary_size" "$backup_cronet_size" rm -rf "$tmp_dir" updates_log "Stable install reported success but sing-box is still extended ($new_version); previous binary restored" "error" echo "{\"success\":false,\"message\":\"sing-box is still the extended build after install; rollback did not take effect (previous binary restored)\"}" @@ -1217,25 +1304,42 @@ _updates_install_sing_box_stable_core() { # Restores the tmpfs backup of /usr/bin/sing-box (and libcronet.so) into place. # Used by the stable path when the package install or validation fails so the # router never ends core-less. Best-effort; logs the outcome. +# +# Args 3/4 are the byte sizes recorded at backup time. The restore is performed +# ONLY if the backup is still byte-complete (size match) — a truncated backup +# (tmpfs ENOSPC) is refused rather than restored as a segfaulting core. updates_stable_rollback() { local backup_binary="$1" local backup_cronet="$2" - - if [ -n "$backup_binary" ] && [ -e "$backup_binary" ]; then - rm -f "$UPDATES_SING_BOX_BIN" 2>/dev/null - if mv -f "$backup_binary" "$UPDATES_SING_BOX_BIN" 2>/dev/null; then - chmod 0755 "$UPDATES_SING_BOX_BIN" 2>/dev/null || true - updates_log "Rollback: restored previous sing-box binary from tmpfs backup" + local backup_binary_size="$3" + local backup_cronet_size="$4" + + if [ -n "$backup_binary" ]; then + # Only restore a byte-complete backup: a truncated backup (tmpfs ENOSPC) + # would otherwise be installed as a segfaulting "safe" core, which is + # worse than not restoring. Surface a loud error and leave the live path. + if updates_backup_is_complete "$backup_binary" "$backup_binary_size"; then + rm -f "$UPDATES_SING_BOX_BIN" 2>/dev/null + if mv -f "$backup_binary" "$UPDATES_SING_BOX_BIN" 2>/dev/null; then + chmod 0755 "$UPDATES_SING_BOX_BIN" 2>/dev/null || true + updates_log "Rollback: restored previous sing-box binary from tmpfs backup" + else + updates_log "Rollback: FAILED to restore sing-box binary from backup" "error" + fi else - updates_log "Rollback: FAILED to restore sing-box binary from backup" "error" + updates_log "Rollback: sing-box backup is corrupt/incomplete; NOT restoring to avoid installing a broken core" "error" fi fi - if [ -n "$backup_cronet" ] && [ -e "$backup_cronet" ]; then - rm -f "$UPDATES_LIBCRONET_LIB" 2>/dev/null - if mv -f "$backup_cronet" "$UPDATES_LIBCRONET_LIB" 2>/dev/null; then - chmod 0644 "$UPDATES_LIBCRONET_LIB" 2>/dev/null || true - updates_log "Rollback: restored previous libcronet.so from tmpfs backup" + if [ -n "$backup_cronet" ]; then + if updates_backup_is_complete "$backup_cronet" "$backup_cronet_size"; then + rm -f "$UPDATES_LIBCRONET_LIB" 2>/dev/null + if mv -f "$backup_cronet" "$UPDATES_LIBCRONET_LIB" 2>/dev/null; then + chmod 0644 "$UPDATES_LIBCRONET_LIB" 2>/dev/null || true + updates_log "Rollback: restored previous libcronet.so from tmpfs backup" + fi + else + updates_log "Rollback: libcronet.so backup is corrupt/incomplete; NOT restoring" "error" fi fi } diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 9c67c636..fe226186 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -11,7 +11,7 @@ # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, # nftv6, diagnostics, subscription, insecure, rejected, # jobstate, selfheal, dnsdetour, globalproxy, stablecheck, -# extcheck, selfupdate +# extcheck, selfupdate, backupguard # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index c953af79..ee1b1cf2 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -3538,6 +3538,157 @@ DRVEOF rm -rf "$work" } +# ───────────────────────────────────────────────────────────────── +# Test: core-swap backup integrity (task-027) +# ───────────────────────────────────────────────────────────────── +# Guards the on-hardware latent bug where a TRUNCATED tmpfs backup (busybox cp +# under ENOSPC) could be restored over /usr/bin/sing-box, installing a +# segfaulting core as the "safe" fallback. Drives the REAL sourced updater.sh: +# * updates_verify_copy — size-match gate used right after the backup cp. +# * updates_backup_is_complete — size-match gate used before every rollback. +# * updates_stable_rollback — must REFUSE to overwrite the live binary from +# a truncated backup (and DO restore a complete +# one), with UPDATES_SING_BOX_BIN pointed at a +# temp file so the container's real binary is +# never touched. +# Asserts: (a) complete backup verifies OK; (b) truncated/missing backup is +# detected (verify nonzero); (c) rollback does not clobber the live path from a +# truncated backup but still restores from a complete one. +test_backup_integrity() { + header "Core-swap Backup Integrity (task-027)" + + local updater="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$updater" ]; then + skip "updater.sh not found in ${NETSHIFT_LIB_DIR}" + return + fi + + local work="/tmp/netshift-backupguard-$$" + rm -rf "$work" + mkdir -p "$work" + + # Driver: source updater.sh, silence logging, re-pin the live-binary paths to + # temp files, then run the verify helpers + the rollback guard. Each check + # echoes a name:OK / name:FAIL token. The driver runs to a result file which + # we parse in the CURRENT shell (no pipe) so the PASS/FAIL counters are exact. + local drv="$work/driver.sh" + cat > "$drv" << 'DRVEOF' +log() { :; } +echolog() { :; } +nolog() { :; } +updates_log() { :; } +. "DRV_UPDATER" +updates_log() { :; } + +W="DRV_WORK" + +# Fixtures: a "source" of 64 bytes, a COMPLETE copy, a TRUNCATED copy. +src="$W/src.bin" +complete="$W/complete.backup" +truncated="$W/truncated.backup" +dd if=/dev/zero of="$src" bs=1 count=64 >/dev/null 2>&1 +cp -p "$src" "$complete" +dd if=/dev/zero of="$truncated" bs=1 count=10 >/dev/null 2>&1 + +# ── (a) a complete backup verifies OK ────────────────────────────────────── +if updates_verify_copy "$src" "$complete"; then + echo 'backupguard-verify-complete-ok:OK' +else + echo 'backupguard-verify-complete-ok:FAIL' +fi + +# ── (b1) a truncated backup is detected (verify nonzero) ──────────────────── +if updates_verify_copy "$src" "$truncated"; then + echo 'backupguard-verify-truncated-detected:FAIL' +else + echo 'backupguard-verify-truncated-detected:OK' +fi + +# ── (b2) a missing backup is detected (verify nonzero) ────────────────────── +if updates_verify_copy "$src" "$W/does-not-exist.backup"; then + echo 'backupguard-verify-missing-detected:FAIL' +else + echo 'backupguard-verify-missing-detected:OK' +fi + +# ── (b3) absent source = nothing to back up = trivially OK ────────────────── +if updates_verify_copy "$W/no-source" "$W/no-dst"; then + echo 'backupguard-verify-absent-source-ok:OK' +else + echo 'backupguard-verify-absent-source-ok:FAIL' +fi + +# ── backup-is-complete: size match / mismatch / missing ───────────────────── +sz=$(wc -c < "$src") +if updates_backup_is_complete "$complete" "$sz"; then + echo 'backupguard-iscomplete-match:OK' +else + echo 'backupguard-iscomplete-match:FAIL' +fi +if updates_backup_is_complete "$truncated" "$sz"; then + echo 'backupguard-iscomplete-mismatch:FAIL' +else + echo 'backupguard-iscomplete-mismatch:OK' +fi +if updates_backup_is_complete "$W/does-not-exist.backup" "$sz"; then + echo 'backupguard-iscomplete-missing:FAIL' +else + echo 'backupguard-iscomplete-missing:OK' +fi + +# ── (c) updates_stable_rollback must NOT clobber the live path from a +# TRUNCATED backup; it MUST restore from a COMPLETE one. ─────────────── +# Point the live paths at temp files holding a known-good "current" core so we +# can detect whether the rollback overwrote them. +UPDATES_SING_BOX_BIN="$W/live-sing-box" +UPDATES_LIBCRONET_LIB="$W/live-libcronet.so" + +# --- truncated backup: rollback must REFUSE (live core left intact) --- +printf 'LIVE-GOOD-CORE-INTACT-MARKER\n' > "$UPDATES_SING_BOX_BIN" +trunc_backup="$W/rb-trunc.backup" +dd if=/dev/zero of="$trunc_backup" bs=1 count=10 >/dev/null 2>&1 +# Record an expected size (64) that does NOT match the 10-byte truncated backup. +updates_stable_rollback "$trunc_backup" "" "64" "" +if grep -q 'LIVE-GOOD-CORE-INTACT-MARKER' "$UPDATES_SING_BOX_BIN" 2>/dev/null; then + echo 'backupguard-rollback-refuses-truncated:OK' +else + echo 'backupguard-rollback-refuses-truncated:FAIL' +fi +# The truncated backup must NOT have been moved into place either. +if [ -f "$trunc_backup" ]; then + echo 'backupguard-rollback-truncated-not-moved:OK' +else + echo 'backupguard-rollback-truncated-not-moved:FAIL' +fi + +# --- complete backup: rollback MUST restore it over the live path --- +printf 'STALE-HALF-WRITTEN\n' > "$UPDATES_SING_BOX_BIN" +good_backup="$W/rb-good.backup" +printf 'RESTORED-PREVIOUS-GOOD-CORE\n' > "$good_backup" +good_sz=$(wc -c < "$good_backup") +updates_stable_rollback "$good_backup" "" "$good_sz" "" +if grep -q 'RESTORED-PREVIOUS-GOOD-CORE' "$UPDATES_SING_BOX_BIN" 2>/dev/null; then + echo 'backupguard-rollback-restores-complete:OK' +else + echo 'backupguard-rollback-restores-complete:FAIL' +fi +DRVEOF + sed -i "s|DRV_UPDATER|$updater|g;s|DRV_WORK|$work|g" "$drv" + + local out="$work/out.txt" + ash "$drv" > "$out" 2>/dev/null || true + + local line + while IFS= read -r line; do + case "$line" in + *:OK) pass "${line%:OK}" ;; + *:FAIL) fail "$line" "$(cat "$out" 2>/dev/null)" ;; + esac + done < "$out" + + rm -rf "$work" +} + # ───────────────────────────────────────────────────────────────── # Main # ───────────────────────────────────────────────────────────────── @@ -3572,6 +3723,7 @@ main() { test_check_update_stable test_check_update_extended test_self_update_netshift + test_backup_integrity ;; deps) test_deps ;; syntax) test_syntax ;; @@ -3590,12 +3742,13 @@ main() { stablecheck) test_check_update_stable ;; extcheck) test_check_update_extended ;; selfupdate) test_self_update_netshift ;; + backupguard) test_backup_integrity ;; jq) test_jq_helpers ;; cm) test_config_manager ;; sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck selfupdate" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck selfupdate backupguard" exit 1 ;; esac From 053680a69591c9c7e0ba85781ca14904b7df86f1 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Sun, 7 Jun 2026 19:38:21 +0300 Subject: [PATCH 62/75] =?UTF-8?q?=D1=80=D1=83=D1=87=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20netshift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 53 +++++++ .../memory/luci-frontend-developer.md | 66 ++++++++- .../memory/shell-backend-developer.md | 49 +++++++ fe-app-netshift/locales/calls.json | 58 ++++---- fe-app-netshift/locales/netshift.pot | 59 ++++---- fe-app-netshift/locales/netshift.ru.po | 7 +- .../src/netshift/methods/shell/index.ts | 23 +++ .../src/netshift/tabs/manager/cards.ts | 50 ++++--- .../netshift/tabs/manager/initController.ts | 34 ++--- .../netshift/tabs/manager/tests/cards.test.js | 76 +++++----- .../resources/view/netshift/main.js | 62 ++++---- luci-app-netshift/po/ru/netshift.po | 7 +- luci-app-netshift/po/templates/netshift.pot | 59 ++++---- netshift/files/usr/bin/netshift | 15 +- netshift/files/usr/lib/updater.sh | 62 ++++++++ tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 137 +++++++++++++++++- 17 files changed, 598 insertions(+), 221 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index c721ad98..e3efa7bc 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -665,3 +665,56 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> PERF note: `dd bs=1 count=12M` to truncate is GLACIAL on armv7 (timed out); use `head -c N` instead. Box cleaned to baseline after (apk del sing-box; no pkgs/table/binary, internet OK, overlay 40%). + +## task-028 (drop v from ipk) + task-029/030 (on-demand NetShift update check) (2026-06-07) + +- task-028 DONE+APPROVED+COMMITTED by operator (76ac754): removed the `v`-prepend + from Dockerfile-ipk (line 7 `export NETSHIFT_VERSION="v${...}"` -> raw + `ENV NETSHIFT_VERSION=${...}`, mirroring apk). Verified: ipk build 0.8.6 -> + `netshift_0.8.6-r1_all.ipk`, control `Version: 0.8.6-r1`, stamped + `NETSHIFT_VERSION="0.8.6"` (no v anywhere); apk regression ok; install.sh + matches by NAME prefix (not version) so unaffected; build.yml version from git + tag not the Dockerfile v; smoke 120/0. packaging.md §3 updated. This was the + operator's chosen fix for the OWRT24/ipk "falsely outdated" UI symptom. +- THEN operator pivoted: the REAL fix wanted = make NetShift update check + ON-DEMAND (button only), like the sing-box cores, instead of auto-fetching on + every UI mount. Root cause: get_system_info did a `curl .../releases/latest` + on EVERY call (netshift:3604), and the UI calls get_system_info on mount + (manager/initController.ts:382, diagnostic:523) -> entering a tab = a GitHub + request. +- task-029 (backend, APPROVED, smoke 127/0): get_system_info now does ZERO + network I/O — `netshift_latest_version="unknown"` constant (KEY KEPT as the + sentinel the UI understands). New `updates_check_netshift` worker in updater.sh + + `netshift:check_update)` in the component_action() router (~1792, next to + netshift:self_update — NO ACL change, component_action already allowed). It + reuses the PRE-EXISTING `updates_netshift_latest_tag` + `NETSHIFT_RELEASE_API_URL` + (task-017), NORMALIZES a leading `v` on BOTH sides (${x#v}) + `%%-*` semver + + the existing `is_min_package_version` -> echoes the SAME JSON as + updates_check_sing_box_stable (success/current_version/latest_version/status). + NO exit (component_action worker -> echo {json}; return N). global_check now + fetches latest ITSELF (one-shot SSH diag, network ok there). dev build + (*COMPILED* placeholder) -> status latest. This ALSO fixes the v-compare bug at + the backend. +- task-030 (frontend, APPROVED, 472 tests, main.js idempotent): new + NetShiftShellMethods.netshiftCheckUpdate() -> fs.exec ['component_action', + 'netshift','check_update'], parsed by the EXISTING parseComponentCheckUpdate + (same shape as cores). runNetshiftCheck REWRITTEN to mirror runSingBoxCheck — + NO LONGER calls fetchSystemInfo as the check (that was the trap: it'd re-read + "unknown" forever). cards.ts netshiftStatus now derives from + managerChecks.netshift.status (null->neutral until checked); REMOVED the + fragile `installed === latest` string compare + systemInfo-latest dependency; + KEPT the dev guard. Diagnostic getNetshiftVersionRow already treats unknown + latest as neutral -> no code change, no auto-"outdated". Orphaned + `'Latest version is unknown'` msgid removed; fe<->luci catalogs byte-identical. +- PATTERN (on-demand component check, reusable): backend worker via + component_action MUST `echo {json}; return N` (NEVER exit — kills dispatcher); + JSON keys MUST mirror updates_check_sing_box_stable so the FE + parseComponentCheckUpdate works unchanged; FE check fn mirrors runSingBoxCheck + and writes managerChecks.<component>; the card derives status from + managerChecks (null=neutral), NOT from systemInfo. Canonical regression: + installed v0.8.6 vs latest 0.8.6 -> latest (not outdated). +- INTEGRATION VERIFIED (source-level, no live LuCI): get_system_info no curl; + router has netshift:check_update; FE method args + ['component_action','netshift','check_update']; runNetshiftCheck has 0 + fetchSystemInfo calls. All gates green. Ready for manual commit (operator + commits; agents never auto-commit). diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 147db4c4..b47352c2 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -646,8 +646,64 @@ append findings; keep under ~200 lines. generate-po ru,distribute-locales}.js` (generate-po reported 335/339 but 9 were genuinely new — its count metric differs). - yarn classic 1.22.22; ran gate via node_modules/.bin (prettier/eslint/vitest/ - tsup). yarn.lock unchanged, no .yarn/.yarnrc.yml. NB: working tree already - carried UNCOMMITTED task-024 + task-025 changes (showToast, dashboard/diag/ - manager styles+renders, section.js, netshift.js, #cbi-netshift-section CSS) — - so `git diff -- src` is large but only my settings block + the 4 catalogs + - main.js belong to task-026. format reported all-unchanged → no new churn. + tsup). yarn.lock unchanged, no .yarn/.yarnrc.yml. NB: working tree already + carried UNCOMMITTED task-024 + task-025 changes (showToast, dashboard/diag/ + manager styles+renders, section.js, netshift.js, #cbi-netshift-section CSS) — + so `git diff -- src` is large but only my settings block + the 4 catalogs + + main.js belong to task-026. format reported all-unchanged → no new churn. + +## task-030 — NetShift update check ON-DEMAND (retires C1's systemInfo-refresh) + +- REVERSES task-018's C1 decision. task-029 (backend, APPROVED) ADDED a real + `component_action netshift check_update` action returning the STANDARD check + JSON `{success,current_version,latest_version,status}` (v-normalized + server-side, SAME shape as the sing-box cores) AND removed the latest-fetch + from `get_system_info` (now returns `netshift_latest_version:"unknown"`). So + the NetShift card is now a TRUE peer of the cores: on-demand check writes + `managerChecks.netshift`, mount does NO network check. +- SHELL METHOD: added `netshiftCheckUpdate()` to `methods/shell/index.ts` — + copy of `singBoxCheckUpdate` but args `['component_action','netshift', + 'check_update']`, parsed by the EXISTING `parseComponentCheckUpdate`, returns + `NetShift.ComponentCheckUpdateResult`. SYNC path (fast call), timeout 600000. + It's a PROPERTY on `NetShiftShellMethods` (not a top-level export) → NO new + symbol in the baseclass.extend export block (verified byte-identical to HEAD). +- runNetshiftCheck (manager/initController.ts): now MIRRORS runSingBoxCheck + exactly — call `netshiftCheckUpdate()`, `if(!parsed.success)` error toast + + return, `status=parsed.status??null`, `setCheckResult('netshift',status, + parsed.latest_version||'')`, `showToast(getCheckToastMessage(status), + 'success')`, catch→error toast, finally→reset loading. STOPPED calling + `fetchSystemInfo()`+`resetCheckResult` as the "check". +- cards.ts: `netshiftStatus(systemInfo, check)` now RETURNS `check.status` + (the on-demand result; null until checked → neutral). KEPT the dev guard + (`normalizeCompiledVersion(...)==='dev' → null`). REMOVED the + `installed===latest` string compare AND the systemInfo.netshift_latest_version + dependency. `netshiftCard` takes the check too; "Install %s" `latest` now + comes from `check.latest_version`. `getComponentCards` passes `checks.netshift` + to `netshiftCard`. The `check_netshift` kind NOW carries + `backendAction:'check_update'` (still a DISTINCT kind so the dispatcher routes + it to runNetshiftCheck, never to the sing-box check method). +- DIAGNOSTIC: NO code change needed. `getNetshiftVersionRow.ts` already treats + `netshift_latest_version === 'unknown'` (and `'loading'`) as + `!hasActualVersion` → returns the plain neutral row (no Outdated/Latest tag). + Since task-029 makes the backend return "unknown", the row auto-degrades to + neutral. Mount's `fetchSystemInfo()` is now network-free (backend change), so + diagnostic entry triggers no GitHub call. Existing + getNetshiftVersionRow.test.ts (passes real versions) stays green unchanged. +- TESTS: rewrote the 5 NetShift cases in manager/tests/cards.test.js to derive + from `managerChecks.netshift` instead of systemInfo: null-status→neutral+ + check_update; check 'outdated'→self_update + Install <check.latest_version>; + 'latest'→Latest badge; dev-build stays neutral even with a check 'outdated'; + systemInfo latest mismatch is IGNORED. 472 tests pass (cards 19). +- LOCALES: removing the `runNetshiftCheck` body ORPHANED `_('Latest version is + unknown')` (no longer referenced anywhere). Ran `node {extract-calls, + generate-pot,generate-po ru,distribute-locales}.js`. msgid delta = PURELY the + 1 removed msgid (calls.json/pot/ru.po) + `#:` line-ref reshuffle + POT header + date. fe↔luci pairs byte-identical (diff -q). No new strings added (all toasts + reused existing msgids). generate-po reported 340/338 (2 stale retained). +- main.js: +36/-26 runtime diff = exactly (new method block + netshiftStatus/ + Card signature change + runNetshiftCheck rewrite). IDEMPOTENT (md5 + 9ce13d2… across 2 builds), banner + `return baseclass.extend({` intact, + export block byte-identical to HEAD. yarn classic 1.22.22; ran via + node_modules/.bin; yarn.lock unchanged, no .yarn/.yarnrc.yml. +- FLAG (no browser in env): the neutral→checked card transition + toast were + verified by reasoning + the pure cards.test.js, NOT screenshotted. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 85d3837b..b3d7687d 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -777,3 +777,52 @@ findings; keep under ~200 lines. piped-while subshell counter quirk). Registered all 5 points (all)/case alias/ usage line/docker-compose comment). shellcheck -S error clean (bin+libs+ install.sh); `smoke-tests all` = 120 passed / 0 failed (110 baseline + 10 new). + +## task-029: NetShift latest-version check on-demand (stop auto-fetch in get_system_info) + +- Root cause: `get_system_info` (bin/netshift) did `curl -m 3 ... releases/latest` + on EVERY call, and the UI calls it on Manager/Diagnostic mount → a GitHub hit + on every page load. Cores do it right via on-demand `component_action`. +- Fix 1 (get_system_info): REMOVED the curl + the `[ -z ] && unknown` line; + replaced with the constant `netshift_latest_version="unknown"`. The KEY stays + (frontend type + global_check jq read it; "unknown" is the zero-network + sentinel the UI understands). Function now does ZERO network I/O. +- Fix 2 (new worker `updates_check_netshift`, updater.sh, right after + `updates_check_sing_box_stable`): SYNC component_action worker, NEVER exits + (echo JSON; return N). Reused the EXISTING shared helper + `updates_netshift_latest_tag` (already there from task-017: `updates_http_get_once + "$NETSHIFT_RELEASE_API_URL"` then `grep '"tag_name":' | head -n1 | cut -d'"' -f4`) + — did NOT add a new helper or a new curl. Empty tag → `{"success":false, + "message":"..."}` return 1 (mirrors stable "feed unreachable"). v-normalization: + `cur_norm="${current_version#v}"; latest_norm="${latest#v}"` then leading-semver + `${x%%-*}`, compared via `is_min_package_version` (sort -V `>=`) → latest/outdated. + JSON shape IDENTICAL to `updates_check_sing_box_stable` + (`success/current_version/latest_version/status`) so frontend + `parseComponentCheckUpdate` is unchanged. +- DEV-BUILD decision: if `$NETSHIFT_VERSION` is the unstamped placeholder + (`*COMPILED*` — it's `__COMPILED_VERSION_VARIABLE__`), report `status:"latest"` + (a dev build is never "outdated"; UI also guards dev separately). It STILL + fetches the real tag for display, and STILL returns success:false on an + unreachable feed (honest failure even for dev). +- Router: added `netshift:check_update) updates_check_netshift ;;` next to the + existing `netshift:self_update` in `component_action()`. NO ACL change + (component_action is wholesale exec-allowed); NO new top-level command. +- Fix 3 (global_check, bin/netshift): since get_system_info no longer carries the + real latest, global_check now calls `updates_netshift_latest_tag` itself + (`netshift_latest_version=$(updates_netshift_latest_tag); [ -z ] && ="unknown"`) + so the `🕳️ NetShift: <ver> (latest: <latest>)` line still shows the true latest. + A network call here is fine — one-shot SSH diagnostic, not the UI mount path. + updater.sh is sourced by bin/netshift so the helper is in scope. +- Constant: `NETSHIFT_RELEASE_API_URL` already existed (task-017) — reused, none + added. +- Smoke: NEW top-level `test_check_update_netshift` (alias `netshiftcheck`, 7 + assertions). Part A = STATIC awk-extract of `get_system_info` body + assert NO + `releases/latest` and `netshift_latest_version="unknown"` present. Part B = + driver sources updater.sh+helpers.sh, silences log, OVERRIDES + `updates_netshift_latest_tag` + sets `NETSHIFT_VERSION` (mirrors the + test_check_update_extended stub style), runs the worker. Cases: v0.8.6 vs 0.8.6 + →latest; 0.8.5 vs 0.8.6→outdated; v0.8.6 vs v0.8.6→latest; JSON-shape has-keys; + empty tag→success:false; placeholder dev-build→latest. Registered all 5 points + (all)/case alias/usage line/docker-compose comment). shellcheck -S error clean + (bin+libs+install.sh); `smoke-tests all` = 127 passed / 0 failed (120 baseline + + 7 new). NO sacred constant/port/mark/path/ACL/frontend change. diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index bfc989dd..fcc2853a 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -182,9 +182,9 @@ "call": "Check update", "key": "Check update", "places": [ - "src/netshift/tabs/manager/cards.ts:143", - "src/netshift/tabs/manager/cards.ts:179", - "src/netshift/tabs/manager/cards.ts:225" + "src/netshift/tabs/manager/cards.ts:144", + "src/netshift/tabs/manager/cards.ts:181", + "src/netshift/tabs/manager/cards.ts:227" ] }, { @@ -780,11 +780,12 @@ "src/netshift/tabs/diagnostic/initController.ts:308", "src/netshift/tabs/manager/initController.ts:122", "src/netshift/tabs/manager/initController.ts:132", - "src/netshift/tabs/manager/initController.ts:166", - "src/netshift/tabs/manager/initController.ts:194", - "src/netshift/tabs/manager/initController.ts:198", - "src/netshift/tabs/manager/initController.ts:231", - "src/netshift/tabs/manager/initController.ts:235" + "src/netshift/tabs/manager/initController.ts:150", + "src/netshift/tabs/manager/initController.ts:160", + "src/netshift/tabs/manager/initController.ts:188", + "src/netshift/tabs/manager/initController.ts:192", + "src/netshift/tabs/manager/initController.ts:225", + "src/netshift/tabs/manager/initController.ts:229" ] }, { @@ -864,9 +865,9 @@ "call": "Install %s", "key": "Install %s", "places": [ - "src/netshift/tabs/manager/cards.ts:135", - "src/netshift/tabs/manager/cards.ts:172", - "src/netshift/tabs/manager/cards.ts:218" + "src/netshift/tabs/manager/cards.ts:136", + "src/netshift/tabs/manager/cards.ts:174", + "src/netshift/tabs/manager/cards.ts:220" ] }, { @@ -1301,13 +1302,6 @@ "src/netshift/tabs/manager/initController.ts:103" ] }, - { - "call": "Latest version is unknown", - "key": "Latest version is unknown", - "places": [ - "src/netshift/tabs/manager/initController.ts:155" - ] - }, { "call": "List Update Frequency", "key": "List Update Frequency", @@ -1417,7 +1411,7 @@ "call": "NetShift updated, version:", "key": "NetShift updated, version:", "places": [ - "src/netshift/tabs/manager/initController.ts:223" + "src/netshift/tabs/manager/initController.ts:217" ] }, { @@ -1460,8 +1454,8 @@ "key": "Not installed", "places": [ "src/netshift/tabs/manager/cards.ts:98", - "src/netshift/tabs/manager/cards.ts:196", - "src/netshift/tabs/manager/cards.ts:241", + "src/netshift/tabs/manager/cards.ts:198", + "src/netshift/tabs/manager/cards.ts:243", "src/netshift/tabs/manager/initController.ts:100" ] }, @@ -1852,7 +1846,7 @@ "call": "Self-update failed", "key": "Self-update failed", "places": [ - "src/netshift/methods/shell/index.ts:230" + "src/netshift/methods/shell/index.ts:253" ] }, { @@ -1902,7 +1896,7 @@ "call": "Sing-box core changed, version:", "key": "Sing-box core changed, version:", "places": [ - "src/netshift/tabs/manager/initController.ts:187" + "src/netshift/tabs/manager/initController.ts:181" ] }, { @@ -2044,21 +2038,21 @@ "call": "Switch to extended", "key": "Switch to extended", "places": [ - "src/netshift/tabs/manager/cards.ts:233" + "src/netshift/tabs/manager/cards.ts:235" ] }, { "call": "Switch to stable", "key": "Switch to stable", "places": [ - "src/netshift/tabs/manager/cards.ts:188" + "src/netshift/tabs/manager/cards.ts:190" ] }, { "call": "Switching sing-box core, this may take a few minutes…", "key": "Switching sing-box core, this may take a few minutes…", "places": [ - "src/netshift/tabs/manager/initController.ts:177" + "src/netshift/tabs/manager/initController.ts:171" ] }, { @@ -2199,14 +2193,12 @@ "src/netshift/tabs/diagnostic/initController.ts:42", "src/netshift/tabs/diagnostic/initController.ts:43", "src/netshift/tabs/diagnostic/initController.ts:44", - "src/netshift/tabs/manager/cards.ts:113", "src/netshift/tabs/manager/initController.ts:37", "src/netshift/tabs/manager/initController.ts:38", "src/netshift/tabs/manager/initController.ts:39", "src/netshift/tabs/manager/initController.ts:40", "src/netshift/tabs/manager/initController.ts:41", "src/netshift/tabs/manager/initController.ts:42", - "src/netshift/tabs/manager/initController.ts:154", "src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7" ] }, @@ -2221,8 +2213,8 @@ "call": "Update", "key": "Update", "places": [ - "src/netshift/tabs/manager/cards.ts:172", - "src/netshift/tabs/manager/cards.ts:218" + "src/netshift/tabs/manager/cards.ts:174", + "src/netshift/tabs/manager/cards.ts:220" ] }, { @@ -2236,14 +2228,14 @@ "call": "Update NetShift", "key": "Update NetShift", "places": [ - "src/netshift/tabs/manager/cards.ts:136" + "src/netshift/tabs/manager/cards.ts:137" ] }, { "call": "Updating NetShift, this may take a few minutes; the page will reload…", "key": "Updating NetShift, this may take a few minutes; the page will reload…", "places": [ - "src/netshift/tabs/manager/initController.ts:214" + "src/netshift/tabs/manager/initController.ts:208" ] }, { @@ -2396,7 +2388,7 @@ "call": "Version", "key": "Version", "places": [ - "src/netshift/tabs/manager/initController.ts:312" + "src/netshift/tabs/manager/initController.ts:306" ] }, { diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index 72d86fbc..869e0d99 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 08:56+0300\n" -"PO-Revision-Date: 2026-06-07 08:56+0300\n" +"POT-Creation-Date: 2026-06-07 16:26+0300\n" +"PO-Revision-Date: 2026-06-07 16:26+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -120,9 +120,9 @@ msgstr "" msgid "Cannot receive checks result" msgstr "" -#: src/netshift/tabs/manager/cards.ts:143 -#: src/netshift/tabs/manager/cards.ts:179 -#: src/netshift/tabs/manager/cards.ts:225 +#: src/netshift/tabs/manager/cards.ts:144 +#: src/netshift/tabs/manager/cards.ts:181 +#: src/netshift/tabs/manager/cards.ts:227 msgid "Check update" msgstr "" @@ -472,11 +472,12 @@ msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:308 #: src/netshift/tabs/manager/initController.ts:122 #: src/netshift/tabs/manager/initController.ts:132 -#: src/netshift/tabs/manager/initController.ts:166 -#: src/netshift/tabs/manager/initController.ts:194 -#: src/netshift/tabs/manager/initController.ts:198 -#: src/netshift/tabs/manager/initController.ts:231 -#: src/netshift/tabs/manager/initController.ts:235 +#: src/netshift/tabs/manager/initController.ts:150 +#: src/netshift/tabs/manager/initController.ts:160 +#: src/netshift/tabs/manager/initController.ts:188 +#: src/netshift/tabs/manager/initController.ts:192 +#: src/netshift/tabs/manager/initController.ts:225 +#: src/netshift/tabs/manager/initController.ts:229 msgid "Failed to execute!" msgstr "" @@ -523,9 +524,9 @@ msgstr "" msgid "Include servers by keyword" msgstr "" -#: src/netshift/tabs/manager/cards.ts:135 -#: src/netshift/tabs/manager/cards.ts:172 -#: src/netshift/tabs/manager/cards.ts:218 +#: src/netshift/tabs/manager/cards.ts:136 +#: src/netshift/tabs/manager/cards.ts:174 +#: src/netshift/tabs/manager/cards.ts:220 msgid "Install %s" msgstr "" @@ -778,10 +779,6 @@ msgstr "" msgid "Latest version is installed" msgstr "" -#: src/netshift/tabs/manager/initController.ts:155 -msgid "Latest version is unknown" -msgstr "" - #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:327 msgid "List Update Frequency" msgstr "" @@ -842,7 +839,7 @@ msgstr "" msgid "NetShift Settings" msgstr "" -#: src/netshift/tabs/manager/initController.ts:223 +#: src/netshift/tabs/manager/initController.ts:217 msgid "NetShift updated, version:" msgstr "" @@ -867,8 +864,8 @@ msgid "Not implement yet" msgstr "" #: src/netshift/tabs/manager/cards.ts:98 -#: src/netshift/tabs/manager/cards.ts:196 -#: src/netshift/tabs/manager/cards.ts:241 +#: src/netshift/tabs/manager/cards.ts:198 +#: src/netshift/tabs/manager/cards.ts:243 #: src/netshift/tabs/manager/initController.ts:100 msgid "Not installed" msgstr "" @@ -1097,7 +1094,7 @@ msgstr "" msgid "Selector Proxy Links" msgstr "" -#: src/netshift/methods/shell/index.ts:230 +#: src/netshift/methods/shell/index.ts:253 msgid "Self-update failed" msgstr "" @@ -1126,7 +1123,7 @@ msgstr "" msgid "Sing-box autostart disabled" msgstr "" -#: src/netshift/tabs/manager/initController.ts:187 +#: src/netshift/tabs/manager/initController.ts:181 msgid "Sing-box core changed, version:" msgstr "" @@ -1208,15 +1205,15 @@ msgstr "" msgid "Successfully copied!" msgstr "" -#: src/netshift/tabs/manager/cards.ts:233 +#: src/netshift/tabs/manager/cards.ts:235 msgid "Switch to extended" msgstr "" -#: src/netshift/tabs/manager/cards.ts:188 +#: src/netshift/tabs/manager/cards.ts:190 msgid "Switch to stable" msgstr "" -#: src/netshift/tabs/manager/initController.ts:177 +#: src/netshift/tabs/manager/initController.ts:171 msgid "Switching sing-box core, this may take a few minutes…" msgstr "" @@ -1300,14 +1297,12 @@ msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:42 #: src/netshift/tabs/diagnostic/initController.ts:43 #: src/netshift/tabs/diagnostic/initController.ts:44 -#: src/netshift/tabs/manager/cards.ts:113 #: src/netshift/tabs/manager/initController.ts:37 #: src/netshift/tabs/manager/initController.ts:38 #: src/netshift/tabs/manager/initController.ts:39 #: src/netshift/tabs/manager/initController.ts:40 #: src/netshift/tabs/manager/initController.ts:41 #: src/netshift/tabs/manager/initController.ts:42 -#: src/netshift/tabs/manager/initController.ts:154 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7 msgid "unknown" msgstr "" @@ -1316,8 +1311,8 @@ msgstr "" msgid "Unknown error" msgstr "" -#: src/netshift/tabs/manager/cards.ts:172 -#: src/netshift/tabs/manager/cards.ts:218 +#: src/netshift/tabs/manager/cards.ts:174 +#: src/netshift/tabs/manager/cards.ts:220 msgid "Update" msgstr "" @@ -1325,11 +1320,11 @@ msgstr "" msgid "Update is available" msgstr "" -#: src/netshift/tabs/manager/cards.ts:136 +#: src/netshift/tabs/manager/cards.ts:137 msgid "Update NetShift" msgstr "" -#: src/netshift/tabs/manager/initController.ts:214 +#: src/netshift/tabs/manager/initController.ts:208 msgid "Updating NetShift, this may take a few minutes; the page will reload…" msgstr "" @@ -1425,7 +1420,7 @@ msgstr "" msgid "Validation errors:" msgstr "" -#: src/netshift/tabs/manager/initController.ts:312 +#: src/netshift/tabs/manager/initController.ts:306 msgid "Version" msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index c2068c4b..8eb77640 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 11:56+0300\n" -"PO-Revision-Date: 2026-06-07 11:56+0300\n" +"POT-Creation-Date: 2026-06-07 19:26+0300\n" +"PO-Revision-Date: 2026-06-07 19:26+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -557,9 +557,6 @@ msgstr "Последняя" msgid "Latest version is installed" msgstr "Установлена последняя версия" -msgid "Latest version is unknown" -msgstr "Последняя версия неизвестна" - msgid "List Update Frequency" msgstr "Частота обновления списков" diff --git a/fe-app-netshift/src/netshift/methods/shell/index.ts b/fe-app-netshift/src/netshift/methods/shell/index.ts index 88a8242f..e12322fd 100644 --- a/fe-app-netshift/src/netshift/methods/shell/index.ts +++ b/fe-app-netshift/src/netshift/methods/shell/index.ts @@ -197,6 +197,29 @@ export const NetShiftShellMethods = { message: response.stderr || '', }; }, + // NetShift update check (sync) — task-029/030 contract: + // component_action netshift check_update + // → {success, current_version, latest_version, status}. Same shape as the + // sing-box cores (parsed by parseComponentCheckUpdate). The status is already + // v-normalized server-side, so the caller TRUSTS result.status (no string + // compare in TS). Stays on the SYNC component_action path (fast call). + netshiftCheckUpdate: + async (): Promise<NetShift.ComponentCheckUpdateResult> => { + const response = await executeShellCommand({ + command: '/usr/bin/netshift', + args: ['component_action', 'netshift', 'check_update'], + timeout: 600000, + }); + + if (response.stdout) { + return parseComponentCheckUpdate(response.stdout); + } + + return { + success: false, + message: response.stderr || '', + }; + }, // NetShift self-update (async) — STABLE task-017 contract: // component_action_async netshift self_update + component_action_status <job>. // Reuses the component-agnostic poll. Because the package install swaps diff --git a/fe-app-netshift/src/netshift/tabs/manager/cards.ts b/fe-app-netshift/src/netshift/tabs/manager/cards.ts index 349a0a15..3f168bb9 100644 --- a/fe-app-netshift/src/netshift/tabs/manager/cards.ts +++ b/fe-app-netshift/src/netshift/tabs/manager/cards.ts @@ -7,11 +7,11 @@ export type ManagerComponentKey = | 'sing_box_extended'; // `check` = a sing-box update check (routed to the sing-box check method); -// `check_netshift` = the NetShift card's on-demand check, which is a -// systemInfo REFRESH (the backend has NO netshift:check_update action — the -// NetShift latest version comes only from get_system_info.netshift_latest_version). -// Keeping it a DISTINCT kind guarantees a NetShift check can never be routed to -// the sing-box check method. +// `check_netshift` = the NetShift card's on-demand check (task-030), which now +// calls the dedicated `component_action netshift check_update` action and writes +// `managerChecks.netshift` — exactly like the sing-box cores. Keeping it a +// DISTINCT kind guarantees a NetShift check can never be routed to the sing-box +// check method (the dispatcher routes it to runNetshiftCheck). export type ManagerActionKind = | 'check' | 'check_netshift' @@ -31,8 +31,8 @@ export interface ManagerActionDescriptor { kind: ManagerActionKind; text: string; // For `update`/`switch`: the backend install action; for `self_update`: - // 'self_update'; for `check`: the sing-box check action. The NetShift - // `check_netshift` kind has NO backend action (it just refreshes systemInfo). + // 'self_update'; for `check`: the sing-box check action; for `check_netshift`: + // the NetShift check action (routed to the dedicated NetShift check method). backendAction?: | 'check_update' | 'check_update_stable' @@ -101,39 +101,40 @@ export function getCheckTag( return { label: _('Dev'), kind: 'neutral' }; } -// NetShift status is derived PURELY from systemInfo (installed vs latest). -// There is no NetShift check write into managerChecks — the on-demand check is -// a systemInfo refresh, after which this re-derives. +// NetShift status is derived from the on-demand check result (task-030): +// `managerChecks.netshift.status` is null until the user presses "Check update" +// → neutral card (no badge, no update button). The backend already computes the +// v-normalized status, so we TRUST it (no installed-vs-latest string compare). +// The `dev`-build guard is kept locally: a dev/placeholder build never shows an +// update prompt regardless of any check result. function netshiftStatus( systemInfo: ManagerSystemInfo, + check: ManagerCheckState, ): NetShift.ComponentUpdateStatus | null { const installed = normalizeCompiledVersion(systemInfo.netshift_version); - const latest = systemInfo.netshift_latest_version; - - if (!latest || latest === 'loading' || latest === _('unknown')) { - return null; - } if (installed === 'dev') { return null; } - return installed === latest ? 'latest' : 'outdated'; + return check.status; } -function netshiftCard(systemInfo: ManagerSystemInfo): ManagerCardDescriptor { - const status = netshiftStatus(systemInfo); - const latest = systemInfo.netshift_latest_version; +function netshiftCard( + systemInfo: ManagerSystemInfo, + check: ManagerCheckState, +): ManagerCardDescriptor { + const status = netshiftStatus(systemInfo, check); + const latest = check.latest_version; const actions: ManagerActionDescriptor[] = []; if (status === 'outdated') { actions.push({ loadingKey: 'netshiftUpdate', kind: 'self_update', - text: - latest && latest !== 'loading' - ? _('Install %s').replace('%s', latest) - : _('Update NetShift'), + text: latest + ? _('Install %s').replace('%s', latest) + : _('Update NetShift'), backendAction: 'self_update', }); } else { @@ -141,6 +142,7 @@ function netshiftCard(systemInfo: ManagerSystemInfo): ManagerCardDescriptor { loadingKey: 'netshiftCheck', kind: 'check_netshift', text: _('Check update'), + backendAction: 'check_update', }); } @@ -255,7 +257,7 @@ export function getComponentCards( checks: Record<ManagerComponentKey, ManagerCheckState>, ): ManagerCardDescriptor[] { return [ - netshiftCard(systemInfo), + netshiftCard(systemInfo, checks.netshift), singBoxStockCard(systemInfo, checks.sing_box_stock), singBoxExtendedCard(systemInfo, checks.sing_box_extended), ]; diff --git a/fe-app-netshift/src/netshift/tabs/manager/initController.ts b/fe-app-netshift/src/netshift/tabs/manager/initController.ts index 683634d5..427c03fa 100644 --- a/fe-app-netshift/src/netshift/tabs/manager/initController.ts +++ b/fe-app-netshift/src/netshift/tabs/manager/initController.ts @@ -135,32 +135,26 @@ async function runSingBoxCheck( } } -// NetShift check: the backend has NO netshift:check_update action — NetShift's -// latest version comes only from get_system_info.netshift_latest_version. So an -// on-demand NetShift check is a systemInfo REFRESH; the card then re-derives its -// status from the refreshed installed-vs-latest comparison. We never write a -// sing-box check result into managerChecks.netshift. +// NetShift check (task-030): on-demand call to the dedicated +// `component_action netshift check_update` action, which returns the same +// {success, current_version, latest_version, status} contract as the sing-box +// cores (status already v-normalized server-side). We TRUST result.status and +// write it into managerChecks.netshift — mirroring runSingBoxCheck precisely. async function runNetshiftCheck(button: ManagerActionDescriptor) { setActionLoading(button.loadingKey, true); try { - await fetchSystemInfo(); - resetCheckResult('netshift'); + const parsed = await NetShiftShellMethods.netshiftCheckUpdate(); - const status = store.get().diagnosticsSystemInfo; - const installed = normalizeCompiledVersion(status.netshift_version); - const latest = status.netshift_latest_version; - - if (!latest || latest === 'loading' || latest === _('unknown')) { - showToast(_('Latest version is unknown'), 'success'); - } else if (installed === 'dev') { - showToast(getCheckToastMessage('dev'), 'success'); - } else { - showToast( - getCheckToastMessage(installed === latest ? 'latest' : 'outdated'), - 'success', - ); + if (!parsed.success) { + showToast(parsed.message || _('Failed to execute!'), 'error'); + return; } + + const status = parsed.status ?? null; + + setCheckResult('netshift', status, parsed.latest_version || ''); + showToast(getCheckToastMessage(status), 'success'); } catch (error) { logger.error('[MANAGER]', 'runNetshiftCheck failed', error); showToast(_('Failed to execute!'), 'error'); diff --git a/fe-app-netshift/src/netshift/tabs/manager/tests/cards.test.js b/fe-app-netshift/src/netshift/tabs/manager/tests/cards.test.js index 870f8a1f..e5d69486 100644 --- a/fe-app-netshift/src/netshift/tabs/manager/tests/cards.test.js +++ b/fe-app-netshift/src/netshift/tabs/manager/tests/cards.test.js @@ -120,13 +120,30 @@ describe('getComponentCards', () => { expect(stock.actions[0].text).toBe('Install 1.12.9'); }); - it('derives an outdated NetShift card from systemInfo latest mismatch', () => { + it('is neutral until checked — null managerChecks.netshift status', () => { + // task-030: mount does NO network check; managerChecks.netshift.status is + // null → no badge, the "Check update" action (no outdated/update button). + const cards = getComponentCards(makeSystemInfo(), emptyChecks); + const netshift = cards[0]; + + expect(netshift.tag).toBeUndefined(); + expect(netshift.actions[0].kind).toBe('check_netshift'); + expect(netshift.actions[0].backendAction).toBe('check_update'); + }); + + it('derives an outdated NetShift card from the on-demand check result', () => { + // task-030: status comes from managerChecks.netshift (the check result), NOT + // from a systemInfo installed-vs-latest string compare. The latest_version + // for the "Install %s" text also comes from the check result. const cards = getComponentCards( makeSystemInfo({ netshift_version: '1.0.0', - netshift_latest_version: '1.1.0', + netshift_latest_version: '1.0.0', }), - emptyChecks, + { + ...emptyChecks, + netshift: { status: 'outdated', latest_version: '1.1.0' }, + }, ); const netshift = cards[0]; @@ -136,14 +153,11 @@ describe('getComponentCards', () => { expect(netshift.actions[0].text).toBe('Install 1.1.0'); }); - it('keeps the NetShift card on Check update when versions match', () => { - const cards = getComponentCards( - makeSystemInfo({ - netshift_version: '1.1.0', - netshift_latest_version: '1.1.0', - }), - emptyChecks, - ); + it('shows the Latest badge + Check update when the check says latest', () => { + const cards = getComponentCards(makeSystemInfo(), { + ...emptyChecks, + netshift: { status: 'latest', latest_version: '1.0.0' }, + }); const netshift = cards[0]; expect(netshift.tag).toEqual({ label: 'Latest', kind: 'success' }); @@ -152,34 +166,25 @@ describe('getComponentCards', () => { expect(netshift.actions[0].kind).toBe('check_netshift'); }); - it('NetShift check action carries NO sing-box backendAction', () => { - // C1 regression guard: the NetShift "Check update" must never be a sing-box - // check (the backend has no netshift:check_update action). Its action has no - // backendAction at all — it triggers a systemInfo refresh in the controller. - const cards = getComponentCards( - makeSystemInfo({ - netshift_version: '1.0.0', - netshift_latest_version: '1.0.0', - }), - emptyChecks, - ); + it('NetShift check action carries its own (non-sing-box) backendAction', () => { + // The NetShift "Check update" routes to runNetshiftCheck (distinct kind) and + // calls `component_action netshift check_update` — NOT a sing-box check + // action. Guard against accidentally reusing a sing-box check action. + const cards = getComponentCards(makeSystemInfo(), emptyChecks); const netshift = cards[0]; expect(netshift.actions[0].kind).toBe('check_netshift'); - expect(netshift.actions[0].backendAction).toBeUndefined(); - expect(['check_update', 'check_update_stable']).not.toContain( + expect(netshift.actions[0].backendAction).toBe('check_update'); + expect(['check_update_stable']).not.toContain( netshift.actions[0].backendAction, ); }); - it('derives NetShift status purely from systemInfo, ignoring managerChecks', () => { - // Even if a (bogus) sing-box-style status leaked into managerChecks.netshift, - // the NetShift card must derive its status from systemInfo versions only. + it('keeps a dev build neutral even if a check result says outdated', () => { + // The dev-build guard: a placeholder/dev install never shows an update + // prompt regardless of any check result. const cards = getComponentCards( - makeSystemInfo({ - netshift_version: '1.0.0', - netshift_latest_version: '1.0.0', - }), + makeSystemInfo({ netshift_version: 'COMPILED_VERSION' }), { ...emptyChecks, netshift: { status: 'outdated', latest_version: '9.9.9' }, @@ -187,15 +192,18 @@ describe('getComponentCards', () => { ); const netshift = cards[0]; - expect(netshift.tag).toEqual({ label: 'Latest', kind: 'success' }); + expect(netshift.version).toBe('dev'); + expect(netshift.tag).toBeUndefined(); expect(netshift.actions[0].kind).toBe('check_netshift'); }); - it('treats an unknown NetShift latest as no status (Check update, no badge)', () => { + it('ignores systemInfo netshift_latest_version for status (now on-demand)', () => { + // task-030: a stale/unknown systemInfo latest must NOT drive the badge — only + // the on-demand managerChecks.netshift result does. const cards = getComponentCards( makeSystemInfo({ netshift_version: '1.0.0', - netshift_latest_version: 'unknown', + netshift_latest_version: '9.9.9', }), emptyChecks, ); diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index d653f238..4a046044 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -1036,6 +1036,26 @@ var NetShiftShellMethods = { message: response.stderr || "" }; }, + // NetShift update check (sync) — task-029/030 contract: + // component_action netshift check_update + // → {success, current_version, latest_version, status}. Same shape as the + // sing-box cores (parsed by parseComponentCheckUpdate). The status is already + // v-normalized server-side, so the caller TRUSTS result.status (no string + // compare in TS). Stays on the SYNC component_action path (fast call). + netshiftCheckUpdate: async () => { + const response = await executeShellCommand({ + command: "/usr/bin/netshift", + args: ["component_action", "netshift", "check_update"], + timeout: 6e5 + }); + if (response.stdout) { + return parseComponentCheckUpdate(response.stdout); + } + return { + success: false, + message: response.stderr || "" + }; + }, // NetShift self-update (async) — STABLE task-017 contract: // component_action_async netshift self_update + component_action_status <job>. // Reuses the component-agnostic poll. Because the package install swaps @@ -5126,33 +5146,30 @@ function getCheckTag(status) { } return { label: _("Dev"), kind: "neutral" }; } -function netshiftStatus(systemInfo) { +function netshiftStatus(systemInfo, check) { const installed = normalizeCompiledVersion(systemInfo.netshift_version); - const latest = systemInfo.netshift_latest_version; - if (!latest || latest === "loading" || latest === _("unknown")) { - return null; - } if (installed === "dev") { return null; } - return installed === latest ? "latest" : "outdated"; + return check.status; } -function netshiftCard(systemInfo) { - const status = netshiftStatus(systemInfo); - const latest = systemInfo.netshift_latest_version; +function netshiftCard(systemInfo, check) { + const status = netshiftStatus(systemInfo, check); + const latest = check.latest_version; const actions = []; if (status === "outdated") { actions.push({ loadingKey: "netshiftUpdate", kind: "self_update", - text: latest && latest !== "loading" ? _("Install %s").replace("%s", latest) : _("Update NetShift"), + text: latest ? _("Install %s").replace("%s", latest) : _("Update NetShift"), backendAction: "self_update" }); } else { actions.push({ loadingKey: "netshiftCheck", kind: "check_netshift", - text: _("Check update") + text: _("Check update"), + backendAction: "check_update" }); } return { @@ -5242,7 +5259,7 @@ function singBoxExtendedCard(systemInfo, check) { } function getComponentCards(systemInfo, checks) { return [ - netshiftCard(systemInfo), + netshiftCard(systemInfo, checks.netshift), singBoxStockCard(systemInfo, checks.sing_box_stock), singBoxExtendedCard(systemInfo, checks.sing_box_extended) ]; @@ -5342,21 +5359,14 @@ async function runSingBoxCheck2(component, button) { async function runNetshiftCheck(button) { setActionLoading(button.loadingKey, true); try { - await fetchSystemInfo2(); - resetCheckResult("netshift"); - const status = store.get().diagnosticsSystemInfo; - const installed = normalizeCompiledVersion(status.netshift_version); - const latest = status.netshift_latest_version; - if (!latest || latest === "loading" || latest === _("unknown")) { - showToast(_("Latest version is unknown"), "success"); - } else if (installed === "dev") { - showToast(getCheckToastMessage("dev"), "success"); - } else { - showToast( - getCheckToastMessage(installed === latest ? "latest" : "outdated"), - "success" - ); + const parsed = await NetShiftShellMethods.netshiftCheckUpdate(); + if (!parsed.success) { + showToast(parsed.message || _("Failed to execute!"), "error"); + return; } + const status = parsed.status ?? null; + setCheckResult("netshift", status, parsed.latest_version || ""); + showToast(getCheckToastMessage(status), "success"); } catch (error) { logger.error("[MANAGER]", "runNetshiftCheck failed", error); showToast(_("Failed to execute!"), "error"); diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index c2068c4b..8eb77640 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 11:56+0300\n" -"PO-Revision-Date: 2026-06-07 11:56+0300\n" +"POT-Creation-Date: 2026-06-07 19:26+0300\n" +"PO-Revision-Date: 2026-06-07 19:26+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -557,9 +557,6 @@ msgstr "Последняя" msgid "Latest version is installed" msgstr "Установлена последняя версия" -msgid "Latest version is unknown" -msgstr "Последняя версия неизвестна" - msgid "List Update Frequency" msgstr "Частота обновления списков" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index 72d86fbc..869e0d99 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 08:56+0300\n" -"PO-Revision-Date: 2026-06-07 08:56+0300\n" +"POT-Creation-Date: 2026-06-07 16:26+0300\n" +"PO-Revision-Date: 2026-06-07 16:26+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -120,9 +120,9 @@ msgstr "" msgid "Cannot receive checks result" msgstr "" -#: src/netshift/tabs/manager/cards.ts:143 -#: src/netshift/tabs/manager/cards.ts:179 -#: src/netshift/tabs/manager/cards.ts:225 +#: src/netshift/tabs/manager/cards.ts:144 +#: src/netshift/tabs/manager/cards.ts:181 +#: src/netshift/tabs/manager/cards.ts:227 msgid "Check update" msgstr "" @@ -472,11 +472,12 @@ msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:308 #: src/netshift/tabs/manager/initController.ts:122 #: src/netshift/tabs/manager/initController.ts:132 -#: src/netshift/tabs/manager/initController.ts:166 -#: src/netshift/tabs/manager/initController.ts:194 -#: src/netshift/tabs/manager/initController.ts:198 -#: src/netshift/tabs/manager/initController.ts:231 -#: src/netshift/tabs/manager/initController.ts:235 +#: src/netshift/tabs/manager/initController.ts:150 +#: src/netshift/tabs/manager/initController.ts:160 +#: src/netshift/tabs/manager/initController.ts:188 +#: src/netshift/tabs/manager/initController.ts:192 +#: src/netshift/tabs/manager/initController.ts:225 +#: src/netshift/tabs/manager/initController.ts:229 msgid "Failed to execute!" msgstr "" @@ -523,9 +524,9 @@ msgstr "" msgid "Include servers by keyword" msgstr "" -#: src/netshift/tabs/manager/cards.ts:135 -#: src/netshift/tabs/manager/cards.ts:172 -#: src/netshift/tabs/manager/cards.ts:218 +#: src/netshift/tabs/manager/cards.ts:136 +#: src/netshift/tabs/manager/cards.ts:174 +#: src/netshift/tabs/manager/cards.ts:220 msgid "Install %s" msgstr "" @@ -778,10 +779,6 @@ msgstr "" msgid "Latest version is installed" msgstr "" -#: src/netshift/tabs/manager/initController.ts:155 -msgid "Latest version is unknown" -msgstr "" - #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:327 msgid "List Update Frequency" msgstr "" @@ -842,7 +839,7 @@ msgstr "" msgid "NetShift Settings" msgstr "" -#: src/netshift/tabs/manager/initController.ts:223 +#: src/netshift/tabs/manager/initController.ts:217 msgid "NetShift updated, version:" msgstr "" @@ -867,8 +864,8 @@ msgid "Not implement yet" msgstr "" #: src/netshift/tabs/manager/cards.ts:98 -#: src/netshift/tabs/manager/cards.ts:196 -#: src/netshift/tabs/manager/cards.ts:241 +#: src/netshift/tabs/manager/cards.ts:198 +#: src/netshift/tabs/manager/cards.ts:243 #: src/netshift/tabs/manager/initController.ts:100 msgid "Not installed" msgstr "" @@ -1097,7 +1094,7 @@ msgstr "" msgid "Selector Proxy Links" msgstr "" -#: src/netshift/methods/shell/index.ts:230 +#: src/netshift/methods/shell/index.ts:253 msgid "Self-update failed" msgstr "" @@ -1126,7 +1123,7 @@ msgstr "" msgid "Sing-box autostart disabled" msgstr "" -#: src/netshift/tabs/manager/initController.ts:187 +#: src/netshift/tabs/manager/initController.ts:181 msgid "Sing-box core changed, version:" msgstr "" @@ -1208,15 +1205,15 @@ msgstr "" msgid "Successfully copied!" msgstr "" -#: src/netshift/tabs/manager/cards.ts:233 +#: src/netshift/tabs/manager/cards.ts:235 msgid "Switch to extended" msgstr "" -#: src/netshift/tabs/manager/cards.ts:188 +#: src/netshift/tabs/manager/cards.ts:190 msgid "Switch to stable" msgstr "" -#: src/netshift/tabs/manager/initController.ts:177 +#: src/netshift/tabs/manager/initController.ts:171 msgid "Switching sing-box core, this may take a few minutes…" msgstr "" @@ -1300,14 +1297,12 @@ msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:42 #: src/netshift/tabs/diagnostic/initController.ts:43 #: src/netshift/tabs/diagnostic/initController.ts:44 -#: src/netshift/tabs/manager/cards.ts:113 #: src/netshift/tabs/manager/initController.ts:37 #: src/netshift/tabs/manager/initController.ts:38 #: src/netshift/tabs/manager/initController.ts:39 #: src/netshift/tabs/manager/initController.ts:40 #: src/netshift/tabs/manager/initController.ts:41 #: src/netshift/tabs/manager/initController.ts:42 -#: src/netshift/tabs/manager/initController.ts:154 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7 msgid "unknown" msgstr "" @@ -1316,8 +1311,8 @@ msgstr "" msgid "Unknown error" msgstr "" -#: src/netshift/tabs/manager/cards.ts:172 -#: src/netshift/tabs/manager/cards.ts:218 +#: src/netshift/tabs/manager/cards.ts:174 +#: src/netshift/tabs/manager/cards.ts:220 msgid "Update" msgstr "" @@ -1325,11 +1320,11 @@ msgstr "" msgid "Update is available" msgstr "" -#: src/netshift/tabs/manager/cards.ts:136 +#: src/netshift/tabs/manager/cards.ts:137 msgid "Update NetShift" msgstr "" -#: src/netshift/tabs/manager/initController.ts:214 +#: src/netshift/tabs/manager/initController.ts:208 msgid "Updating NetShift, this may take a few minutes; the page will reload…" msgstr "" @@ -1425,7 +1420,7 @@ msgstr "" msgid "Validation errors:" msgstr "" -#: src/netshift/tabs/manager/initController.ts:312 +#: src/netshift/tabs/manager/initController.ts:306 msgid "Version" msgstr "" diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index afc853d8..5dda77dd 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -3601,8 +3601,13 @@ get_system_info() { netshift_version="$NETSHIFT_VERSION" - netshift_latest_version=$(curl -m 3 -s https://api.github.com/repos/yandexru45/netshift/releases/latest | grep '"tag_name":' | cut -d'"' -f4) - [ -z "$netshift_latest_version" ] && netshift_latest_version="unknown" + # On-demand only: get_system_info must do NO network I/O (the UI calls it on + # every Manager/Diagnostic mount). The real latest is fetched only by the + # on-demand "component_action netshift check_update" action (and by + # global_check, the one-shot SSH diagnostic). The key is kept for backward + # compatibility (frontend type + global_check jq read it); "unknown" is the + # zero-network sentinel the UI already understands. + netshift_latest_version="unknown" if [ -f /www/luci-static/resources/view/netshift/main.js ]; then luci_app_version=$(grep 'var NETSHIFT_LUCI_APP_VERSION' /www/luci-static/resources/view/netshift/main.js | cut -d'"' -f2) @@ -4114,7 +4119,11 @@ global_check() { local netshift_version netshift_latest_version luci_app_version sing_box_version openwrt_version device_model sing_box_extended netshift_version=$(echo "$system_info_json" | jq -r '.netshift_version // "unknown"') - netshift_latest_version=$(echo "$system_info_json" | jq -r '.netshift_latest_version // "unknown"') + # get_system_info no longer fetches the latest (it does no network I/O). + # global_check is a one-shot SSH diagnostic, so fetch the real latest here + # itself (shared helper); fall back to "unknown" if GitHub is unreachable. + netshift_latest_version=$(updates_netshift_latest_tag) + [ -z "$netshift_latest_version" ] && netshift_latest_version="unknown" luci_app_version=$(echo "$system_info_json" | jq -r '.luci_app_version // "unknown"') sing_box_version=$(echo "$system_info_json" | jq -r '.sing_box_version // "unknown"') openwrt_version=$(echo "$system_info_json" | jq -r '.openwrt_version // "unknown"') diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 32701819..2718de3a 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -1496,6 +1496,65 @@ updates_check_sing_box_stable() { return 0 } +# Checks whether a newer NetShift release is available on GitHub. ON-DEMAND +# (the "Check for updates" button) — mirrors the sing-box cores so get_system_info +# never touches the network. SYNC (quick call → component_action path). Graceful +# on an unreachable/rate-limited API: echoes {"success":false,"message":"..."} +# and returns non-zero. NEVER exits (runs via component_action → JSON + rc). +# +# Output (mirrors updates_check_sing_box_stable): +# {"success":true,"current_version":"...","latest_version":"...", +# "status":"latest"|"outdated"} +# +# v-normalization: a single leading "v" is stripped from BOTH the installed +# version and the GitHub tag before comparing (task-028 dropped the v from the +# build, but a v-tagged release would still break a raw compare). The compare is +# on the leading semver (drop any "-..." suffix) via the same sort -V based +# is_min_package_version the cores use. +updates_check_netshift() { + local current_version latest cur_norm latest_norm cur_semver latest_semver status + + current_version="$NETSHIFT_VERSION" + + # Dev/unstamped build: the placeholder __COMPILED_VERSION_VARIABLE__ contains + # "COMPILED" and is not a real semver. Report it honestly as "latest" (a dev + # build is never "outdated"; the UI also guards dev separately) and still fetch + # the real latest tag for display. + case "$current_version" in + *COMPILED*) + latest="$(updates_netshift_latest_tag)" + if [ -z "$latest" ]; then + echo "{\"success\":false,\"message\":\"Could not determine the latest NetShift release (GitHub API unreachable or rate-limited)\"}" + return 1 + fi + echo "{\"success\":true,\"current_version\":\"$current_version\",\"latest_version\":\"$latest\",\"status\":\"latest\"}" + return 0 + ;; + esac + + latest="$(updates_netshift_latest_tag)" + if [ -z "$latest" ]; then + echo "{\"success\":false,\"message\":\"Could not determine the latest NetShift release (GitHub API unreachable or rate-limited)\"}" + return 1 + fi + + # Strip a single leading "v" from both sides (no-op if absent), then compare + # on the leading semver only. + cur_norm="${current_version#v}" + latest_norm="${latest#v}" + cur_semver="${cur_norm%%-*}" + latest_semver="${latest_norm%%-*}" + + if is_min_package_version "$cur_semver" "$latest_semver"; then + status="latest" + else + status="outdated" + fi + + echo "{\"success\":true,\"current_version\":\"$current_version\",\"latest_version\":\"$latest\",\"status\":\"$status\"}" + return 0 +} + # ── NetShift self-update (Component Manager, task-017) ────────────── # # Variant A: a targeted package upgrade (download the release .ipk/.apk from @@ -1730,6 +1789,9 @@ component_action() { sing_box:check_update_stable) updates_check_sing_box_stable ;; + netshift:check_update) + updates_check_netshift + ;; netshift:self_update) updates_self_update_netshift ;; diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index fe226186..4fe062b2 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -11,7 +11,7 @@ # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, # nftv6, diagnostics, subscription, insecure, rejected, # jobstate, selfheal, dnsdetour, globalproxy, stablecheck, -# extcheck, selfupdate, backupguard +# extcheck, netshiftcheck, selfupdate, backupguard # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index ee1b1cf2..a03f354c 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -3261,6 +3261,139 @@ DRVEOF rm -rf "$work" } +# ───────────────────────────────────────────────────────────────── +# Test: NetShift update check on-demand (task-029) +# ───────────────────────────────────────────────────────────────── +# Two parts: +# (A) STATIC: get_system_info must do NO network I/O — the GitHub curl is gone +# and netshift_latest_version is the constant "unknown". +# (B) updates_check_netshift version compare + v-normalization + JSON shape: +# source updater.sh, silence logging, OVERRIDE updates_netshift_latest_tag +# (the shared tag fetch) + set NETSHIFT_VERSION, run the check. Stub inputs: +# STUBNS_INSTALLED = $NETSHIFT_VERSION; STUBNS_TAG = the GitHub latest tag +# (empty → unreachable branch). +test_check_update_netshift() { + header "NetShift Update Check — on-demand + v-prefix (task-029)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local bin="${NETSHIFT_SRC}/usr/bin/netshift" + local updater="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$updater" ] || [ ! -r "$bin" ]; then + skip "updater.sh / bin not found in ${NETSHIFT_SRC}" + return + fi + + # ── Part A (static): get_system_info has NO live GitHub curl ──────────────── + # Extract the get_system_info function body and assert it contains no curl to + # the releases API, and that it pins netshift_latest_version="unknown". + local fn + fn="$(awk '/^get_system_info\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "$bin")" + if [ -n "$fn" ] \ + && ! printf '%s' "$fn" | grep -q 'releases/latest' \ + && printf '%s' "$fn" | grep -q 'netshift_latest_version="unknown"'; then + pass "netshiftcheck-get_system_info-no-network:OK" + else + fail "netshiftcheck-get_system_info-no-network:FAIL" "$fn" + fi + + local work="/tmp/netshift-netshiftcheck-$$" + rm -rf "$work" + mkdir -p "$work" + + # ── Part B: driver sources updater.sh + helpers.sh, silences logging, + # overrides the shared tag fetch + NETSHIFT_VERSION, runs the check. + local drv="$work/driver.sh" + cat > "$drv" << 'DRVEOF' +log() { :; } +echolog() { :; } +nolog() { :; } +. "DRV_HELPERS" +. "DRV_UPDATER" +NETSHIFT_VERSION="$STUBNS_INSTALLED" +updates_netshift_latest_tag() { printf '%s' "$STUBNS_TAG"; } +updates_check_netshift +DRVEOF + sed -i "s|DRV_UPDATER|$updater|g;s|DRV_HELPERS|${NETSHIFT_LIB_DIR}/helpers.sh|g" "$drv" + + local out="$work/out.json" + run_netshiftcheck() { + ash "$drv" > "$out" 2>/dev/null || true + } + + # ── Case 1: installed v0.8.6 vs latest 0.8.6 (no v) → latest (NOT outdated) ── + export STUBNS_INSTALLED="v0.8.6" + export STUBNS_TAG="0.8.6" + run_netshiftcheck + if jq -e '.success == true and .status == "latest" + and .current_version == "v0.8.6" + and .latest_version == "0.8.6"' "$out" > /dev/null 2>&1; then + pass "netshiftcheck-vprefix-installed-eq-latest:OK" + else + fail "netshiftcheck-vprefix-installed-eq-latest:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 2: installed 0.8.5 vs latest 0.8.6 → outdated ────────────────────── + export STUBNS_INSTALLED="0.8.5" + export STUBNS_TAG="0.8.6" + run_netshiftcheck + if jq -e '.success == true and .status == "outdated" + and .current_version == "0.8.5" + and .latest_version == "0.8.6"' "$out" > /dev/null 2>&1; then + pass "netshiftcheck-older-outdated:OK" + else + fail "netshiftcheck-older-outdated:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 3: installed v0.8.6 vs latest v0.8.6 (both v) → latest ───────────── + export STUBNS_INSTALLED="v0.8.6" + export STUBNS_TAG="v0.8.6" + run_netshiftcheck + if jq -e '.success == true and .status == "latest"' "$out" > /dev/null 2>&1; then + pass "netshiftcheck-both-vprefix-latest:OK" + else + fail "netshiftcheck-both-vprefix-latest:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 4: JSON shape — keys success/current_version/latest_version/status ─ + export STUBNS_INSTALLED="0.8.5" + export STUBNS_TAG="0.8.6" + run_netshiftcheck + if jq -e 'has("success") and has("current_version") + and has("latest_version") and has("status")' "$out" > /dev/null 2>&1; then + pass "netshiftcheck-json-shape:OK" + else + fail "netshiftcheck-json-shape:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 5: tag fetch failure (empty) → success:false ─────────────────────── + export STUBNS_INSTALLED="0.8.6" + export STUBNS_TAG="" + run_netshiftcheck + if jq -e '.success == false and (.message | length) > 0' "$out" > /dev/null 2>&1; then + pass "netshiftcheck-fetch-failure-successfalse:OK" + else + fail "netshiftcheck-fetch-failure-successfalse:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + # ── Case 6: dev/unstamped build (placeholder) → latest (graceful) ─────────── + export STUBNS_INSTALLED="__COMPILED_VERSION_VARIABLE__" + export STUBNS_TAG="0.8.6" + run_netshiftcheck + if jq -e '.success == true and .status == "latest" + and .latest_version == "0.8.6"' "$out" > /dev/null 2>&1; then + pass "netshiftcheck-dev-build-graceful:OK" + else + fail "netshiftcheck-dev-build-graceful:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + unset STUBNS_INSTALLED STUBNS_TAG + rm -rf "$work" +} + # ───────────────────────────────────────────────────────────────── # Test: NetShift self-update (task-017) # ───────────────────────────────────────────────────────────────── @@ -3722,6 +3855,7 @@ main() { test_global_proxy test_check_update_stable test_check_update_extended + test_check_update_netshift test_self_update_netshift test_backup_integrity ;; @@ -3741,6 +3875,7 @@ main() { globalproxy) test_global_proxy ;; stablecheck) test_check_update_stable ;; extcheck) test_check_update_extended ;; + netshiftcheck) test_check_update_netshift ;; selfupdate) test_self_update_netshift ;; backupguard) test_backup_integrity ;; jq) test_jq_helpers ;; @@ -3748,7 +3883,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck selfupdate backupguard" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" exit 1 ;; esac From 9fee5f283b619fdd057c30a2c836551c0933d130 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Wed, 10 Jun 2026 16:28:20 +0300 Subject: [PATCH 63/75] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BB?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=8E=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=B0=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=81=D0=BA=D0=B0=D1=87=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 52 +++++ .../memory/luci-frontend-developer.md | 34 +++ .../memory/shell-backend-developer.md | 42 ++++ fe-app-netshift/locales/calls.json | 215 ++++++++++-------- fe-app-netshift/locales/netshift.pot | 207 +++++++++-------- fe-app-netshift/locales/netshift.ru.po | 16 +- fe-app-netshift/src/netshift/types.ts | 1 + .../resources/view/netshift/section.js | 15 ++ luci-app-netshift/po/ru/netshift.po | 16 +- luci-app-netshift/po/templates/netshift.pot | 207 +++++++++-------- netshift/files/etc/config/netshift | 10 + netshift/files/usr/bin/netshift | 9 +- netshift/files/usr/lib/constants.sh | 6 + netshift/files/usr/lib/helpers.sh | 34 ++- tests/entrypoint.sh | 85 +++++++ 15 files changed, 655 insertions(+), 294 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index e3efa7bc..4dca0c9c 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -718,3 +718,55 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> ['component_action','netshift','check_update']; runNetshiftCheck has 0 fetchSystemInfo calls. All gates green. Ready for manual commit (operator commits; agents never auto-commit). + +## task-031/032 subscription format/UA preference (try Xray-JSON first) (2026-06-07) + +- PROBLEM (from a user, Иван): a panel returns a sing-box config (missing + xhttp/hysteria2 outbounds) under the default `singbox/<ver>` UA, but returns an + Xray JSON (which HAS xhttp) under a Happ-like UA. Manual paste of the link + works (xray-json parsed), via subscription it doesn't. +- ROOT CAUSE (explore-verified): download_subscription_into_cache UA-probe loop + BREAKS on the FIRST UA whose body is usable (bin/netshift:566-585). UA order + (auto) = singbox/<ver> ALWAYS FIRST -> cached winner -> whitelist (v2rayN Happ + Hiddify Clash.Meta ClashMetaForAndroid). So a valid sing-box JSON under the + first UA terminates the probe and the Happ/Xray UA is never tried. +- SECOND GAP (on record, OUT OF SCOPE): xray_json_to_uri_lines emits xhttp + (helpers.sh:1208-1211) but NOT hysteria2 (protocol gate only vless/trojan/ + shadowsocks — hysteria2 isn't an Xray protocol). hysteria2 works via the + URI-list path, not the xray-json converter. If a user reports missing + hysteria2-from-xray-json, need a sample of their subscription FIRST (likely + it's in clash-yaml or uri-list, not xray-json) before any converter fix. +- OPERATOR DECISION: Variant A — a per-section UCI option + `subscription_format_preference` (auto|xray|singbox, default auto) that REORDERS + the UA candidates (NOT changing the break-on-first-usable loop). dropdown + values auto/xray/singbox; explicit user choice OUTRANKS the cached UA-winner. +- task-031 (backend, APPROVED W/ CONDITIONS->met, smoke 127/0): new 3rd arg + `format_preference` to build_subscription_user_agent_candidates (helpers.sh); + xray -> SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES="v2rayN Happ" (new constant) + FIRST, then default, then cached winner, then rest; singbox/auto/empty/unknown + -> today's exact order (unknown==auto, forward-compatible); configured-UA + short-circuit unchanged (explicit UA still emits ONLY itself). The dedup `seen` + loop is reused so the front-loaded xray UAs outrank the cached winner. + download_subscription_into_cache reads the option (default auto) + passes 3rd + arg. UCI example documents BOTH the new option AND the previously-undocumented + subscription_user_agent. CASE I = 9 assertions, all OK in smoke. +- task-032 (frontend, APPROVED): form.ListValue subscription_format_preference in + the `subscription` tab modelled on subscription_update_interval; values + auto/xray/singbox, default auto, same depends; type union added to + ConfigProxySubscriptionSection (optional). section.js+type-only -> main.js NO + diff (correct). 4 new msgids, ru filled, fe<->luci byte-identical. Contract + verified end-to-end: bin reads name, helpers branches on xray, FE writes exactly + those values. +- REUSABLE (reviewer): for backend-coupled UI enum dropdowns the safe-match + criterion is "every UI value is handled + unknown/empty -> sane default", NOT a + strict 1:1 set match (here auto/singbox/unknown all fold to the default + ordering, only xray is distinct). +- TECH-DEBT FOLLOW-UP (found by reviewer, pre-existing, harness-wide, NOT a + task-031 defect): the smoke `fb-case*`/`rh-case*` tests parse tokens via + `cmd | while read ... pass/fail` — the `while` runs in a PIPE SUBSHELL so + PASS/FAIL increments are LOST; a `:FAIL` prints red but does NOT fail CI + (pipeline rc = while rc = 0, set -e doesn't trip). The COUNTED pattern is + `while read ... done < "$out"` (redirect, current shell) used by + test_backup_integrity. Worth a dedicated task to migrate fb/rh parsers so these + assertions are truly gated. Until then, trust the per-token ✓/✗ marks, not just + "Results: N passed". diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index b47352c2..485b2cdc 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -707,3 +707,37 @@ append findings; keep under ~200 lines. node_modules/.bin; yarn.lock unchanged, no .yarn/.yarnrc.yml. - FLAG (no browser in env): the neutral→checked card transition + toast were verified by reasoning + the pure cards.test.js, NOT screenshotted. + +## task-032 — subscription_format_preference dropdown (Subscription tab) + +- Added a `form.ListValue` `subscription_format_preference` in section.js + (HAND-WRITTEN, NOT bundled) right AFTER `subscription_url` (~144-157), + modelled EXACTLY on `subscription_update_interval`: `.value('auto',_('Auto'))` + / `.value('xray',_('Xray JSON (Happ)'))` / `.value('singbox',_('Sing-box'))`, + `o.default='auto'`, same `depends({connection_type:'proxy',proxy_config_type: + 'subscription'})`. NO explicit `rmempty` — like the interval field, LuCI + ListValue defaults `rmempty=true`, so selecting the default ('auto') does NOT + write a spurious UCI value (matches backend task-031 which treats empty/ + unknown as auto). Single-literal `_()` description (no concat). +- types.ts: added optional union `subscription_format_preference?: 'auto' | + 'xray' | 'singbox';` to `ConfigProxySubscriptionSection` (after + subscription_url). Pure type-only → erased at build. +- BACKEND CONTRACT (task-031, in working tree): UCI option name EXACTLY + `subscription_format_preference`, values auto/xray/singbox; netshift bin reads + it (`uci -q get …subscription_format_preference`, empty→auto). Confirmed via + grep before editing. +- main.js: NO diff (section.js hand-written + type-only types.ts), like + task-023. Build still run to confirm; `git diff --stat main.js` empty. +- locales: `node {extract-calls,generate-pot,generate-po ru,distribute- + locales}.js`. msgid delta PURELY ADDITIVE — 4 added (Auto / Subscription + format / Xray JSON (Happ) / the description), 0 removed; "Sing-box" REUSED an + existing msgid (so generate-po reported 339/342, only 3 truly-new beyond the + reused one). Filled 4 ru msgstr in SOURCE locales/netshift.ru.po (Auto→Авто, + Subscription format→Формат подписки, Xray JSON (Happ)→Xray JSON (Happ), + description translated) then distribute → po/ru + po/templates byte-identical + to source (diff -q). Only header msgstr empty. 5 catalog files touched. +- yarn classic 1.22.22; ran gate via node_modules/.bin (prettier --write src / + eslint src --ext .ts,.tsx --max-warnings=0 / vitest 472 pass / tsup). format + diff on src = ONLY my 1 types.ts line (no churn). yarn.lock unchanged, no + .yarn/.yarnrc.yml. FLAG (no browser): dropdown rendering/auto-hide not + screenshotted — verified structurally. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index b3d7687d..2481ae22 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -826,3 +826,45 @@ findings; keep under ~200 lines. (all)/case alias/usage line/docker-compose comment). shellcheck -S error clean (bin+libs+install.sh); `smoke-tests all` = 127 passed / 0 failed (120 baseline + 7 new). NO sacred constant/port/mark/path/ACL/frontend change. + +## task-031 — subscription_format_preference (UA probe reorder) + +- Per-section UCI option `subscription_format_preference` (auto|xray|singbox, + default auto) REORDERS the UA candidate probe so xray-yielding UAs can be + tried FIRST. Root cause it fixes: the download probe loop in + `download_subscription_into_cache` (bin/netshift) breaks on the first usable + body, so the always-first `singbox/<ver>` UA wins and the Happ/v2rayN UA + (which some panels answer with Xray JSON carrying xhttp nodes) is never tried. + We ONLY reorder — first-usable-still-wins loop is untouched. +- `build_subscription_user_agent_candidates` (helpers.sh ~746) now takes a 3rd + arg `format_preference`. Configured-UA short-circuit (arg1 set → emit only it) + is UNCHANGED and still outranks preference. For auto-mode ordering I switched + from a fixed `for candidate in ...` list to `set -- <list>; for candidate in + "$@"` so I could pick the list by preference in an `if`: + - xray: `set -- $SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES "$default" "$pref" $SUBSCRIPTION_USER_AGENT_CANDIDATES` + - else (auto/empty/singbox/unknown): `set -- "$default" "$pref" $SUBSCRIPTION_USER_AGENT_CANDIDATES` (today's exact order). + The EXISTING newline `seen` dedup loop is reused unchanged → xray UAs precede + the cached winner AND default, and nothing is emitted twice. Unknown values + fall through `else` = auto (forward-compatible). Keep the + `# shellcheck disable=SC2086` intentional-word-split comment on EACH `set --` + line that splices a `$LIST`. +- New constant: `SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES="v2rayN Happ"` in + constants.sh next to `SUBSCRIPTION_USER_AGENT_CANDIDATES` (the xray subset; + no inline magic strings per project-core §5). +- bin/netshift `download_subscription_into_cache` (~530): added local + `subscription_format_preference`, read via + `uci -q get netshift.${section}.subscription_format_preference`, default + "auto" when empty, passed as 3rd arg to the builder (~539). Per-URL cache + + UA-winner caching unchanged. +- UCI example (etc/config/netshift) documents BOTH the new option AND the + previously-read-but-undocumented `subscription_user_agent` (schema honesty). +- Tests: extended existing CASE I in `test_subscription`'s `fb` sub-script + (which sources real constants.sh+helpers.sh under ash). Added cases c2–h: + empty/auto/singbox→default first; xray→v2rayN,Happ first + outrank + default&cached pref + dedup; unknown→auto; configured+xray→only configured. + The `fb` parser is generic `*:OK`/`*:FAIL`, so new `fb-caseI-*` lines need NO + case alias. shellcheck -S error clean; `smoke-tests all` = 127 passed / + 0 failed (+8 new CASE-I assertions, same 127 total since they're sub-tokens). +- GOTCHA: `test_rejected_hash` emits `rh-case1/2/6:FAIL` sub-tokens that print + red but are NOT counted by the global tally (suite still EXIT=0, 127/0) — this + is PRE-EXISTING on the clean tree (verified by git stash), not a regression. diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index fcc2853a..e1a35f01 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -45,14 +45,14 @@ "call": "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", "key": "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588" ] }, { "call": "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", "key": "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676" ] }, { @@ -74,7 +74,7 @@ "call": "Allow insecure TLS for subscription fetch", "key": "Allow insecure TLS for subscription fetch", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163" ] }, { @@ -88,21 +88,28 @@ "call": "Applicable for SOCKS and Shadowsocks proxy", "key": "Applicable for SOCKS and Shadowsocks proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:356" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:371" ] }, { "call": "At least one valid domain must be specified. Comments-only content is not allowed.", "key": "At least one valid domain must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:633" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648" ] }, { "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:722" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:737" + ] + }, + { + "call": "Auto", + "key": "Auto", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153" ] }, { @@ -237,7 +244,7 @@ "call": "Community Lists", "key": "Community Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:494" ] }, { @@ -329,14 +336,14 @@ "call": "Custom domains", "key": "Custom domains", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:572" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587" ] }, { "call": "Custom subnets", "key": "Custom subnets", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:660" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675" ] }, { @@ -414,15 +421,15 @@ "call": "Disabled", "key": "Disabled", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:592", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:680" ] }, { "call": "Disables TLS certificate verification when downloading the subscription.", "key": "Disables TLS certificate verification when downloading the subscription.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164" ] }, { @@ -450,7 +457,7 @@ "call": "DNS over HTTPS (DoH)", "key": "DNS over HTTPS (DoH)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:445", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:460", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48" ] }, @@ -458,7 +465,7 @@ "call": "DNS over TLS (DoT)", "key": "DNS over TLS (DoT)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:461", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49" ] }, @@ -466,7 +473,7 @@ "call": "DNS Protocol Type", "key": "DNS Protocol Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:442", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45" ] }, @@ -481,7 +488,7 @@ "call": "DNS Server", "key": "DNS Server", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:456", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:471", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58" ] }, @@ -510,7 +517,7 @@ "call": "Domain Resolver", "key": "Domain Resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446" ] }, { @@ -561,15 +568,15 @@ "call": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "key": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220" ] }, { "call": "Dynamic List", "key": "Dynamic List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:578", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:666" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:593", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681" ] }, { @@ -583,14 +590,14 @@ "call": "Enable built-in DNS resolver for domains handled by this section", "key": "Enable built-in DNS resolver for domains handled by this section", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447" ] }, { "call": "Enable DNS resolve to get real IP when routing", "key": "Enable DNS resolve to get real IP when routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:899" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914" ] }, { @@ -611,7 +618,7 @@ "call": "Enable Mixed Proxy", "key": "Enable Mixed Proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:868" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:883" ] }, { @@ -625,7 +632,7 @@ "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:869" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:884" ] }, { @@ -653,91 +660,91 @@ "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:615" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:630" ] }, { "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603" ] }, { "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:691" ] }, { "call": "Every 1 minute", "key": "Every 1 minute", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:272" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:287" ] }, { "call": "Every 12 hours", "key": "Every 12 hours", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:170" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:185" ] }, { "call": "Every 3 hours", "key": "Every 3 hours", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:183" ] }, { "call": "Every 3 minutes", "key": "Every 3 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:273" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:288" ] }, { "call": "Every 30 minutes", "key": "Every 30 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181" ] }, { "call": "Every 30 seconds", "key": "Every 30 seconds", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:271" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:286" ] }, { "call": "Every 5 minutes", "key": "Every 5 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:274" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:289" ] }, { "call": "Every 6 hours", "key": "Every 6 hours", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:184" ] }, { "call": "Every day", "key": "Every day", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:171" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:186" ] }, { "call": "Every hour", "key": "Every hour", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:167" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:182" ] }, { @@ -758,7 +765,7 @@ "call": "Exclude servers by keyword", "key": "Exclude servers by keyword", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219" ] }, { @@ -802,7 +809,7 @@ "call": "Fully Routed IPs", "key": "Fully Routed IPs", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:840" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:855" ] }, { @@ -823,28 +830,28 @@ "call": "Global Proxy", "key": "Global Proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:366" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:381" ] }, { "call": "Group by countries", "key": "Group by countries", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194" ] }, { "call": "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag", "key": "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:195" ] }, { "call": "How often to automatically update the subscription", "key": "How often to automatically update the subscription", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179" ] }, { @@ -858,7 +865,7 @@ "call": "Include servers by keyword", "key": "Include servers by keyword", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:192" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:207" ] }, { @@ -1284,7 +1291,7 @@ "call": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "key": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:193" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:208" ] }, { @@ -1327,14 +1334,14 @@ "call": "Local Domain Lists", "key": "Local Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:759" ] }, { "call": "Local Subnet Lists", "key": "Local Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783" ] }, { @@ -1376,7 +1383,7 @@ "call": "Mixed Proxy Port", "key": "Mixed Proxy Port", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:882" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:897" ] }, { @@ -1390,7 +1397,7 @@ "call": "Must be a number in the range of 50 - 1000", "key": "Must be a number in the range of 50 - 1000", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324" ] }, { @@ -1432,7 +1439,7 @@ "call": "Network Interface", "key": "Network Interface", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399" ] }, { @@ -1483,7 +1490,7 @@ "call": "Only one section can be global at a time.", "key": "Only one section can be global at a time.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:375" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390" ] }, { @@ -1593,28 +1600,28 @@ "call": "Regional options cannot be used together", "key": "Regional options cannot be used together", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528" ] }, { "call": "Remote Domain Lists", "key": "Remote Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:792" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:807" ] }, { "call": "Remote Subnet Lists", "key": "Remote Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:816" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831" ] }, { "call": "Resolve real IP for routing", "key": "Resolve real IP for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:898" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913" ] }, { @@ -1628,7 +1635,7 @@ "call": "Route all unmatched traffic through this section's outbound.", "key": "Route all unmatched traffic through this section's outbound.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:367" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382" ] }, { @@ -1712,7 +1719,7 @@ "call": "Russia inside restrictions", "key": "Russia inside restrictions", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547" ] }, { @@ -1733,7 +1740,7 @@ "call": "Select a predefined list for routing", "key": "Select a predefined list for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:480" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:495" ] }, { @@ -1768,14 +1775,14 @@ "call": "Select network interface for VPN connection", "key": "Select network interface for VPN connection", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400" ] }, { "call": "Select or enter DNS server address", "key": "Select or enter DNS server address", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59" ] }, @@ -1797,7 +1804,7 @@ "call": "Select the DNS protocol type for the domain resolver", "key": "Select the DNS protocol type for the domain resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:443" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:458" ] }, { @@ -1839,7 +1846,7 @@ "call": "Selector Proxy Links", "key": "Selector Proxy Links", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:231" ] }, { @@ -1882,6 +1889,7 @@ "call": "Sing-box", "key": "Sing-box", "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155", "src/netshift/tabs/dashboard/initController.ts:354" ] }, @@ -1959,29 +1967,29 @@ "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:841" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:856" ] }, { "call": "Specify remote URLs to download and use domain lists", "key": "Specify remote URLs to download and use domain lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808" ] }, { "call": "Specify remote URLs to download and use subnet lists", "key": "Specify remote URLs to download and use subnet lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:817" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:832" ] }, { "call": "Specify the path to the list file located on the router filesystem", "key": "Specify the path to the list file located on the router filesystem", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:745", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:769" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:760", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784" ] }, { @@ -2013,11 +2021,18 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:22" ] }, + { + "call": "Subscription format", + "key": "Subscription format", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148" + ] + }, { "call": "Subscription Update Interval", "key": "Subscription Update Interval", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:178" ] }, { @@ -2087,8 +2102,8 @@ "call": "Text List", "key": "Text List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:579", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:667" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:682" ] }, { @@ -2102,28 +2117,28 @@ "call": "The interval between connectivity tests", "key": "The interval between connectivity tests", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284" ] }, { "call": "The maximum difference in response times (ms) allowed when comparing servers", "key": "The maximum difference in response times (ms) allowed when comparing servers", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299" ] }, { "call": "The URL used to test server connectivity", "key": "The URL used to test server connectivity", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:317" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332" ] }, { "call": "This is a security trade-off: an attacker could intercept the fetch.", "key": "This is a security trade-off: an attacker could intercept the fetch.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168" ] }, { @@ -2172,7 +2187,7 @@ "call": "UDP (Unprotected DNS)", "key": "UDP (Unprotected DNS)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:462", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50" ] }, @@ -2180,7 +2195,7 @@ "call": "UDP over TCP", "key": "UDP over TCP", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:355" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370" ] }, { @@ -2278,35 +2293,35 @@ "call": "URLTest Check Interval", "key": "URLTest Check Interval", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:268" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283" ] }, { "call": "URLTest Proxy Links", "key": "URLTest Proxy Links", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:242" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:257" ] }, { "call": "URLTest Testing URL", "key": "URLTest Testing URL", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:331" ] }, { "call": "URLTest Tolerance", "key": "URLTest Tolerance", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:298" ] }, { "call": "Use only for IP-host panels that serve an invalid or self-signed certificate.", "key": "Use only for IP-host panels that serve an invalid or self-signed certificate.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:151" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166" ] }, { @@ -2320,35 +2335,35 @@ "call": "Use with Exclusion sections to route specific domains directly.", "key": "Use with Exclusion sections to route specific domains directly.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:388" ] }, { "call": "User Domains", "key": "User Domains", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602" ] }, { "call": "User Domains List", "key": "User Domains List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:614" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629" ] }, { "call": "User Subnets", "key": "User Subnets", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:690" ] }, { "call": "User Subnets List", "key": "User Subnets List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:717" ] }, { @@ -2380,8 +2395,8 @@ "call": "Validation errors:", "key": "Validation errors:", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:647", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:749" ] }, { @@ -2411,29 +2426,29 @@ "key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "places": [ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:217", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:258" ] }, { "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:530" ] }, { "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:534" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549" ] }, { "call": "When enabled, traffic not matching any other section's lists will go through this proxy.", "key": "When enabled, traffic not matching any other section's lists will go through this proxy.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384" ] }, { @@ -2443,6 +2458,20 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:117" ] }, + { + "call": "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format.", + "key": "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149" + ] + }, + { + "call": "Xray JSON (Happ)", + "key": "Xray JSON (Happ)", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154" + ] + }, { "call": "YACD Secret Key", "key": "YACD Secret Key", diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index 869e0d99..7d0153fd 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 16:26+0300\n" -"PO-Revision-Date: 2026-06-07 16:26+0300\n" +"POT-Creation-Date: 2026-06-07 17:04+0300\n" +"PO-Revision-Date: 2026-06-07 17:04+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -40,11 +40,11 @@ msgstr "" msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676 msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" msgstr "" @@ -57,7 +57,7 @@ msgstr "" msgid "Advanced" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163 msgid "Allow insecure TLS for subscription fetch" msgstr "" @@ -65,18 +65,22 @@ msgstr "" msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:356 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:371 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:633 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:722 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:737 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153 +msgid "Auto" +msgstr "" + #: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:43 msgid "Available actions" msgstr "" @@ -154,7 +158,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:494 msgid "Community Lists" msgstr "" @@ -207,11 +211,11 @@ msgstr "" msgid "Currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:572 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 msgid "Custom domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:660 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675 msgid "Custom subnets" msgstr "" @@ -256,12 +260,12 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:592 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:680 msgid "Disabled" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164 msgid "Disables TLS certificate verification when downloading the subscription." msgstr "" @@ -277,17 +281,17 @@ msgstr "" msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:445 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:460 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:461 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:442 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 msgid "DNS Protocol Type" msgstr "" @@ -296,7 +300,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:456 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:471 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58 msgid "DNS Server" msgstr "" @@ -313,7 +317,7 @@ msgstr "" msgid "Domain and subnet lists that decide which traffic uses this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446 msgid "Domain Resolver" msgstr "" @@ -343,12 +347,12 @@ msgstr "" msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:578 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:666 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:593 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681 msgid "Dynamic List" msgstr "" @@ -356,11 +360,11 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:899 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914 msgid "Enable DNS resolve to get real IP when routing" msgstr "" @@ -372,7 +376,7 @@ msgstr "" msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:868 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:883 msgid "Enable Mixed Proxy" msgstr "" @@ -380,7 +384,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:869 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:884 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -396,55 +400,55 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:615 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:630 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:691 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:272 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:287 msgid "Every 1 minute" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:170 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:185 msgid "Every 12 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:183 msgid "Every 3 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:273 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:288 msgid "Every 3 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181 msgid "Every 30 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:271 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:286 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:274 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:289 msgid "Every 5 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:184 msgid "Every 6 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:171 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:186 msgid "Every day" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:167 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:182 msgid "Every hour" msgstr "" @@ -456,7 +460,7 @@ msgstr "" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219 msgid "Exclude servers by keyword" msgstr "" @@ -488,7 +492,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:840 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:855 msgid "Fully Routed IPs" msgstr "" @@ -500,19 +504,19 @@ msgstr "" msgid "Global check" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:366 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:381 msgid "Global Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194 msgid "Group by countries" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:195 msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 msgid "How often to automatically update the subscription" msgstr "" @@ -520,7 +524,7 @@ msgstr "" msgid "HTTP error" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:192 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:207 msgid "Include servers by keyword" msgstr "" @@ -766,7 +770,7 @@ msgstr "" msgid "Issues detected" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:193 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:208 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" @@ -791,11 +795,11 @@ msgstr "" msgid "Lists & Updates" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:759 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783 msgid "Local Subnet Lists" msgstr "" @@ -819,7 +823,7 @@ msgstr "" msgid "Mixed proxy and DNS resolution tuning" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:882 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:897 msgid "Mixed Proxy Port" msgstr "" @@ -827,7 +831,7 @@ msgstr "" msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -851,7 +855,7 @@ msgstr "" msgid "Network" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399 msgid "Network Interface" msgstr "" @@ -884,7 +888,7 @@ msgstr "" msgid "Not running" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:375 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390 msgid "Only one section can be global at a time." msgstr "" @@ -949,19 +953,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:792 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:807 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:816 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:898 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913 msgid "Resolve real IP for routing" msgstr "" @@ -969,7 +973,7 @@ msgstr "" msgid "Restart NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:367 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 msgid "Route all unmatched traffic through this section's outbound." msgstr "" @@ -1017,7 +1021,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547 msgid "Russia inside restrictions" msgstr "" @@ -1029,7 +1033,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:480 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:495 msgid "Select a predefined list for routing" msgstr "" @@ -1049,11 +1053,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59 msgid "Select or enter DNS server address" msgstr "" @@ -1066,7 +1070,7 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:443 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:458 msgid "Select the DNS protocol type for the domain resolver" msgstr "" @@ -1090,7 +1094,7 @@ msgstr "" msgid "Selector" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:231 msgid "Selector Proxy Links" msgstr "" @@ -1115,6 +1119,7 @@ msgstr "" msgid "Show sing-box config" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155 #: src/netshift/tabs/dashboard/initController.ts:354 msgid "Sing-box" msgstr "" @@ -1159,20 +1164,20 @@ msgstr "" msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:841 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:856 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:817 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:832 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:745 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:769 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:760 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1193,7 +1198,11 @@ msgstr "" msgid "Subscription feeds, server filters and URLTest tuning" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148 +msgid "Subscription format" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:178 msgid "Subscription Update Interval" msgstr "" @@ -1233,8 +1242,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:579 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:667 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:682 msgid "Text List" msgstr "" @@ -1242,19 +1251,19 @@ msgstr "" msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:317 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332 msgid "The URL used to test server connectivity" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 msgid "This is a security trade-off: an attacker could intercept the fetch." msgstr "" @@ -1282,12 +1291,12 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:462 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:355 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370 msgid "UDP over TCP" msgstr "" @@ -1349,23 +1358,23 @@ msgstr "" msgid "URLTest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:268 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:242 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:257 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:331 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:298 msgid "URLTest Tolerance" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:151 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 msgid "Use only for IP-host panels that serve an invalid or self-signed certificate." msgstr "" @@ -1373,23 +1382,23 @@ msgstr "" msgid "Use this only when the router has working IPv6 connectivity." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:388 msgid "Use with Exclusion sections to route specific domains directly." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:614 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:690 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:717 msgid "User Subnets List" msgstr "" @@ -1415,8 +1424,8 @@ msgstr "" msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:647 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:749 msgid "Validation errors:" msgstr "" @@ -1434,20 +1443,20 @@ msgid "Visit Wiki" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:217 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:258 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:530 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:534 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 msgid "When enabled, traffic not matching any other section's lists will go through this proxy." msgstr "" @@ -1455,6 +1464,14 @@ msgstr "" msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149 +msgid "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154 +msgid "Xray JSON (Happ)" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:432 msgid "YACD Secret Key" msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 8eb77640..87046a7b 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 19:26+0300\n" -"PO-Revision-Date: 2026-06-07 19:26+0300\n" +"POT-Creation-Date: 2026-06-07 20:04+0300\n" +"PO-Revision-Date: 2026-06-07 20:04+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -62,6 +62,9 @@ msgstr "Необходимо указать хотя бы один действ msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы." +msgid "Auto" +msgstr "Авто" + msgid "Available actions" msgstr "Доступные действия" @@ -854,6 +857,9 @@ msgstr "Подписка" msgid "Subscription feeds, server filters and URLTest tuning" msgstr "Источники подписок, фильтры серверов и настройка URLTest" +msgid "Subscription format" +msgstr "Формат подписки" + msgid "Subscription Update Interval" msgstr "Интервал обновления подписки" @@ -1022,6 +1028,12 @@ msgstr "Когда включено, трафик, не совпадающий msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "Какая секция прокси/VPN обслуживает DNS. Оставьте пустым, чтобы использовать первый настроенный outbound." +msgid "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format." +msgstr "Какой формат подписки (клиент) запрашивать первым. «Авто» использует порядок по умолчанию. Выберите «Xray JSON (Happ)», если ваша панель отдаёт некоторые узлы (например, xhttp) только под клиентом вроде Happ, или «Sing-box», чтобы предпочесть формат sing-box." + +msgid "Xray JSON (Happ)" +msgstr "Xray JSON (Happ)" + msgid "YACD Secret Key" msgstr "Секретный ключ YACD" diff --git a/fe-app-netshift/src/netshift/types.ts b/fe-app-netshift/src/netshift/types.ts index 0a296e11..0575ca91 100644 --- a/fe-app-netshift/src/netshift/types.ts +++ b/fe-app-netshift/src/netshift/types.ts @@ -118,6 +118,7 @@ export namespace NetShift { connection_type: 'proxy'; proxy_config_type: 'subscription'; subscription_url: string[]; + subscription_format_preference?: 'auto' | 'xray' | 'singbox'; subscription_update_interval?: string; subscription_group_by_countries?: '0' | '1'; subscription_filter_include_keywords?: string[]; diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index 8d2fa2f3..56b2d0e0 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -141,6 +141,21 @@ function createSectionContent(section) { return validation.message; }; + o = section.taboption( + "subscription", + form.ListValue, + "subscription_format_preference", + _("Subscription format"), + _( + "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format.", + ), + ); + o.value("auto", _("Auto")); + o.value("xray", _("Xray JSON (Happ)")); + o.value("singbox", _("Sing-box")); + o.default = "auto"; + o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o = section.taboption( "subscription", form.Flag, diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 8eb77640..87046a7b 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 19:26+0300\n" -"PO-Revision-Date: 2026-06-07 19:26+0300\n" +"POT-Creation-Date: 2026-06-07 20:04+0300\n" +"PO-Revision-Date: 2026-06-07 20:04+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -62,6 +62,9 @@ msgstr "Необходимо указать хотя бы один действ msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы." +msgid "Auto" +msgstr "Авто" + msgid "Available actions" msgstr "Доступные действия" @@ -854,6 +857,9 @@ msgstr "Подписка" msgid "Subscription feeds, server filters and URLTest tuning" msgstr "Источники подписок, фильтры серверов и настройка URLTest" +msgid "Subscription format" +msgstr "Формат подписки" + msgid "Subscription Update Interval" msgstr "Интервал обновления подписки" @@ -1022,6 +1028,12 @@ msgstr "Когда включено, трафик, не совпадающий msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "Какая секция прокси/VPN обслуживает DNS. Оставьте пустым, чтобы использовать первый настроенный outbound." +msgid "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format." +msgstr "Какой формат подписки (клиент) запрашивать первым. «Авто» использует порядок по умолчанию. Выберите «Xray JSON (Happ)», если ваша панель отдаёт некоторые узлы (например, xhttp) только под клиентом вроде Happ, или «Sing-box», чтобы предпочесть формат sing-box." + +msgid "Xray JSON (Happ)" +msgstr "Xray JSON (Happ)" + msgid "YACD Secret Key" msgstr "Секретный ключ YACD" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index 869e0d99..7d0153fd 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 16:26+0300\n" -"PO-Revision-Date: 2026-06-07 16:26+0300\n" +"POT-Creation-Date: 2026-06-07 17:04+0300\n" +"PO-Revision-Date: 2026-06-07 17:04+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -40,11 +40,11 @@ msgstr "" msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:573 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:661 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676 msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" msgstr "" @@ -57,7 +57,7 @@ msgstr "" msgid "Advanced" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163 msgid "Allow insecure TLS for subscription fetch" msgstr "" @@ -65,18 +65,22 @@ msgstr "" msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:356 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:371 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:633 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:722 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:737 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153 +msgid "Auto" +msgstr "" + #: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:43 msgid "Available actions" msgstr "" @@ -154,7 +158,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:494 msgid "Community Lists" msgstr "" @@ -207,11 +211,11 @@ msgstr "" msgid "Currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:572 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 msgid "Custom domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:660 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675 msgid "Custom subnets" msgstr "" @@ -256,12 +260,12 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:577 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:665 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:592 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:680 msgid "Disabled" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164 msgid "Disables TLS certificate verification when downloading the subscription." msgstr "" @@ -277,17 +281,17 @@ msgstr "" msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:445 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:460 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:461 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:442 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 msgid "DNS Protocol Type" msgstr "" @@ -296,7 +300,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:456 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:471 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58 msgid "DNS Server" msgstr "" @@ -313,7 +317,7 @@ msgstr "" msgid "Domain and subnet lists that decide which traffic uses this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:431 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446 msgid "Domain Resolver" msgstr "" @@ -343,12 +347,12 @@ msgstr "" msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:205 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:578 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:666 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:593 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681 msgid "Dynamic List" msgstr "" @@ -356,11 +360,11 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:432 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:899 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914 msgid "Enable DNS resolve to get real IP when routing" msgstr "" @@ -372,7 +376,7 @@ msgstr "" msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:868 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:883 msgid "Enable Mixed Proxy" msgstr "" @@ -380,7 +384,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:869 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:884 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -396,55 +400,55 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:615 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:630 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:691 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:272 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:287 msgid "Every 1 minute" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:170 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:185 msgid "Every 12 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:183 msgid "Every 3 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:273 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:288 msgid "Every 3 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:181 msgid "Every 30 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:271 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:286 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:274 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:289 msgid "Every 5 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:169 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:184 msgid "Every 6 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:171 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:186 msgid "Every day" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:167 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:182 msgid "Every hour" msgstr "" @@ -456,7 +460,7 @@ msgstr "" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:204 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219 msgid "Exclude servers by keyword" msgstr "" @@ -488,7 +492,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:840 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:855 msgid "Fully Routed IPs" msgstr "" @@ -500,19 +504,19 @@ msgstr "" msgid "Global check" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:366 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:381 msgid "Global Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194 msgid "Group by countries" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:180 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:195 msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:164 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 msgid "How often to automatically update the subscription" msgstr "" @@ -520,7 +524,7 @@ msgstr "" msgid "HTTP error" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:192 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:207 msgid "Include servers by keyword" msgstr "" @@ -766,7 +770,7 @@ msgstr "" msgid "Issues detected" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:193 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:208 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" @@ -791,11 +795,11 @@ msgstr "" msgid "Lists & Updates" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:744 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:759 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783 msgid "Local Subnet Lists" msgstr "" @@ -819,7 +823,7 @@ msgstr "" msgid "Mixed proxy and DNS resolution tuning" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:882 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:897 msgid "Mixed Proxy Port" msgstr "" @@ -827,7 +831,7 @@ msgstr "" msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:309 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -851,7 +855,7 @@ msgstr "" msgid "Network" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399 msgid "Network Interface" msgstr "" @@ -884,7 +888,7 @@ msgstr "" msgid "Not running" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:375 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390 msgid "Only one section can be global at a time." msgstr "" @@ -949,19 +953,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:792 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:807 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:816 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:898 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913 msgid "Resolve real IP for routing" msgstr "" @@ -969,7 +973,7 @@ msgstr "" msgid "Restart NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:367 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 msgid "Route all unmatched traffic through this section's outbound." msgstr "" @@ -1017,7 +1021,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:532 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547 msgid "Russia inside restrictions" msgstr "" @@ -1029,7 +1033,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:480 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:495 msgid "Select a predefined list for routing" msgstr "" @@ -1049,11 +1053,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:385 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59 msgid "Select or enter DNS server address" msgstr "" @@ -1066,7 +1070,7 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:443 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:458 msgid "Select the DNS protocol type for the domain resolver" msgstr "" @@ -1090,7 +1094,7 @@ msgstr "" msgid "Selector" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:216 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:231 msgid "Selector Proxy Links" msgstr "" @@ -1115,6 +1119,7 @@ msgstr "" msgid "Show sing-box config" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:155 #: src/netshift/tabs/dashboard/initController.ts:354 msgid "Sing-box" msgstr "" @@ -1159,20 +1164,20 @@ msgstr "" msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:841 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:856 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:793 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:817 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:832 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:745 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:769 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:760 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1193,7 +1198,11 @@ msgstr "" msgid "Subscription feeds, server filters and URLTest tuning" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:163 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148 +msgid "Subscription format" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:178 msgid "Subscription Update Interval" msgstr "" @@ -1233,8 +1242,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:579 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:667 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:682 msgid "Text List" msgstr "" @@ -1242,19 +1251,19 @@ msgstr "" msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:269 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:317 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332 msgid "The URL used to test server connectivity" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:153 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:168 msgid "This is a security trade-off: an attacker could intercept the fetch." msgstr "" @@ -1282,12 +1291,12 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:462 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:355 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370 msgid "UDP over TCP" msgstr "" @@ -1349,23 +1358,23 @@ msgstr "" msgid "URLTest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:268 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:242 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:257 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:316 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:331 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:298 msgid "URLTest Tolerance" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:151 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:166 msgid "Use only for IP-host panels that serve an invalid or self-signed certificate." msgstr "" @@ -1373,23 +1382,23 @@ msgstr "" msgid "Use this only when the router has working IPv6 connectivity." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:373 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:388 msgid "Use with Exclusion sections to route specific domains directly." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:614 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:690 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:702 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:717 msgid "User Subnets List" msgstr "" @@ -1415,8 +1424,8 @@ msgstr "" msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:647 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:734 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:749 msgid "Validation errors:" msgstr "" @@ -1434,20 +1443,20 @@ msgid "Visit Wiki" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:217 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:243 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:258 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:515 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:530 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:534 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:369 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 msgid "When enabled, traffic not matching any other section's lists will go through this proxy." msgstr "" @@ -1455,6 +1464,14 @@ msgstr "" msgid "Which proxy/VPN section carries the DNS. Leave unset to use the first configured outbound." msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:149 +msgid "Which subscription format (client) to fetch first. Auto uses the default order. Choose Xray JSON (Happ) when your panel only exposes some nodes (e.g. xhttp) under a Happ-like client, or Sing-box to prefer the sing-box format." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:154 +msgid "Xray JSON (Happ)" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:432 msgid "YACD Secret Key" msgstr "" diff --git a/netshift/files/etc/config/netshift b/netshift/files/etc/config/netshift index 91da2191..bcda891d 100644 --- a/netshift/files/etc/config/netshift +++ b/netshift/files/etc/config/netshift @@ -59,6 +59,16 @@ config section 'main' # # add wget --no-check-certificate for IP-host panels whose HTTPS cert is # # invalid/self-signed/missing-SAN. Disables certificate verification. # #option subscription_insecure '0' +# # Force a specific client User-Agent for the fetch. When set, ONLY this +# # UA is used (no probing). Leave unset to auto-probe well-known clients. +# #option subscription_user_agent 'Happ' +# # Preferred body FORMAT to probe first (auto|xray|singbox, default auto). +# # Panels often serve a different config per User-Agent. 'xray' tries the +# # Xray-JSON UAs (Happ/v2rayN) FIRST to recover xhttp nodes a panel only +# # exposes in its Xray JSON; 'singbox' keeps the singbox/<ver> UA first; +# # 'auto' preserves the default order. Ignored when subscription_user_agent +# # is set (an explicit UA always wins). +# #option subscription_format_preference 'auto' # option subscription_update_interval '1h' # #option subscription_group_by_countries '0' # #option urltest_check_interval '3m' diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 5dda77dd..e3062f70 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -507,7 +507,7 @@ download_subscription_into_cache() { local tmpfile persist_tmpfile url_tmpfile rejected_cache_path tmp_hash rejected_hash validation_reason file_size fallback_tmp local configured_user_agent user_agent_cache_path cached_user_agent candidates_file local effective_user_agent download_ok winning_user_agent ua_tmpfile - local subscription_insecure + local subscription_insecure subscription_format_preference ensure_subscription_cache_dir || { log "Failed to prepare persistent subscription cache directory '$SUBSCRIPTION_CACHE_FOLDER' for section '$section'" "error" @@ -529,8 +529,13 @@ download_subscription_into_cache() { user_agent_cache_path="$(get_subscription_user_agent_cache_path "$section" "$urlhash")" configured_user_agent="$(uci -q get "netshift.${section}.subscription_user_agent" 2>/dev/null)" cached_user_agent="$(cat "$user_agent_cache_path" 2>/dev/null)" + # Per-section format preference reorders the UA probe (auto|xray|singbox). + # Empty/unset -> auto (today's order); unknown values are treated as auto by + # the builder. The probe loop still keeps the first body with valid outbounds. + subscription_format_preference="$(uci -q get "netshift.${section}.subscription_format_preference" 2>/dev/null)" + [ -n "$subscription_format_preference" ] || subscription_format_preference="auto" candidates_file="${tmpfile}.ua" - if ! build_subscription_user_agent_candidates "$configured_user_agent" "$cached_user_agent" > "$candidates_file"; then + if ! build_subscription_user_agent_candidates "$configured_user_agent" "$cached_user_agent" "$subscription_format_preference" > "$candidates_file"; then log "Failed to build subscription User-Agent candidate list for section '$section'" "error" rm -f "$tmpfile" "$candidates_file" return 11 diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 799c6dae..41cbacce 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -28,6 +28,12 @@ TMP_SUBSCRIPTION_MERGE_FOLDER="$TMP_SING_BOX_FOLDER/subscription-merge" # "singbox/<version>" candidate is prepended at runtime (it depends on the # installed sing-box). Order matters: most-likely-to-work first. SUBSCRIPTION_USER_AGENT_CANDIDATES="v2rayN Happ Hiddify Clash.Meta ClashMetaForAndroid" +# Subset of SUBSCRIPTION_USER_AGENT_CANDIDATES that well-known panels answer with +# an Xray JSON body (which carries xhttp/transport nodes the default sing-box JSON +# may omit). Used by build_subscription_user_agent_candidates when a section's +# subscription_format_preference is "xray": these UAs are probed FIRST so an +# Xray-JSON feed is recovered before a sing-box JSON under the default UA wins. +SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES="v2rayN Happ" CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker JQ_REQUIRED_VERSION="1.7.1" COREUTILS_BASE64_REQUIRED_VERSION="9.7" diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index d0cbeef4..a298a463 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -729,13 +729,24 @@ get_subscription_user_agent() { # Arguments: # $1 - configured User-Agent (empty for auto mode) # $2 - preferred User-Agent (e.g. the previously cached winner; tried early) +# $3 - format preference: "auto" (default) | "xray" | "singbox". Reorders the +# auto-mode candidates so the preferred FORMAT's UA is probed first; the +# probe loop still keeps the first body that yields valid outbounds. # Behavior: -# - configured non-empty: emit ONLY that value (respect the user's choice). -# - auto: emit "singbox/<ver>", then the preferred one, then the whitelist -# from constants (SUBSCRIPTION_USER_AGENT_CANDIDATES), skipping duplicates. +# - configured non-empty: emit ONLY that value (respect the user's choice; +# an explicit UA always outranks the format preference). +# - auto/empty/unrecognised: emit "singbox/<ver>", then the preferred one, +# then the whitelist (SUBSCRIPTION_USER_AGENT_CANDIDATES) — today's order. +# - xray: emit the Xray-JSON UAs (SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES) +# FIRST (outranking the cached winner + default), then "singbox/<ver>", +# then the preferred one, then the rest of the whitelist. +# - singbox: same as auto (singbox/<ver> first); the explicit name for the +# current default ordering. +# All orderings are de-duplicated with the newline "seen" set below. build_subscription_user_agent_candidates() { local configured_user_agent="${1:-}" local preferred_user_agent="${2:-}" + local format_preference="${3:-}" local default_user_agent candidate seen if [ -n "$configured_user_agent" ]; then @@ -745,8 +756,21 @@ build_subscription_user_agent_candidates() { default_user_agent="$(get_subscription_user_agent)" seen="" - # shellcheck disable=SC2086 # word-splitting of the candidate list is intentional - for candidate in "$default_user_agent" "$preferred_user_agent" $SUBSCRIPTION_USER_AGENT_CANDIDATES; do + + # Order the auto-mode candidate stream by the requested format preference. + # "xray" front-loads the Xray-JSON-yielding UAs (so they outrank the cached + # winner and the default); "singbox"/"auto"/empty/unknown keep today's order + # (default UA -> cached winner -> whitelist). Any unknown value falls through + # to the default ordering (forward-compatible). + if [ "$format_preference" = "xray" ]; then + # shellcheck disable=SC2086 # word-splitting of the candidate lists is intentional + set -- $SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES "$default_user_agent" "$preferred_user_agent" $SUBSCRIPTION_USER_AGENT_CANDIDATES + else + # shellcheck disable=SC2086 # word-splitting of the candidate list is intentional + set -- "$default_user_agent" "$preferred_user_agent" $SUBSCRIPTION_USER_AGENT_CANDIDATES + fi + + for candidate in "$@"; do [ -n "$candidate" ] || continue # Skip a candidate already emitted. Wrap stored names in newlines so the # substring test matches whole entries only. diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index a03f354c..07b5dc9b 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1340,6 +1340,91 @@ else echo "fb-caseI-configured-only(got '$caseI_conf' lines=$caseI_conf_lines):FAIL" fi +# (c2) Empty preference (auto via 3rd arg) keeps today's order: default first. +caseI_emptyp="$(build_subscription_user_agent_candidates "" "" "")" +caseI_emptyp_first="$(printf '%s\n' "$caseI_emptyp" | sed -n '1p')" +if [ "$caseI_emptyp_first" = "$caseI_default" ]; then + echo 'fb-caseI-emptypref-default-first:OK' +else + echo "fb-caseI-emptypref-default-first(got '$caseI_emptyp_first'):FAIL" +fi + +# (d) auto preference keeps today's order: default singbox/<ver> first. +caseI_autop="$(build_subscription_user_agent_candidates "" "" "auto")" +caseI_autop_first="$(printf '%s\n' "$caseI_autop" | sed -n '1p')" +if [ "$caseI_autop_first" = "$caseI_default" ]; then + echo 'fb-caseI-autopref-default-first:OK' +else + echo "fb-caseI-autopref-default-first(got '$caseI_autop_first'):FAIL" +fi + +# (e) singbox preference: default singbox/<ver> first (defined behaviour). +caseI_sbp="$(build_subscription_user_agent_candidates "" "Hiddify" "singbox")" +caseI_sbp_first="$(printf '%s\n' "$caseI_sbp" | sed -n '1p')" +if [ "$caseI_sbp_first" = "$caseI_default" ]; then + echo 'fb-caseI-singboxpref-default-first:OK' +else + echo "fb-caseI-singboxpref-default-first(got '$caseI_sbp_first'):FAIL" +fi + +# (f) xray preference: the Xray-JSON UAs (Happ/v2rayN) come FIRST — before the +# default singbox/<ver> AND before the cached preferred winner. Pass a cached +# preferred ('Hiddify') to prove the xray UAs outrank it. +caseI_xray="$(build_subscription_user_agent_candidates "" "Hiddify" "xray")" +caseI_xray_first="$(printf '%s\n' "$caseI_xray" | sed -n '1p')" +caseI_xray_second="$(printf '%s\n' "$caseI_xray" | sed -n '2p')" +# Position helper: line number of an exact match (empty if absent). +caseI_pos() { printf '%s\n' "$1" | grep -Fxn "$2" | head -n1 | cut -d: -f1; } +caseI_xray_p_v2rayn="$(caseI_pos "$caseI_xray" 'v2rayN')" +caseI_xray_p_happ="$(caseI_pos "$caseI_xray" 'Happ')" +caseI_xray_p_default="$(caseI_pos "$caseI_xray" "$caseI_default")" +caseI_xray_p_pref="$(caseI_pos "$caseI_xray" 'Hiddify')" +# First two lines are the xray subset "v2rayN Happ" (in constant order). +if [ "$caseI_xray_first" = 'v2rayN' ] && [ "$caseI_xray_second" = 'Happ' ]; then + echo 'fb-caseI-xraypref-xray-first:OK' +else + echo "fb-caseI-xraypref-xray-first(1st='$caseI_xray_first' 2nd='$caseI_xray_second'):FAIL" +fi +# xray UAs precede the default and the cached preferred winner. +if [ -n "$caseI_xray_p_v2rayn" ] && [ -n "$caseI_xray_p_happ" ] && + [ -n "$caseI_xray_p_default" ] && [ -n "$caseI_xray_p_pref" ] && + [ "$caseI_xray_p_v2rayn" -lt "$caseI_xray_p_default" ] && + [ "$caseI_xray_p_happ" -lt "$caseI_xray_p_default" ] && + [ "$caseI_xray_p_v2rayn" -lt "$caseI_xray_p_pref" ] && + [ "$caseI_xray_p_happ" -lt "$caseI_xray_p_pref" ]; then + echo 'fb-caseI-xraypref-outranks-default-and-cache:OK' +else + echo "fb-caseI-xraypref-outranks-default-and-cache(v2rayN=$caseI_xray_p_v2rayn happ=$caseI_xray_p_happ def=$caseI_xray_p_default pref=$caseI_xray_p_pref):FAIL" +fi +# Dedup holds: no UA emitted twice (each of v2rayN/Happ/default appears once). +caseI_xray_v2rayn_count="$(printf '%s\n' "$caseI_xray" | grep -Fxc 'v2rayN')" +caseI_xray_happ_count="$(printf '%s\n' "$caseI_xray" | grep -Fxc 'Happ')" +caseI_xray_def_count="$(printf '%s\n' "$caseI_xray" | grep -Fxc "$caseI_default")" +if [ "$caseI_xray_v2rayn_count" = "1" ] && [ "$caseI_xray_happ_count" = "1" ] && + [ "$caseI_xray_def_count" = "1" ]; then + echo 'fb-caseI-xraypref-dedup:OK' +else + echo "fb-caseI-xraypref-dedup(v2rayN=$caseI_xray_v2rayn_count happ=$caseI_xray_happ_count def=$caseI_xray_def_count):FAIL" +fi + +# (g) Unrecognised preference falls back to auto order: default first. +caseI_unk="$(build_subscription_user_agent_candidates "" "Hiddify" "totally-bogus")" +caseI_unk_first="$(printf '%s\n' "$caseI_unk" | sed -n '1p')" +if [ "$caseI_unk_first" = "$caseI_default" ]; then + echo 'fb-caseI-unknownpref-auto-first:OK' +else + echo "fb-caseI-unknownpref-auto-first(got '$caseI_unk_first'):FAIL" +fi + +# (h) Explicit configured UA still short-circuits regardless of preference. +caseI_conf_xray="$(build_subscription_user_agent_candidates "MyClient/1.0" "Hiddify" "xray")" +caseI_conf_xray_lines="$(printf '%s\n' "$caseI_conf_xray" | grep -c .)" +if [ "$caseI_conf_xray" = "MyClient/1.0" ] && [ "$caseI_conf_xray_lines" = "1" ]; then + echo 'fb-caseI-configured-overrides-pref:OK' +else + echo "fb-caseI-configured-overrides-pref(got '$caseI_conf_xray' lines=$caseI_conf_xray_lines):FAIL" +fi + # ── CASE J: subscription keyword whitelist/blacklist filter ───────── # Drive sing_box_cf_prepare_subscription_batch directly with a synthetic # subscription JSON and assert kept counts/names for include/exclude lists. From aa377d56fd5fb3bb3e3b44514e7e817e9d35608d Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Wed, 10 Jun 2026 18:25:19 +0300 Subject: [PATCH 64/75] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=BC=D0=B0?= =?UTF-8?q?=D1=80=D1=88=D1=80=D1=83=D1=82=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 54 +++++ .../memory/shell-backend-developer.md | 70 +++++++ netshift/files/usr/bin/netshift | 23 ++- .../files/usr/lib/sing_box_config_manager.sh | 8 +- tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 191 +++++++++++++++++- 6 files changed, 343 insertions(+), 5 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 4dca0c9c..aaa9d719 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -770,3 +770,57 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> test_backup_integrity. Worth a dedicated task to migrate fb/rh parsers so these assertions are truly gated. Until then, trust the per-token ✓/✗ marks, not just "Results: N passed". + +## task-033 CRITICAL multi-section regression 0.8.5->0.8.6 (2026-06-10) + +- USER REPORTS: after 0.8.5->0.8.6 upgrade, creating ANY extra connection section + (even URL-only / unreachable endpoint) black-holes ALL networking: sing-box + stays up but every flow -> `outbound/direct[direct-out]: i/o timeout`, DNS-over- + proxy n/a on all servers, some report sing-box won't come up. Rollback to 0.8.5 + fixes it. +- DIAGNOSIS METHOD (reusable): bisected 0.8.5..0.8.6 with an explore agent, then + PROVED config-gen is NOT the bug by generating a 2-section (ss+hysteria2) config + in the OWRT smoke container with a DEVICE-FAITHFUL uci (real /etc/config/netshift + + config_load via /lib/functions.sh) -> valid, both outbounds, sing-box check + PASS. LANDMINE I hit: a harness that points NETSHIFT_CONFIG at a RAW uci FILE and + calls config_load on it returns ALL-EMPTY config_get (incl. the first section) -> + looks like "hysteria2 wipes config" but it's a HARNESS ARTIFACT, not a bug. + ALWAYS repro device-faithfully (real `uci`/`/etc/config`), never a raw-file + config_load. I distrusted the artifact and re-verified -> correct call. +- ROOT CAUSE: the nft marking-model rewrite (commit 03806d7 "ipv6+doh-block", + finalized d391e32 which deleted @netshift_subnets/NFT_COMMON_SET_NAME). 0.8.5 + marked ONLY traffic destined to @netshift_subnets + FakeIP range into sing-box + (destination-selective = fail-open by construction). 0.8.6 marks ALL LAN tcp/udp + into sing-box (FAKEIP_MARK 0x00100000) with route.final=direct-out and lets + sing-box decide via FakeIP DNS. THE BUG (confirmed on a LIVE kernel by the + backend dev): sing-box's OWN egress (direct-out, proxy-server dials, DNS upstream) + inherits the tproxy SO_MARK 0x00100000, gets re-caught by + `ip rule fwmark 0x100000/0x100000 table 105` (-> local default dev lo -> tproxy) + -> LOOPS -> i/o timeout. And nothing ever APPLIED NFT_OUTBOUND_MARK (0x00200000) + to sing-box egress (it was referenced ONLY in the dead `mangle_output meta mark + 0x00200000 return` rule). 0.8.5 masked this because unrelated traffic never + entered sing-box. So one not-ready section poisons the WHOLE shared pipeline. +- FIX (task-033, APPROVED, smoke 131/0): emit sing-box `route.default_mark = + NFT_OUTBOUND_MARK` so ALL sing-box egress is stamped 0x00200000. The ip rules + match ONLY 0x00100000 (FAKEIP) and the two marks are DISJOINT BITS (bit20 vs + bit21), so 0x00200000 egress escapes to the main table + the existing + mangle_output return rule fires -> fail-open restored. 2-line surgical change: + sing_box_cm_configure_route gained an OPTIONAL 6th arg default_mark (emitted as a + jq NUMBER via tonumber; empty arg = byte-identical to legacy 5-arg); bin/netshift + sing_box_configure_route computes `$(( NFT_OUTBOUND_MARK ))` (ash hex->decimal + 2097152 because jq tonumber can't parse hex) and passes it to BOTH route branches + (auto-detect + explicit) -> covers v4+v6. NO sacred VALUE changed (only APPLIES + the existing constant). Features preserved (IPv6/DoH-block/per-section/DNS-over- + proxy untouched). New test_section_isolation (alias isolation) incl. a LIVE-kernel + proof: si-live-loop-fakeip-blackholes (reproduces bug) + si-live-loop-outbound- + escapes (proves fix). Whole-chain verified: device-faithful 2-section config has + route.default_mark:2097152, sing-box check PASS. +- REUSABLE: nft `ip rule fwmark X/X` is a MASKED match -> egress stamped with a + DISJOINT-bit mark escapes. The standard sing-box tproxy "mark everything" model + REQUIRES route.default_mark (or per-outbound routing_mark) on sing-box egress, or + its own connections loop back into tproxy. When reviewing "mark everything" + tproxy designs, always check that sing-box egress carries the escape mark. +- READY for manual commit (agents never auto-commit). NOT yet tested on real + hardware with multiple sections — the live-kernel loop/escape proof is in the + smoke container (egress mark chain + real ip rule); a real LAN-client end-to-end + path needs a device (container busybox ip lacks netns/veth). diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 2481ae22..c61ef736 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -868,3 +868,73 @@ findings; keep under ~200 lines. - GOTCHA: `test_rejected_hash` emits `rh-case1/2/6:FAIL` sub-tokens that print red but are NOT counted by the global tally (suite still EXIT=0, 127/0) — this is PRE-EXISTING on the clean tree (verified by git stash), not a regression. + +## task-033: multi-section direct-out i/o timeout — egress mark gap (route.default_mark) + +- ROOT CAUSE (confirmed on a LIVE kernel in the smoke container): the 0.8.6 + "mark everything" nft model marks ALL LAN tcp/udp with NFT_FAKEIP_MARK + (0x00100000) in `mangle` prerouting, and `ip rule fwmark 0x00100000 lookup + netshift` (table `netshift` = `local default dev lo`) redirects it to tproxy. + But NOTHING stamped a mark on sing-box's OWN egress. So sing-box's outbound + sockets — especially `direct-out`, which under route.final=direct-out now + carries ALL unmatched + proxy-server-dial + DNS-upstream traffic — inherited + the tproxy SO_MARK (0x00100000), and the SAME `ip rule` re-captured them into + `local lo` -> looped back into tproxy -> never reached the internet. That is + the exact `outbound/direct[direct-out]: i/o timeout` triad (+ DNS n/a, since + the DNS upstream egress looped too). 0.8.5 masked it: destination-selective + marking meant unrelated traffic never entered sing-box. +- LIVE PROOF (decisive, reproducible with only busybox curl+nft+ip in the + container — NO netns/veth needed): install the real `ip rule fwmark + 0x00100000 lookup netshift` + a tiny `type route hook output` nft chain that + sets a chosen mark on egress to a test IP, then `curl -m`: + * egress UNMARKED -> rc=301 ~0.1s (control, reaches internet) + * egress mark 0x00100000 (FAKEIP) -> rc=28 timeout 5s = LOOP/BLACKHOLE (bug) + * egress mark 0x00200000 (OUTBOUND) -> rc=301 ~0.15s = ESCAPES (fix) + Because `ip rule 105` matches ONLY 0x00100000, a 0x00200000-marked egress + falls through to the main table and egresses normally. +- FIX (minimal, fail-open, B-variant per spec — keep mark-everything but make it + fail OPEN): emit `route.default_mark = NFT_OUTBOUND_MARK` so every sing-box + egress connection is stamped 0x00200000. Per sing-box docs, route.default_mark + is "Set routing mark by default" (Linux), overridden by outbound.routing_mark. + This makes ALL egress (direct-out, proxy-server dials, DNS upstream) escape + the `ip rule`, and the EXISTING `mangle_output meta mark 0x00200000 return` + rule (previously dead — nothing set it) now actually fires to keep that egress + out of the proxy chain. NO sacred VALUE changed — only newly APPLYING the + existing NFT_OUTBOUND_MARK constant to egress (explicitly allowed by the spec). +- IMPL: `sing_box_cm_configure_route` gained optional 6th arg `default_mark` + (sing_box_config_manager.sh) merged conditionally + (`+ (if $default_mark != "" then {default_mark: ($default_mark|tonumber)} + else {} end)`) — EMPTY arg is byte-identical to the legacy 5-arg call + (back-compat asserted in smoke). Caller `sing_box_configure_route` + (bin/netshift) derives `default_egress_mark=$(( NFT_OUTBOUND_MARK ))` (ash + arithmetic converts the hex `0x00200000` -> decimal `2097152`; sing-box + default_mark is an INTEGER, jq `tonumber` does NOT parse hex so pass decimal) + and passes it to BOTH branches (auto-detect "" iface + explicit iface). +- FEATURES PRESERVED: default_mark is route-global -> applies to v4 AND v6 egress + (both ip rules match only FAKEIP_MARK, so both escape). DoH-block / per-section + route rules / DNS-over-proxy are route.rules / dns.servers — untouched. IPv6 + inbounds/sets/rules untouched. Verified v6 + explicit-interface variants both + carry default_mark=2097152 and `sing-box check` passes. +- SMOKE: new top-level `test_section_isolation` (alias `isolation`, after + test_nft_ipv6). 8 asserts: (a) config-gen contract — default_mark present as a + NUMBER == decimal NFT_OUTBOUND_MARK, distinct from FAKEIP mark, empty-arg + byte-parity + key-omitted; (b) 2-section config (section 2 hysteria2 + UNREACHABLE) with the REAL generated route passes `sing-box check` + carries + default_mark; (c) LIVE-kernel fail-open proof (gated on nft+curl+net+!SKIP): + fakeip-marked egress black-holes, outbound-marked egress escapes. Registered + all 5 points (all)/case alias/usage line/docker-compose comment). +- GOTCHA repeats bitten: (1) `$$` inside a heredoc driver expands to the + DRIVER's pid, not the entrypoint's — pass output paths in via sed + placeholder, don't rely on `$$` matching across the two shells. (2) The + generic `*:OK)` case pattern does NOT match a line with trailing text like + `si-...:OK (2097152, number)` — use `*:OK*`/`*:FAIL*` (FAIL first). (3) + Under the suite's `set -e`, bare `ip -4 rule del ... 2>/dev/null` / + `nft delete ...` abort the whole function when the object is absent — guard + EACH with `|| true`. (4) Driver-extracted route needs + `del(.route.rules[]?.__service_tag)` before `sing-box check` (the strip + normally happens in sing_box_cm_save_config_to_file's walk()). +- shellcheck -S error clean (bin + sing_box_config_manager.sh + install.sh); + `smoke-tests all` = 131 passed / 0 failed (was 127 baseline; `isolation`'s 4 + direct pass calls counted, its 8 piped-while ✓ marks are the source of truth, + all green). The pre-existing `rh-case1/2/6:FAIL` red marks persist (documented + task-031 quirk, suite still EXIT=0). diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index e3062f70..cb0ffbd4 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -2320,13 +2320,32 @@ sing_box_configure_route() { fi fi + # Stamp every sing-box-originated egress connection with NFT_OUTBOUND_MARK + # (route.default_mark). The nft mangle prerouting chain marks ALL LAN traffic + # with NFT_FAKEIP_MARK and `ip rule ... fwmark NFT_FAKEIP_MARK lookup + # netshift` redirects it to the tproxy path. Without an egress mark, + # sing-box's own outbound sockets (especially direct-out, which carries ALL + # unmatched traffic) inherit the tproxy SO_MARK (NFT_FAKEIP_MARK), so the + # `ip rule` re-captures them into `local default dev lo` -> they loop back + # into tproxy and never reach the internet (the 0.8.6 "direct-out i/o + # timeout" regression). Marking egress with NFT_OUTBOUND_MARK makes the + # `ip rule` (which matches only NFT_FAKEIP_MARK) skip these packets so they + # egress via the main table, and the existing `mangle_output meta mark + # NFT_OUTBOUND_MARK return` rule keeps them out of the proxy chain. This is + # fail-open: unmatched/unproxied traffic always reaches the internet even + # when a section's outbound is unreachable. sing-box default_mark is an + # integer; convert the hex constant to decimal via ash arithmetic. + local default_egress_mark + default_egress_mark=$(( NFT_OUTBOUND_MARK )) + local output_network_interface config_get output_network_interface "settings" "output_network_interface" if [ -z "$output_network_interface" ]; then - config=$(sing_box_cm_configure_route "$config" "$route_final" true "$SB_DNS_SERVER_TAG") + config=$(sing_box_cm_configure_route "$config" "$route_final" true "$SB_DNS_SERVER_TAG" "" \ + "$default_egress_mark") else config=$(sing_box_cm_configure_route "$config" "$route_final" false "$SB_DNS_SERVER_TAG" \ - "$output_network_interface") + "$output_network_interface" "$default_egress_mark") fi local sniff_inbounds sniff_inbounds_csv diff --git a/netshift/files/usr/lib/sing_box_config_manager.sh b/netshift/files/usr/lib/sing_box_config_manager.sh index 1e4c2c9d..2baccd5a 100644 --- a/netshift/files/usr/lib/sing_box_config_manager.sh +++ b/netshift/files/usr/lib/sing_box_config_manager.sh @@ -1197,10 +1197,13 @@ sing_box_cm_add_selector_outbound() { # auto_detect_interface: boolean, enable or disable automatic interface detection # default_domain_resolver: string, default DNS resolver for domain-based routing # default_interface: string, default network interface to use when auto detection is disabled (optional) +# default_mark: integer, routing mark stamped on sing-box's own egress +# connections so the nft/ip-rule tproxy path does not re-capture them +# (optional; empty -> omitted, byte-identical to the no-mark output) # Outputs: # Writes updated JSON configuration to stdout # Example: -# CONFIG=$(sing_box_cm_configure_route "$CONFIG" "direct-out" true "udp-server") +# CONFIG=$(sing_box_cm_configure_route "$CONFIG" "direct-out" true "udp-server" "" 2097152) ####################################### sing_box_cm_configure_route() { local config="$1" @@ -1208,12 +1211,14 @@ sing_box_cm_configure_route() { local auto_detect_interface="$3" local default_domain_resolver="$4" local default_interface="$5" + local default_mark="$6" echo "$config" | jq \ --arg final "$final" \ --argjson auto_detect_interface "$auto_detect_interface" \ --arg default_domain_resolver "$default_domain_resolver" \ --arg default_interface "$default_interface" \ + --arg default_mark "$default_mark" \ '.route = { rules: (.route.rules // []), rule_set: (.route.rule_set // []), @@ -1222,6 +1227,7 @@ sing_box_cm_configure_route() { default_domain_resolver: $default_domain_resolver } + (if $default_interface != "" then { default_interface: $default_interface } else {} end) + + (if $default_mark != "" then { default_mark: ($default_mark | tonumber) } else {} end) ' } diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 4fe062b2..cb391f10 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -9,7 +9,7 @@ # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, -# nftv6, diagnostics, subscription, insecure, rejected, +# nftv6, isolation, diagnostics, subscription, insecure, rejected, # jobstate, selfheal, dnsdetour, globalproxy, stablecheck, # extcheck, netshiftcheck, selfupdate, backupguard # ────────────────────────────────────────────────────────────────── diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 07b5dc9b..6cc9d41a 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -426,6 +426,193 @@ test_nft_ipv6() { nft delete table inet "$test_table" 2>/dev/null } +# ───────────────────────────────────────────────────────────────── +# Test: Section-isolation invariant (task-033) +# +# Regression: upgrading 0.8.5 -> 0.8.6 made ANY additional / not-ready / +# unreachable section black-hole ALL traffic to outbound/direct[direct-out] +# with `i/o timeout` (+ DNS n/a). Root cause (confirmed on a live kernel): +# the nft `mangle` prerouting chain marks ALL LAN tcp/udp with NFT_FAKEIP_MARK +# and `ip rule ... fwmark NFT_FAKEIP_MARK lookup netshift` redirects it to +# tproxy, but NOTHING stamped a mark on sing-box's OWN egress. So sing-box's +# direct-out sockets (which now carry ALL unmatched traffic) inherited the +# tproxy SO_MARK (NFT_FAKEIP_MARK) and the `ip rule` re-captured them into +# `local default dev lo` -> they looped back into tproxy and timed out. +# +# Fix: sing_box_cm_configure_route now emits route.default_mark = +# NFT_OUTBOUND_MARK, so every sing-box egress connection is marked +# NFT_OUTBOUND_MARK. The `ip rule` matches only NFT_FAKEIP_MARK, so the marked +# egress escapes via the main table (fail-open) and the existing +# `mangle_output meta mark NFT_OUTBOUND_MARK return` rule keeps it out of the +# proxy chain. +# +# This test pins (a) the config-gen contract (default_mark present, decimal, +# == NFT_OUTBOUND_MARK; empty-arg path byte-identical for back-compat), (b) +# that sing-box accepts a 2-section config (one outbound unreachable) with the +# generated route, and (c) — when runnable on the live kernel — that an egress +# packet carrying NFT_OUTBOUND_MARK reaches the internet while one carrying +# NFT_FAKEIP_MARK loops/black-holes (the exact mechanism of the regression). +# ───────────────────────────────────────────────────────────────── +test_section_isolation() { + header "Section-isolation invariant (task-033)" + + if ! command -v sing-box > /dev/null 2>&1; then + skip "sing-box not installed" + return + fi + + local lib="${NETSHIFT_LIB_DIR}" + local cm_lib="$lib/sing_box_config_manager.sh" + local const_lib="$lib/constants.sh" + if [ ! -r "$cm_lib" ] || [ ! -r "$const_lib" ]; then + fail "sing_box_config_manager.sh / constants.sh not found" + return + fi + + # ── (a) config-gen contract: default_mark present + correct + back-compat ── + local drv="/tmp/test-section-isolation-$$.sh" + cat > "$drv" << 'SIEOF' +. "CONST_LIB" +. "CM_LIB" + +mark_dec=$(( NFT_OUTBOUND_MARK )) +seed='{"route":{},"outbounds":[{"type":"direct","tag":"direct-out"}]}' + +# WITH a default_mark (the fix path): assert it lands as a NUMBER equal to the +# decimal NFT_OUTBOUND_MARK. +with=$(sing_box_cm_configure_route "$seed" "direct-out" true "dns-server" "" "$mark_dec") +got=$(echo "$with" | jq -r '.route.default_mark // "MISSING"') +got_type=$(echo "$with" | jq -r '.route.default_mark | type') +if [ "$got" = "$mark_dec" ] && [ "$got_type" = "number" ]; then + echo "si-default-mark-present:OK ($got, $got_type)" +else + echo "si-default-mark-present:FAIL (got '$got' type '$got_type', want '$mark_dec' number)" +fi + +# The mark must NOT collide with NFT_FAKEIP_MARK (which the ip rule catches). +if [ "$mark_dec" != "$(( NFT_FAKEIP_MARK ))" ]; then + echo "si-mark-distinct-from-fakeip:OK" +else + echo "si-mark-distinct-from-fakeip:FAIL (egress mark == fakeip mark -> would still loop)" +fi + +# WITHOUT a default_mark (empty 6th arg): must be byte-identical to the legacy +# 5-arg call (back-compat for any other caller / the off path). +empty6=$(sing_box_cm_configure_route "$seed" "direct-out" true "dns-server" "" "") +legacy5=$(sing_box_cm_configure_route "$seed" "direct-out" true "dns-server" "") +if [ "$(echo "$empty6" | jq -cS .)" = "$(echo "$legacy5" | jq -cS .)" ]; then + echo "si-empty-mark-byte-parity:OK" +else + echo "si-empty-mark-byte-parity:FAIL (empty default_mark changed output)" +fi +if echo "$empty6" | jq -e '.route | has("default_mark")' > /dev/null 2>&1; then + echo "si-empty-mark-omitted:FAIL (default_mark key present when empty)" +else + echo "si-empty-mark-omitted:OK" +fi + +# Emit the generated route (with mark) for the caller to build a full config. +echo "$with" | jq -c 'del(.route.rules[]?.__service_tag) | .route' > "ROUTE_JSON" +SIEOF + local route_json="/tmp/si-route-$$.json" + sed -i "s#CONST_LIB#$const_lib#g; s#CM_LIB#$cm_lib#g; s#ROUTE_JSON#$route_json#g" "$drv" + + rm -f "$route_json" + local out + out="$(ash "$drv" 2>&1 || true)" + echo "$out" | while IFS= read -r line; do + case "$line" in + *:FAIL*) fail "$line" ;; + *:OK*) pass "$line" ;; + esac + done + + # ── (b) 2-section config (section 2 unreachable) + generated route: check ── + local mark_dec + # shellcheck disable=SC1090 + . "$const_lib" + mark_dec=$(( NFT_OUTBOUND_MARK )) + if [ -r "$route_json" ]; then + local cfg="/tmp/si-config-$$.json" + jq -n --slurpfile route "$route_json" '{ + log: { level: "warn" }, + dns: { servers: [ { tag: "dns-server", type: "udp", server: "1.1.1.1" } ], final: "dns-server" }, + inbounds: [ { type: "tproxy", tag: "tproxy-in", listen: "127.0.0.1", listen_port: 1602 } ], + outbounds: [ + { type: "direct", tag: "direct-out" }, + { type: "shadowsocks", tag: "main-out", server: "10.10.10.10", server_port: 8388, method: "aes-256-gcm", password: "password" }, + { type: "hysteria2", tag: "second-out", server: "198.51.100.99", server_port: 443, password: "pass", tls: { enabled: true, insecure: true } } + ], + route: $route[0] + }' > "$cfg" + if sing-box check -c "$cfg" > /dev/null 2>&1; then + pass "si-2section-unreachable-check:OK (sing-box accepts config + route.default_mark)" + else + fail "si-2section-unreachable-check:FAIL" "$(sing-box check -c "$cfg" 2>&1)" + fi + # the generated route must actually carry default_mark + if [ "$(jq -r '.route.default_mark' "$cfg")" = "$mark_dec" ]; then + pass "si-config-has-default-mark:OK" + else + fail "si-config-has-default-mark:FAIL" + fi + rm -f "$cfg" + else + fail "si-route-gen:FAIL (route JSON not produced)" + fi + + # ── (c) live-kernel fail-open mechanism (only if nft + curl + net) ────────── + # Build the EXACT ip rule the backend installs (fwmark NFT_FAKEIP_MARK -> + # table netshift = local default dev lo) and prove: + # - egress carrying NFT_FAKEIP_MARK loops/black-holes (the bug) + # - egress carrying NFT_OUTBOUND_MARK (the fix) reaches the internet + if [ "${TEST_SKIP_NETWORK:-0}" = "1" ]; then + skip "si-live-loop: network skipped (TEST_SKIP_NETWORK=1)" + elif ! command -v nft > /dev/null 2>&1 || ! command -v curl > /dev/null 2>&1; then + skip "si-live-loop: nft/curl not available" + elif ! curl -s -m 5 -o /dev/null http://1.1.1.1/ 2>/dev/null; then + skip "si-live-loop: no outbound connectivity in container" + else + # All ip/nft mutations are best-effort and may legitimately return + # non-zero (rule absent on first del, etc.); guard each against `set -e`. + grep -q "105 netshift" /etc/iproute2/rt_tables 2>/dev/null || \ + echo "105 netshift" >> /etc/iproute2/rt_tables + ip -4 route replace local 0.0.0.0/0 dev lo table netshift 2>/dev/null || \ + ip -4 route add local 0.0.0.0/0 dev lo table netshift 2>/dev/null || true + ip -4 rule del fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table netshift priority 105 2>/dev/null || true + ip -4 rule add fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table netshift priority 105 2>/dev/null || true + + nft delete table inet netshift_si_test 2>/dev/null || true + nft add table inet netshift_si_test 2>/dev/null || true + nft add chain inet netshift_si_test out \ + '{ type route hook output priority -200; policy accept; }' 2>/dev/null || true + + # FAKEIP_MARK egress -> must loop (curl times out, rc!=0). + nft flush chain inet netshift_si_test out 2>/dev/null || true + nft add rule inet netshift_si_test out ip daddr 1.0.0.1 meta mark set "$NFT_FAKEIP_MARK" counter 2>/dev/null || true + if curl -s -m 6 -o /dev/null http://1.0.0.1/ 2>/dev/null; then + fail "si-live-loop-fakeip-blackholes:FAIL (fakeip-marked egress unexpectedly escaped)" + else + pass "si-live-loop-fakeip-blackholes:OK (fakeip-marked egress loops, as in the bug)" + fi + + # OUTBOUND_MARK egress (the fix) -> must reach the internet (rc==0). + nft flush chain inet netshift_si_test out 2>/dev/null || true + nft add rule inet netshift_si_test out ip daddr 1.0.0.1 meta mark set "$NFT_OUTBOUND_MARK" counter 2>/dev/null || true + if curl -s -m 6 -o /dev/null http://1.0.0.1/ 2>/dev/null; then + pass "si-live-loop-outbound-escapes:OK (outbound-marked egress reaches internet -> fail-open)" + else + fail "si-live-loop-outbound-escapes:FAIL (outbound-marked egress did NOT escape)" + fi + + nft delete table inet netshift_si_test 2>/dev/null || true + ip -4 rule del fwmark "$NFT_FAKEIP_MARK"/"$NFT_FAKEIP_MARK" table netshift priority 105 2>/dev/null || true + ip -4 route flush table netshift 2>/dev/null || true + fi + + rm -f "$drv" "$route_json" +} + # ───────────────────────────────────────────────────────────────── # Test: sing-box Config Generation # ───────────────────────────────────────────────────────────────── @@ -3930,6 +4117,7 @@ main() { test_sing_box_config test_nft test_nft_ipv6 + test_section_isolation test_diagnostics test_subscription test_insecure_fetch @@ -3950,6 +4138,7 @@ main() { helpers) test_helpers ;; nft) test_nft ;; nftv6) test_nft_ipv6 ;; + isolation) test_section_isolation ;; diagnostics) test_diagnostics ;; subscription) test_subscription ;; insecure) test_insecure_fetch ;; @@ -3968,7 +4157,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 isolation diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" exit 1 ;; esac From c6fb96254a0d3f4ec2fe9181cf862a8ed5e7b4c6 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Thu, 11 Jun 2026 03:54:01 +0300 Subject: [PATCH 65/75] =?UTF-8?q?=D0=B0=D0=B1=D1=81=D0=BE=D0=BB=D1=8E?= =?UTF-8?q?=D1=82=D0=BD=D0=BE=20=D0=B2=D0=B5=D1=81=D1=8C=20=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=84=D0=B8=D0=BA=20=D0=B8=D0=B4=D1=91=D1=82=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20=D1=81=D0=B8=D0=BD=D0=B3=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=D1=81=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B2=20?= =?UTF-8?q?=D1=81=D0=BB=D1=83=D1=87=D0=B0=D0=B5=20=D0=B3=D0=BB=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=BB=20=D0=BF=D1=80=D0=BE=D0=BA=D1=81=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 66 +++ .../memory/shell-backend-developer.md | 123 ++++++ netshift/files/usr/bin/netshift | 282 +++++++++++- netshift/files/usr/lib/constants.sh | 9 + netshift/files/usr/lib/nft.sh | 66 +++ tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 402 +++++++++++++++++- 7 files changed, 933 insertions(+), 17 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index aaa9d719..db847f8d 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -824,3 +824,69 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> hardware with multiple sections — the live-kernel loop/escape proof is in the smoke container (egress mark chain + real ip rule); a real LAN-client end-to-end path needs a device (container busybox ip lacks netns/veth). + +## task-034 selective marking (CPU regression) + hardware test (2026-06-10) + +- SECOND facet of the 0.8.6 nft "mark everything" regression (sibling of + task-033): users (Oleg etc.) report ALL traffic goes through sing-box even when + only selected lists are proxied -> torrent/4K pins sing-box at 100% CPU on weak + routers (Orange Pi R1 Plus LTS / RK3328). 0.8.5 marked selectively (proxied + subnets + FakeIP range) so direct traffic bypassed sing-box. task-033's + default_mark fixed the egress LOOP, NOT the ingress VOLUME — different bug. +- FIX (Option 1, operator-chosen): restore destination-selective nft marking — + mark (union of proxied subnets) + FakeIP 198.18/15 + DoH-CIDRs (+IPv6 mirror); + mark-EVERYTHING only when global_proxy active. Re-added NFT_COMMON_SET_NAME + (+_V6) set, restored the subnet-population path deleted in d391e32 (feeding the + nft set ALONGSIDE the sing-box ip_cidr rule_set from ONE centralized point so + they can't drift). KEY INSIGHT: nft only decides ENTER-or-not; per-section + outbound selection stays inside sing-box route rules -> a single union set is + correct for multi-section. router-origin stays direct (task-033 default_mark + + mangle_output untouched). global_proxy read in nft layer via + get_global_proxy_section (UCI-only, no side effects). +- RE-OPEN: smoke 143/0 + code-review APPROVED, but ON HARDWARE the live nft chain + was STILL mark-all + netshift_subnets set absent, despite the installed binary + containing the selective code. ROOT: create_nft_rules was NOT IDEMPOTENT — + `nft add chain/rule` only APPEND, the table was never flushed. On the + respawn/upgrade path (no clean stop) a STALE mark-all rule from the prior build + sat ATOP the prerouting chain and marked everything before the selective rules. + clean stop->start was fine (stop_main deletes the table); apk reinstall / procd + respawn / crash was not. FIX: `nft_delete_table` (new nft.sh helper) as the + FIRST action in create_nft_rules -> idempotent rebuild. Strengthened smoke test + Scenario 6 PRESEEDS a stale mark-all table + runs create_nft_rules with NO + external delete -> proves flush clears it (revert flush => 16/1; with => 17/17). + smoke all 148/0. code-review-002 APPROVED. +- HARDWARE VERIFIED (router 192.168.1.101, OWRT25/apk): after the flush fix, a + real `create_nft_rules` (direct + via init restart) produces SELECTIVE rules + (`daddr @netshift_subnets` + `daddr 198.18.0.0/15`, NO `l4proto tcp/udp meta + mark set` mark-all) and CREATES netshift_subnets. Final live chain: 0 mark-all / + 2 selective, sing-box up, internet+DNS OK. The CPU regression is fixed (direct + traffic no longer enters sing-box). Counters 0 on the subnet set only because + that subscription has domain lists (FakeIP), no subnet lists — normal. +- LANDMINE #A (apk equal-version no-overwrite): `apk add --force-reinstall + <pkg>` with the SAME version (0.8.6-r1 over 0.8.6-r1) does NOT reliably + overwrite the on-disk files on OWRT25/apk — I chased a ghost for several steps + thinking the new code was installed when nft.sh was still the old build. To + force a real file refresh, BUMP THE VERSION (built 0.8.7) so apk does a true + upgrade. Always verify a fix landed on-disk by grepping the installed file + (e.g. `grep -c nft_delete_table /usr/lib/netshift/nft.sh`), not by trusting + apk's "force-reinstall". +- LANDMINE #B (apk post-install/upgrade trigger HANGS on OWRT25): the netshift + post-upgrade trigger (which runs the service start/restart) BLOCKS apk + indefinitely (start waits on network/subscription), leaving the apk DB on the + OLD version + holding /lib/apk/db/lock, while the FILES are already unpacked + correctly. Symptom: `apk add` "stuck", `ERROR: Unable to lock database: + Resource temporarily unavailable` on the next call, a zombie `apk add` in + `ps`. Recovery: `pkill -9 apk; rm -f /lib/apk/db/lock`. This is a REAL apk-path + bug worth a packaging fix (post-install must not synchronously block on a + network-dependent service start — enable/start should be detached/non-blocking + or deferred). Functionally the files install fine; only the DB version record + and the trigger hang. Flag to packaging. +- LANDMINE #C (router teardown drops the SSH session): `/etc/init.d/netshift + stop`/`restart` rebuilds DNS/nft and frequently kills the ssh control session + mid-command; run teardown/restart with `&` + a generous sleep, then RE-CONNECT + in a fresh session to read results. Don't trust a truncated command as failure. +- LANDMINE #D (procd respawn contaminates manual nft tests): killing sing-box / + deleting the nft table for an isolated test is instantly undone by procd + respawn / the monitor re-applying the service ruleset; to test create_nft_rules + in isolation you must disable the service (shutdown_correctly=1 + kill monitor) + or do it in the smoke container, not on a live procd-managed router. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index c61ef736..f1cfe28e 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -938,3 +938,126 @@ findings; keep under ~200 lines. direct pass calls counted, its 8 piped-while ✓ marks are the source of truth, all green). The pre-existing `rh-case1/2/6:FAIL` red marks persist (documented task-031 quirk, suite still EXIT=0). + +## task-034 — Destination-selective nft marking (CPU regression fix) + +- ROOT: 0.8.6 marked ALL LAN tcp/udp into tproxy (mangle prerouting), so every + forwarded flow entered sing-box (sniff + full route-rule walk per conn) → + 100% CPU on weak routers. 0.8.5 marked SELECTIVELY: only `@netshift_subnets` + (proxied dest subnets) + FakeIP range `198.18.0.0/15` (proxied domains). + Commit 03806d7 went mark-all; d391e32 then deleted NFT_COMMON_SET_NAME + its + population. Restored Option-1 selective marking. +- KEY INSIGHT: nft only decides ENTER-or-not. Inside sing-box, per-section + ip_cidr/domain route rules still pick `<section>-out`. So ONE union nft set + (all sections' proxied subnets) + FakeIP range gates ingress; multi-section + outbound selection is unaffected. No per-section nft sets needed. +- Re-added `NFT_COMMON_SET_NAME="netshift_subnets"` + v6 mirror + `NFT_COMMON_SET_NAME_V6` to constants.sh (NEW SET, not a changed sacred + value). Created in create_nft_rules (v4 always, v6 only if + netshift_ipv6_enabled). +- New prerouting mangle (DEFAULT, no global_proxy): `ip daddr @localv4 return` + (kept) → `ip daddr @netshift_subnets mark set FAKEIP_MARK` → `ip daddr + 198.18.0.0/15 mark set` → (ipv6: `@netshift_subnets_v6` + `fd00:ec3a::/32`) → + DoH-block CIDRs inline when block_doh=1. NO unconditional mark-all. +- GLOBAL_PROXY OVERRIDE: `get_global_proxy_section` is a standalone UCI-only + helper (reads config_foreach, NOT the `$config` string) so create_nft_rules + can call it directly (it runs separately from sing_box_configure_route). When + non-empty → keep mark-EVERYTHING tcp/udp (global proxy wants all traffic in). +- fully_routed_ips are SOURCE clients (source_ip_cidr) — selective dest marking + would bypass them for direct dests. Added `nft_mark_fully_routed_source_ips` + (config_foreach over proxy/vpn sections) emitting `ip[6] saddr <ip> mark set` + rules in the default branch only (global_proxy already marks all). +- ONE centralized population point (no drift): `populate_netshift_subnets_from_file` + / `_from_string` feed the nft union set (v4 via nft_add_set_elements_from_file_chunked, + v6 via NEW `nft_add_set_elements_from_file_chunked_v6` in nft.sh — splits by + presence of `:`; there is NO is_ipv6 helper). Called ALONGSIDE every sing-box + ip_cidr rule_set population: configure_user_subnet_list (string), + import_local_subnets_list_handler (file), import_community_service_subnet_list_handler + (restored twitter/meta/telegram/cloudflare/hetzner/ovh/digitalocean/cloudfront/ + roblox/discord URLs; discord ALSO keeps its dport set), import_subnets_from_remote_* + (restored json/srs extract via extract_ip_cidr_from_json_ruleset_to_file + + decompile_binary_ruleset; plain too). +- TIMING: user/local subnets populate at config-gen (sing_box_init_config, after + create_nft_rules → set exists). community/remote populate in background + list_update (set exists; ensure_nft_ready_for_list_update recreates table if + missing). reload/restart = stop+start so both regenerate. Matches 0.8.5. +- PRESERVED: dns-in (127.0.0.42:53) is a separate inbound, not via marked + tproxy — localv4 return (127.0.0.0/8) doesn't break DNS steering. Domain + routing via FakeIP range mark. task-033 route.default_mark + mangle_output + router-origin direct UNTOUCHED. discord dport rule + general @set rule both + just set the same mark (idempotent, no shadow/double-mark harm). +- TEST: new `test_selective_marking` (alias `selmark`) awk-extracts the SHIPPED + create_nft_rules + helpers VERBATIM, stubs UCI/predicates via SCN_* env, + builds the REAL ruleset on a real nft table (override NFT_TABLE_NAME), dumps + `nft list chain ... mangle` and asserts: (1) @set+FakeIP present & NO mark-all + (structural: `meta mark set` + `l4proto` line WITHOUT daddr/saddr = mark-all), + (2) direct-IP bypass = that mark-all absent, (3) global_proxy → mark-all + present + @set absent, (4) ipv6 mirror present + no mark-all + FakeIP v6, + (5) fully_routed saddr rule + sing-box check on a 2-section config. 12 asserts + all green on real nft. Registered all)/alias/usage/compose comment. +- GATES: shellcheck -S error clean (bin + nft.sh + constants.sh + install.sh + + tests). `smoke-tests all` = 143 passed / 0 failed. + +## task-034 RE-OPEN: create_nft_rules was NOT idempotent (stale mark-all survived) + +- HARDWARE BUG the first pass + smoke missed: on a real router the LIVE mangle + chain was STILL mark-all + the `netshift_subnets` set was absent, even though + the installed bin had the selective code and `get_global_proxy_section` + returned empty. CONTRADICTION: stubbed-nft trace = selective; real-nft service + start = mark-all. +- HYPOTHESIS VERDICT: **H1 FALSE, H2-variant TRUE.** Faithful container repro + (REAL get_global_proxy_section / _determine_global_proxy_section / + section_has_configured_outbound / get_subscription_urls_for_section via a real + `config_load` of a hardware-shaped config: one subscription proxy section, + global_proxy=0) confirmed `get_global_proxy_section` correctly returns EMPTY → + selective branch taken (H1 false; the mark-all branch is NOT wrongly entered). + The mark-all rules came from a LEFTOVER table (H2): `create_nft_rules` is NOT + idempotent — `nft add chain`/`nft add rule` only ever APPEND, and the function + did `nft add table` (idempotent, no flush) but never deleted the existing + table. So a `NetShiftTable` left behind by a previous start that was not + cleanly stopped — **procd respawn (`command /usr/bin/netshift start` re-run + with no `stop`), in-place package upgrade, or a crash** — keeps its stale + rules and the new selective rules pile on TOP. A stale mark-EVERYTHING rule + (from a prior global_proxy run or the 0.8.6 mark-all build) sits at the TOP of + the prerouting chain and marks ALL traffic before the new destination-selective + rules ever evaluate → "everything proxied / 100% CPU" even though the selective + code is present. Reproduced in-container: two consecutive create_nft_rules + (1st global_proxy, 2nd selective, NO stop between) → final chain had BOTH the + mark-all rules (first) AND the selective rules (dead). `stop_main` DOES delete + the table, so the clean stop→start path was always fine; the bug only bit the + no-stop respawn/upgrade path. `ensure_nft_ready_for_list_update` only calls + create_nft_rules when the table is MISSING, so it was never the rebuild path. +- FIX (minimal, deterministic): new `nft_delete_table` helper in nft.sh (delete + inet table iff it exists, fail-open `2>/dev/null`) called at the TOP of + create_nft_rules (`log "Flush stale nft table before rebuild"; nft_delete_table + "$NFT_TABLE_NAME"`) BEFORE `nft_create_table`. create_nft_rules rebuilds the + whole table (sets+chains+rules) from scratch, so flush-first is safe and makes + it idempotent: the FINAL live chain is always exactly the intended ruleset + regardless of prior state. No sacred VALUE changed; ensure_nft_ready no-op + (table already absent when it calls). task-033 default_mark + mangle_output + untouched. +- WHY THE OLD TEST MISSED IT: `test_selective_marking` (1) STUBBED + `get_global_proxy_section`, so it never ran the real UCI helper, and (2) always + did `nft delete table` first (clean slate) and called create_nft_rules ONCE — + so it never exercised the stale-table/respawn path that is the actual service + reality. +- STRENGTHENED TEST (5 new asserts, now 17 total for selmark, suite 143→148): + * Scenario 6 (THE regression repro): env `SCN_PRESEED=1` makes the driver + leave a STALE mark-all table (full localv4/interface sets + ct/return + + mark-all tcp/udp) then run create_nft_rules ON TOP with REAL nft and NO + delete — faithfully reproducing the respawn/upgrade path. Asserts the FINAL + live chain has NO mark-all (`meta mark set`+`l4proto` line w/o daddr/saddr), + the @set rule IS present, the union set exists, and EXACTLY ONE @set rule + (proves rebuild, not append). + * Scenario 7 (`selective:realgp`): sources real /lib/functions.sh + + /lib/config/uci.sh, awk-extracts the SHIPPED get_global_proxy_section chain, + `config_load`s a real hardware-shaped config (subscription proxy, + global_proxy=0), asserts `GP=[]` → selective branch (kills the stub blind + spot). Skips if LuCI/uci absent. + * PROVEN to catch the bug: reverting the flush makes `selective:respawn — + stale mark-all rule SURVIVED the rebuild` FAIL (16/1); with the fix 17/0. +- GATES: shellcheck -S error clean (bin + nft.sh + constants.sh + install.sh + + tests/entrypoint.sh). `smoke-tests all` = 148 passed / 0 failed (143 + 5). + Pre-existing `rh-case1/2/6:FAIL` red marks persist (documented task-031 quirk; + suite still EXIT=0). No registration change needed (selmark already in + all)/alias/usage/compose). diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index cb0ffbd4..41ac8e18 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -1116,7 +1116,109 @@ nft_init_interfaces_set() { done } +# ── task-034: destination-selective marking population ────────────────────── +# The prerouting `mangle` chain marks ONLY proxied destinations into tproxy. +# Those destinations live in the union nft set NFT_COMMON_SET_NAME (IPv4) and +# its IPv6 mirror NFT_COMMON_SET_NAME_V6. These two helpers are the SINGLE +# centralized population point: every place that adds an ip_cidr to a sing-box +# route rule_set ALSO calls one of these so the nft set and the sing-box +# rule_set cannot drift (the historical 0.8.5 two-sources-of-truth fragility). +# +# Fail-open: if the set does not exist yet (table not built) or a line is +# malformed, that subnet simply isn't marked (goes direct) — never a blackhole. + +# Add the IPv4/IPv6 CIDRs found in a file into the union destination set(s). +populate_netshift_subnets_from_file() { + local filepath="$1" + + [ -f "$filepath" ] || return 0 + + if nft list set inet "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" > /dev/null 2>&1; then + nft_add_set_elements_from_file_chunked "$filepath" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" + fi + + if netshift_ipv6_enabled && + nft list set inet "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME_V6" > /dev/null 2>&1; then + nft_add_set_elements_from_file_chunked_v6 "$filepath" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME_V6" + fi +} + +# Add a comma/space-separated string of CIDRs into the union destination set(s). +# Items are written to a temp file and routed through the file-based path so the +# v4/v6 split logic is shared. +populate_netshift_subnets_from_string() { + local items="$1" + local tmpfile + + [ -n "$items" ] || return 0 + + tmpfile="$(mktemp)" || return 0 + printf '%s\n' "$items" | tr ', ' '\n\n' > "$tmpfile" + populate_netshift_subnets_from_file "$tmpfile" + rm -f "$tmpfile" +} + +# task-034: add source-based mark-all rules for every section's fully_routed_ips +# (LAN clients whose ALL traffic must enter sing-box, regardless of destination). +# Only used in the destination-selective default mode; under global_proxy ALL +# traffic is already marked. Fail-open: a missing/empty list adds no rule. +nft_mark_fully_routed_source_ips() { + config_foreach _nft_mark_fully_routed_ips_for_section "section" +} + +_nft_mark_fully_routed_ips_for_section() { + local section="$1" + local connection_type fully_routed_ips ip + + config_get connection_type "$section" "connection_type" + case "$connection_type" in + proxy | vpn) ;; + *) return 0 ;; + esac + + config_get fully_routed_ips "$section" "fully_routed_ips" + [ -n "$fully_routed_ips" ] || return 0 + + _nft_fully_routed_list_seen="" + config_list_foreach "$section" "fully_routed_ips" _nft_mark_fully_routed_ip_handler + # Fallback for option-form (space-separated) values. + if [ -z "$_nft_fully_routed_list_seen" ]; then + for ip in $fully_routed_ips; do + _nft_mark_fully_routed_ip_handler "$ip" + done + fi +} + +_nft_mark_fully_routed_ip_handler() { + local ip="$1" + + _nft_fully_routed_list_seen=1 + [ -n "$ip" ] || return 0 + + case "$ip" in + *:*) + if netshift_ipv6_enabled; then + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip6 saddr "$ip" meta mark set "$NFT_FAKEIP_MARK" counter + fi + ;; + *) + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip saddr "$ip" meta mark set "$NFT_FAKEIP_MARK" counter + ;; + esac +} + create_nft_rules() { + # Rebuild the table deterministically. `nft add chain`/`nft add rule` only + # ever APPEND, so if a NetShiftTable was left behind by a previous start + # that was not cleanly stopped (procd respawn, in-place package upgrade, or + # a crash), its STALE rules survive and the new rules pile on top. A leftover + # mark-EVERYTHING rule would then sit at the top of the prerouting chain and + # mark all traffic before the destination-selective rules below ever run — + # re-introducing the 100% CPU regression even though the selective code is + # present. Flush first so create_nft_rules always yields exactly this chain. + log "Flush stale nft table before rebuild" + nft_delete_table "$NFT_TABLE_NAME" + log "Create nft table" nft_create_table "$NFT_TABLE_NAME" @@ -1149,6 +1251,12 @@ create_nft_rules() { }' fi + log "Create proxied-subnets union set" + nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" + if netshift_ipv6_enabled; then + nft_create_ipv6_set "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME_V6" + fi + log "Create interface set" nft_init_interfaces_set @@ -1162,8 +1270,61 @@ create_nft_rules() { if netshift_ipv6_enabled; then nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip6 daddr "@$NFT_LOCALV6_SET_NAME" return fi - nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter - nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter + + # task-034: destination-selective marking. + # + # DEFAULT (no global_proxy section): mark ONLY traffic whose DESTINATION is + # - a proxied subnet (@NFT_COMMON_SET_NAME / its v6 mirror), or + # - the FakeIP range (proxied DOMAINS resolve to FakeIPs via the dns-in + # inbound, so marking the FakeIP range carries domain routing in), or + # - a DoH-block CIDR (when block_doh is on, so DoH probes are forced into + # sing-box where the route-level reject rule drops them). + # Everything else (a torrent/4K stream to a random direct IP) is NEVER + # marked -> never enters sing-box -> CPU stays at 0.8.5 levels. + # `fully_routed_ips` are SOURCE clients whose ALL traffic must be proxied, + # so they are marked by source regardless of destination. + # + # GLOBAL_PROXY OVERRIDE: when a global_proxy section is active, ALL LAN + # tcp/udp is legitimately wanted inside sing-box (it routes every unmatched + # flow through the proxy), so we keep the mark-EVERYTHING rules. The same + # condition is read independently here (UCI only, via get_global_proxy_section) + # because create_nft_rules runs separately from sing_box_configure_route. + local nft_global_proxy_section + nft_global_proxy_section="$(get_global_proxy_section)" + if [ -n "$nft_global_proxy_section" ]; then + log "Global proxy section '$nft_global_proxy_section' active: marking ALL LAN traffic into sing-box" "info" + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter + else + # Source-based mark-all for fully_routed_ips clients (regardless of dest). + nft_mark_fully_routed_source_ips + + # Destination-selective: proxied subnets (union set). + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_COMMON_SET_NAME" meta mark set "$NFT_FAKEIP_MARK" counter + # Destination-selective: FakeIP range (proxied domains). + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "$SB_FAKEIP_INET4_RANGE" meta mark set "$NFT_FAKEIP_MARK" counter + if netshift_ipv6_enabled; then + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip6 daddr "@$NFT_COMMON_SET_NAME_V6" meta mark set "$NFT_FAKEIP_MARK" counter + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip6 daddr "$SB_FAKEIP_INET6_RANGE" meta mark set "$NFT_FAKEIP_MARK" counter + fi + + # DoH-block CIDRs (when enabled): force well-known DoH resolver IPs into + # sing-box so the route-level DoH reject rule can drop them. + local nft_block_doh + config_get_bool nft_block_doh "settings" "block_doh" 0 + if [ "$nft_block_doh" -eq 1 ]; then + log "DoH blocking enabled: marking DoH resolver CIDRs into sing-box" "info" + local doh_cidr + for doh_cidr in $DOH_BLOCK_IPV4_CIDRS; do + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "$doh_cidr" meta mark set "$NFT_FAKEIP_MARK" counter + done + if netshift_ipv6_enabled; then + for doh_cidr in $DOH_BLOCK_IPV6_CIDRS; do + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip6 daddr "$doh_cidr" meta mark set "$NFT_FAKEIP_MARK" counter + done + fi + fi + fi nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto tcp tproxy ip to "$SB_TPROXY_INBOUND_ADDRESS:$SB_TPROXY_INBOUND_PORT" counter nft add rule inet "$NFT_TABLE_NAME" proxy meta mark \& "$NFT_FAKEIP_MARK" == "$NFT_FAKEIP_MARK" meta l4proto udp tproxy ip to "$SB_TPROXY_INBOUND_ADDRESS:$SB_TPROXY_INBOUND_PORT" counter @@ -2696,6 +2857,9 @@ configure_user_subnet_list() { items="$(parse_domain_or_subnet_string_to_commas_string "$items" "subnets")" json_array="$(comma_string_to_json_array "$items")" patch_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array" + # task-034: feed the same subnets into the nft union set so the prerouting + # mangle chain marks them into sing-box (centralized alongside the rule_set). + populate_netshift_subnets_from_string "$items" } configure_local_domain_lists() { @@ -2738,6 +2902,8 @@ import_local_subnets_list_handler() { fi import_plain_subnet_list_to_local_source_ruleset_chunked "$local_subnet_list_filepath" "$ruleset_filepath" + # task-034: also feed the nft union set (centralized alongside the rule_set). + populate_netshift_subnets_from_file "$local_subnet_list_filepath" } configure_remote_domain_or_subnet_list_handler() { @@ -2936,14 +3102,34 @@ import_community_service_subnet_list_handler() { # Routing for every community service is carried by a sing-box remote rule # set (see configure_community_list_handler -> $SRS_MAIN_URL/<service>.srs). - # Only discord still needs an nft path here: it has a live dport-restricted - # mangle rule (@netshift_discord_subnets udp dport {...}) that a sing-box - # route rule cannot express, so its subnets must populate that nft set. - # The other services formerly populated @netshift_subnets, but that set was - # matched by NO nft rule (dead path) and is no longer created/populated. + # task-034: the SUBNET-based community services (twitter/meta/telegram/ + # cloudflare/hetzner/ovh/digitalocean/cloudfront/roblox/discord) route by IP, + # not by domain, so their destination IPs would NOT be covered by the FakeIP + # mark rule. We therefore (re)populate the nft union set NFT_COMMON_SET_NAME + # so the prerouting mangle chain marks them into sing-box. Domain-based + # services (youtube, russia_inside, ...) are carried by the FakeIP range + # mark rule and need no nft set population here. + # + # discord is special: it ALSO needs its own dport-restricted mangle rule + # (@netshift_discord_subnets udp dport {...}) that a sing-box route rule + # cannot express, so its subnets populate BOTH the discord set (for the + # dport rule) and the general union set (so TCP/other-UDP discord traffic + # is marked too, without the dport restriction). + local url is_discord + is_discord=0 case "$service" in + "twitter") url=$SUBNETS_TWITTER ;; + "meta") url=$SUBNETS_META ;; + "telegram") url=$SUBNETS_TELERAM ;; + "cloudflare") url=$SUBNETS_CLOUDFLARE ;; + "hetzner") url=$SUBNETS_HETZNER ;; + "ovh") url=$SUBNETS_OVH ;; + "digitalocean") url=$SUBNETS_DIGITALOCEAN ;; + "cloudfront") url=$SUBNETS_CLOUDFRONT ;; + "roblox") url=$SUBNETS_ROBLOX ;; "discord") - URL=$SUBNETS_DISCORD + url=$SUBNETS_DISCORD + is_discord=1 if ! nft list set inet "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" > /dev/null 2>&1; then nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" fi @@ -2960,14 +3146,18 @@ import_community_service_subnet_list_handler() { tmpfile=$(mktemp) http_proxy_address="$(get_service_proxy_address)" - download_to_file "$URL" "$tmpfile" "$http_proxy_address" + download_to_file "$url" "$tmpfile" "$http_proxy_address" if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then log "Download $service list failed" "error" return 1 fi - nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" + if [ "$is_discord" -eq 1 ]; then + nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" + fi + # task-034: feed the union set (centralized with the sing-box .srs rule_set). + populate_netshift_subnets_from_file "$tmpfile" rm -f "$tmpfile" } @@ -3045,12 +3235,20 @@ import_subnets_from_remote_subnet_list_handler() { file_extension="$(url_get_file_extension "$url")" log "Detected file extension: '$file_extension'" "debug" case "$file_extension" in - json | srs) - # JSON/SRS remote subnet lists are routed via a sing-box remote rule set + json) + # JSON/SRS remote subnet lists are ROUTED via a sing-box remote rule set # (configure_remote_domain_or_subnet_list_handler -> sing_box_cm_add_remote_ruleset), - # and sing-box manages their updates automatically. The previous import - # here only fed the dead nft @netshift_subnets set, so it is dropped. - log "No subnet import needed for $file_extension list - sing-box manages updates automatically." + # and sing-box manages their updates automatically. But task-034 needs + # their destination IPs in the nft union set so the prerouting mangle + # chain marks them into sing-box (an .srs/.json ruleset matches IPs that + # are NOT in the FakeIP range). So we extract the ip_cidr entries and + # feed the union set here. + log "Import subnets from a remote JSON list (for nft selective marking)" "info" + import_subnets_from_remote_json_file "$url" + ;; + srs) + log "Import subnets from a remote SRS list (for nft selective marking)" "info" + import_subnets_from_remote_srs_file "$url" ;; *) log "Import subnets from a remote plain-text list" "info" @@ -3059,6 +3257,58 @@ import_subnets_from_remote_subnet_list_handler() { esac } +# task-034: download a remote JSON sing-box ruleset, extract its ip_cidr entries +# and feed the nft union set. Routing is still done by the sing-box remote rule +# set; this only adds the nft-layer ingress gating. Fail-open on download error. +import_subnets_from_remote_json_file() { + local url="$1" + local json_tmpfile subnets_tmpfile http_proxy_address + json_tmpfile="$(mktemp)" + subnets_tmpfile="$(mktemp)" + http_proxy_address="$(get_service_proxy_address)" + + download_to_file "$url" "$json_tmpfile" "$http_proxy_address" + + if [ $? -ne 0 ] || [ ! -s "$json_tmpfile" ]; then + log "Download $url list failed" "error" + rm -f "$json_tmpfile" "$subnets_tmpfile" + return 1 + fi + + extract_ip_cidr_from_json_ruleset_to_file "$json_tmpfile" "$subnets_tmpfile" + populate_netshift_subnets_from_file "$subnets_tmpfile" + rm -f "$json_tmpfile" "$subnets_tmpfile" +} + +# task-034: download a remote binary (.srs) ruleset, decompile to JSON, extract +# its ip_cidr entries and feed the nft union set. Fail-open on download/decompile. +import_subnets_from_remote_srs_file() { + local url="$1" + local binary_tmpfile json_tmpfile subnets_tmpfile http_proxy_address + binary_tmpfile="$(mktemp)" + json_tmpfile="$(mktemp)" + subnets_tmpfile="$(mktemp)" + http_proxy_address="$(get_service_proxy_address)" + + download_to_file "$url" "$binary_tmpfile" "$http_proxy_address" + + if [ $? -ne 0 ] || [ ! -s "$binary_tmpfile" ]; then + log "Download $url list failed" "error" + rm -f "$binary_tmpfile" "$json_tmpfile" "$subnets_tmpfile" + return 1 + fi + + if ! decompile_binary_ruleset "$binary_tmpfile" "$json_tmpfile"; then + log "Failed to decompile binary rule set file" "error" + rm -f "$binary_tmpfile" "$json_tmpfile" "$subnets_tmpfile" + return 1 + fi + + extract_ip_cidr_from_json_ruleset_to_file "$json_tmpfile" "$subnets_tmpfile" + populate_netshift_subnets_from_file "$subnets_tmpfile" + rm -f "$binary_tmpfile" "$json_tmpfile" "$subnets_tmpfile" +} + import_subnets_from_remote_plain_file() { local url="$1" local section="$2" @@ -3079,6 +3329,8 @@ import_subnets_from_remote_plain_file() { ruleset_tag=$(get_ruleset_tag "$section" "remote" "subnets") ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json" import_plain_subnet_list_to_local_source_ruleset_chunked "$tmpfile" "$ruleset_filepath" + # task-034: also feed the nft union set (centralized with the rule_set). + populate_netshift_subnets_from_file "$tmpfile" rm -f "$tmpfile" } diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 41cbacce..173af4cb 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -43,6 +43,15 @@ RT_TABLE_NAME="netshift" NFT_TABLE_NAME="NetShiftTable" NFT_LOCALV4_SET_NAME="localv4" NFT_LOCALV6_SET_NAME="localv6" +# Destination set holding the UNION of every proxy section's proxied IPv4 +# subnets (user/local/remote/community subnet lists). Used by the prerouting +# `mangle` chain to mark ONLY proxied destinations into the tproxy path, so +# non-proxied traffic (e.g. a torrent to a random direct IP) never enters +# sing-box (task-034). The per-section outbound is still selected by sing-box +# route rules — nft only decides enter-or-not, so a single union set is enough. +NFT_COMMON_SET_NAME="netshift_subnets" +# IPv6 mirror of NFT_COMMON_SET_NAME (only created/used when IPv6 is enabled). +NFT_COMMON_SET_NAME_V6="netshift_subnets_v6" NFT_DISCORD_SET_NAME="netshift_discord_subnets" NFT_INTERFACE_SET_NAME="interfaces" NFT_FAKEIP_MARK="0x00100000" diff --git a/netshift/files/usr/lib/nft.sh b/netshift/files/usr/lib/nft.sh index 06a6d4e7..65c1f612 100644 --- a/netshift/files/usr/lib/nft.sh +++ b/netshift/files/usr/lib/nft.sh @@ -6,6 +6,24 @@ nft_create_table() { nft add table inet "$name" } +# Delete an nftables inet table if it exists (idempotent, fail-open). +# create_nft_rules rebuilds the whole table from scratch, so it MUST start from +# a clean slate: `nft add table` is idempotent but `nft add rule`/`nft add +# chain` only ever APPEND. Without this flush, a table left behind by a previous +# start that was not cleanly stopped (a procd respawn, an in-place package +# upgrade, or a crash) keeps its stale rules and the freshly-added rules pile on +# top of them. In particular a stale mark-EVERYTHING rule (from global_proxy or +# an older mark-all build) would sit at the top of the prerouting chain and mark +# all traffic before the new destination-selective rules are ever evaluated, +# silently re-introducing the "everything proxied / 100% CPU" regression. +nft_delete_table() { + local name="$1" + + if nft list table inet "$name" > /dev/null 2>&1; then + nft delete table inet "$name" 2>/dev/null + fi +} + # Create a set within a table for storing IPv4 addresses nft_create_ipv4_set() { local table="$1" @@ -76,3 +94,51 @@ nft_add_set_elements_from_file_chunked() { nft_add_set_elements "$nft_table_name" "$nft_set_name" "$array" fi } + +# IPv6 counterpart of nft_add_set_elements_from_file_chunked. Adds only the +# IPv6 / IPv6-CIDR lines from the file into an ipv6_addr set. A line is treated +# as IPv6 when it contains a ':' (after trimming); anything else (IPv4, blank, +# comment) is skipped. Fail-open: a malformed line is simply not added, so the +# corresponding traffic goes direct rather than blackholing. +nft_add_set_elements_from_file_chunked_v6() { + local filepath="$1" + local nft_table_name="$2" + local nft_set_name="$3" + local chunk_size="${4:-5000}" + + local array count + count=0 + while IFS= read -r line; do + line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + [ -z "$line" ] && continue + + case "$line" in + *:*) ;; + *) + log "'$line' is not IPv6 or IPv6 CIDR" "debug" + continue + ;; + esac + + if [ -z "$array" ]; then + array="$line" + else + array="$array,$line" + fi + + count=$((count + 1)) + + if [ "$count" = "$chunk_size" ]; then + log "Adding $count elements to nft set $nft_set_name" "debug" + nft_add_set_elements "$nft_table_name" "$nft_set_name" "$array" + array="" + count=0 + fi + done < "$filepath" + + if [ -n "$array" ]; then + log "Adding $count elements to nft set $nft_set_name" "debug" + nft_add_set_elements "$nft_table_name" "$nft_set_name" "$array" + fi +} diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index cb391f10..f723a4f0 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -9,7 +9,7 @@ # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, -# nftv6, isolation, diagnostics, subscription, insecure, rejected, +# nftv6, selmark, isolation, diagnostics, subscription, insecure, rejected, # jobstate, selfheal, dnsdetour, globalproxy, stablecheck, # extcheck, netshiftcheck, selfupdate, backupguard # ────────────────────────────────────────────────────────────────── diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 6cc9d41a..23694446 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -426,6 +426,404 @@ test_nft_ipv6() { nft delete table inet "$test_table" 2>/dev/null } +# ───────────────────────────────────────────────────────────────── +# Test: Destination-selective nft marking (task-034) +# +# Regression: 0.8.6 marked ALL LAN tcp/udp into tproxy (mangle prerouting), +# so EVERY forwarded flow (e.g. a torrent to a random direct IP) entered +# sing-box -> sniff + full route-rule walk per connection -> 100% CPU on a +# weak router, even when only selected lists were configured for proxying. +# 0.8.5 marked SELECTIVELY: only proxied destination subnets +# (@netshift_subnets) + the FakeIP range (proxied domains). task-034 restores +# that selective model, keeping mark-EVERYTHING only when a global_proxy +# section is active. +# +# This test awk-extracts the SHIPPED create_nft_rules (+ its task-034 helpers) +# verbatim from the live bin, stubs the few UCI/predicate functions, and runs +# the real ruleset against a real nft table, then inspects the mangle chain. +# Five cases (per the spec): +# 1. selective marking present + NO unconditional mark-all (default) +# 2. a direct (non-listed) destination is NOT marked -> bypasses sing-box +# (rule-structure check; + live counter when runnable) +# 3. global_proxy override = mark-all IS present +# 4. IPv6 mirror selective when enable_ipv6=1 (+ no v6 mark-all) +# 5. domain routing intact: FakeIP range still marked; sing-box check passes +# ───────────────────────────────────────────────────────────────── +test_selective_marking() { + header "Destination-selective nft marking (task-034)" + + if ! command -v nft > /dev/null 2>&1; then + skip "nft not available" + return + fi + + local bin="${NETSHIFT_SRC}/usr/bin/netshift" + local lib="${NETSHIFT_LIB_DIR}" + if [ ! -r "$bin" ] || [ ! -r "$lib/constants.sh" ] || [ ! -r "$lib/nft.sh" ]; then + skip "selective-marking (bin / constants.sh / nft.sh not found)" + return + fi + + # Source the runtime contract values + nft helpers for the constants the + # assertions reference (NFT_COMMON_SET_NAME, FakeIP ranges, marks). + # shellcheck disable=SC1090 + . "$lib/constants.sh" + + # Confirm the new constants were actually re-added (DoD item). + if [ -n "$NFT_COMMON_SET_NAME" ] && [ -n "$NFT_COMMON_SET_NAME_V6" ]; then + pass "selective:constants — NFT_COMMON_SET_NAME(+v6) defined" + else + fail "selective:constants — NFT_COMMON_SET_NAME(+v6) missing" + return + fi + + # Common driver preamble shared by every scenario. Args via env: + # SCN_TABLE unique nft table name for this run + # SCN_IPV6 1 to enable the IPv6 mirror, else 0 + # SCN_GLOBALPROXY non-empty -> global_proxy section name, else "" + # SCN_BLOCKDOH 1 to enable DoH-block CIDR marking, else 0 + # SCN_FULLROUTED space-separated fully_routed_ips (proxy section), else "" + # The driver writes the real shipped create_nft_rules + helpers, runs it, + # then dumps `nft list chain inet <table> mangle` to stdout for the parent + # to parse. Each emitted token is a name:OK / name:FAIL line. + local drv="/tmp/netshift-selmark-$$.sh" + cat > "$drv" << 'SELEOF' +set -e +LIB="LIB_DIR_PLACEHOLDER" +BIN="BIN_PATH_PLACEHOLDER" + +# shellcheck disable=SC1090 +. "$LIB/constants.sh" +# shellcheck disable=SC1090 +. "$LIB/nft.sh" + +# Override the table name so we never touch the real NetShiftTable. +NFT_TABLE_NAME="$SCN_TABLE" + +# Quiet logger. +log() { :; } +nolog() { :; } +echolog() { :; } + +# ── UCI / predicate stubs driven by env ────────────────────────── +netshift_ipv6_enabled() { [ "${SCN_IPV6:-0}" = "1" ]; } +get_global_proxy_section() { printf '%s' "${SCN_GLOBALPROXY:-}"; } + +config_get() { + # $1=var $2=section $3=option [$4=default] + eval "$1=\"\${4:-}\"" + case "$3" in + source_network_interfaces) eval "$1=\"selmark0\"" ;; + esac +} +config_get_bool() { + # $1=var $2=section $3=option [$4=default] + eval "$1=\"\${4:-0}\"" + case "$3" in + block_doh) eval "$1=\"${SCN_BLOCKDOH:-0}\"" ;; + exclude_ntp) eval "$1=\"0\"" ;; + esac +} +# fully_routed_ips iteration: one proxy section "frsec" carrying SCN_FULLROUTED. +config_foreach() { + # $1=callback $2=type + [ -n "${SCN_FULLROUTED:-}" ] || return 0 + "$1" "frsec" +} +config_list_foreach() { + # $1=section $2=option $3=callback + [ "$2" = "fully_routed_ips" ] || return 0 + for _ip in ${SCN_FULLROUTED:-}; do + "$3" "$_ip" + done +} +# The fully_routed section is always a proxy section in this harness. +# (config_get above returns connection_type default "", so force it here.) +_orig_cg=config_get + +# Extract the shipped functions verbatim (column-0 opener to column-0 '}'). +for fn in nft_init_interfaces_set populate_netshift_subnets_from_file \ + populate_netshift_subnets_from_string nft_mark_fully_routed_source_ips \ + _nft_mark_fully_routed_ips_for_section _nft_mark_fully_routed_ip_handler \ + create_nft_rules; do + eval "$(awk -v f="$fn" '$0 ~ "^"f"\\(\\) \\{"{p=1} p{print} p&&/^\}/{exit}' "$BIN")" +done + +# The fully_routed handler reads connection_type via config_get; make that +# section a proxy section so its IPs get a source mark rule. +config_get() { + eval "$1=\"\${4:-}\"" + case "$3" in + source_network_interfaces) eval "$1=\"selmark0\"" ;; + connection_type) eval "$1=\"proxy\"" ;; + fully_routed_ips) eval "$1=\"${SCN_FULLROUTED:-}\"" ;; + esac +} + +# SCN_PRESEED: when set, do NOT start from a clean slate. Instead leave behind a +# STALE mark-EVERYTHING table (as a previous global_proxy/0.8.6 run would) and +# then run create_nft_rules on top of it WITHOUT a stop — faithfully reproducing +# the procd-respawn / in-place-upgrade service path that the original test +# missed. The fix (create_nft_rules flushing the table first) must make the +# FINAL live chain purely selective regardless of this leftover. +if [ "${SCN_PRESEED:-0}" = "1" ]; then + nft delete table inet "$NFT_TABLE_NAME" 2>/dev/null || true + nft add table inet "$NFT_TABLE_NAME" + nft add set inet "$NFT_TABLE_NAME" "$NFT_LOCALV4_SET_NAME" '{ type ipv4_addr; flags interval; auto-merge; }' + nft add set inet "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME" '{ type ifname; flags interval; }' + nft add element inet "$NFT_TABLE_NAME" "$NFT_INTERFACE_SET_NAME" '{ "selmark0" }' + nft add chain inet "$NFT_TABLE_NAME" mangle '{ type filter hook prerouting priority -150; policy accept; }' + nft add rule inet "$NFT_TABLE_NAME" mangle ct status dnat return + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" ip daddr "@$NFT_LOCALV4_SET_NAME" return + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" meta l4proto tcp meta mark set "$NFT_FAKEIP_MARK" counter + nft add rule inet "$NFT_TABLE_NAME" mangle iifname "@$NFT_INTERFACE_SET_NAME" meta l4proto udp meta mark set "$NFT_FAKEIP_MARK" counter + # Build on TOP of the stale table (no nft delete here on purpose). + create_nft_rules >/dev/null 2>&1 +else + # Clean slate, then build the real ruleset. + nft delete table inet "$NFT_TABLE_NAME" 2>/dev/null || true + create_nft_rules >/dev/null 2>&1 +fi + +nft list chain inet "$NFT_TABLE_NAME" mangle 2>/dev/null +echo "---SETS---" +nft list set inet "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" 2>/dev/null || true +SELEOF + sed -i "s|LIB_DIR_PLACEHOLDER|$lib|g; s|BIN_PATH_PLACEHOLDER|$bin|g" "$drv" + + # ── Scenario 1+2+5: default selective (no global_proxy) ────────── + local out1 + out1="$(SCN_TABLE="selmark_def_$$" SCN_IPV6=0 SCN_GLOBALPROXY="" SCN_BLOCKDOH=0 \ + SCN_FULLROUTED="" sh "$drv" 2>/dev/null)" + nft delete table inet "selmark_def_$$" 2>/dev/null + + # The selective marks must be present. + if echo "$out1" | grep -q "@$NFT_COMMON_SET_NAME"; then + pass "selective:default — proxied-subnets set rule present (@$NFT_COMMON_SET_NAME)" + else + fail "selective:default — @$NFT_COMMON_SET_NAME mark rule missing" "$(echo "$out1" | grep -i 'mark set' || echo "$out1")" + fi + if echo "$out1" | grep -Fq "$SB_FAKEIP_INET4_RANGE"; then + pass "selective:default — FakeIP range marked ($SB_FAKEIP_INET4_RANGE) [domain routing intact]" + else + fail "selective:default — FakeIP range mark rule missing" + fi + # The proxied-subnets union set must exist (DoD: created). + if echo "$out1" | grep -q -- "---SETS---" && \ + echo "$out1" | sed -n '/---SETS---/,$p' | grep -q "set $NFT_COMMON_SET_NAME"; then + pass "selective:default — union set $NFT_COMMON_SET_NAME created" + else + fail "selective:default — union set $NFT_COMMON_SET_NAME not created" + fi + + # Regression bypass: there must be NO unconditional mark-all tcp/udp rule + # (a mark-set rule that has NO daddr / saddr / set qualifier). We detect it + # structurally: a `meta l4proto (tcp|udp) meta mark set` line that does NOT + # also contain `daddr` or `saddr`. + local markall_lines + markall_lines="$(echo "$out1" | grep 'meta mark set' | grep 'l4proto' | grep -v 'daddr' | grep -v 'saddr' || true)" + if [ -z "$markall_lines" ]; then + pass "selective:bypass — NO unconditional mark-all rule (direct IP NOT marked)" + else + fail "selective:bypass — unconditional mark-all rule still present" "$markall_lines" + fi + + # ── Scenario 3: global_proxy override -> mark-all present ──────── + local out3 + out3="$(SCN_TABLE="selmark_gp_$$" SCN_IPV6=0 SCN_GLOBALPROXY="gpsec" SCN_BLOCKDOH=0 \ + SCN_FULLROUTED="" sh "$drv" 2>/dev/null)" + nft delete table inet "selmark_gp_$$" 2>/dev/null + + local gp_markall + gp_markall="$(echo "$out3" | grep 'meta mark set' | grep 'l4proto' | grep -v 'daddr' | grep -v 'saddr' || true)" + if [ -n "$gp_markall" ]; then + pass "selective:globalproxy — mark-EVERYTHING rules present under global_proxy" + else + fail "selective:globalproxy — mark-all rules missing under global_proxy" "$out3" + fi + # And under global_proxy the selective @set rule should NOT be added. + if echo "$out3" | grep -q "@$NFT_COMMON_SET_NAME"; then + fail "selective:globalproxy — selective @set rule unexpectedly present under global_proxy" + else + pass "selective:globalproxy — selective @set rule correctly bypassed" + fi + + # ── Scenario 4: IPv6 mirror selective (enable_ipv6=1) ──────────── + # Only meaningful if the kernel supports the v6 set + ip6 rules. + local out4 + out4="$(SCN_TABLE="selmark_v6_$$" SCN_IPV6=1 SCN_GLOBALPROXY="" SCN_BLOCKDOH=0 \ + SCN_FULLROUTED="" sh "$drv" 2>/dev/null)" + nft delete table inet "selmark_v6_$$" 2>/dev/null + + if echo "$out4" | grep -q "ip6 daddr @$NFT_COMMON_SET_NAME_V6"; then + pass "selective:ipv6 — v6 union set mark rule present (@$NFT_COMMON_SET_NAME_V6)" + local v6_markall + v6_markall="$(echo "$out4" | grep 'meta mark set' | grep 'l4proto' | grep -v 'daddr' | grep -v 'saddr' || true)" + if [ -z "$v6_markall" ]; then + pass "selective:ipv6 — no mark-all rule with IPv6 enabled" + else + fail "selective:ipv6 — unexpected mark-all rule with IPv6 enabled" "$v6_markall" + fi + if echo "$out4" | grep -Fq "$SB_FAKEIP_INET6_RANGE"; then + pass "selective:ipv6 — FakeIP v6 range marked ($SB_FAKEIP_INET6_RANGE)" + else + fail "selective:ipv6 — FakeIP v6 range mark rule missing" + fi + else + # The driver enables v6 only if netshift_ipv6_enabled() returns true, + # which it forced; absence here means the kernel rejected the v6 set/rule. + skip "selective:ipv6 — v6 set/rule not applied (kernel ip6 support?)" + fi + + # ── Live counter proof (best-effort, needs NET_ADMIN + a usable table) ── + # Apply the default-selective table once more and probe with `nft` matching: + # add a known proxied subnet to the union set, then verify a packet-shaped + # match logic — we cannot synthesize forwarded packets here, so we assert + # the deterministic rule ORDERING instead: the @localv4 return precedes the + # @set mark, and there is no catch-all mark after it. + local out_order + out_order="$(SCN_TABLE="selmark_ord_$$" SCN_IPV6=0 SCN_GLOBALPROXY="" SCN_BLOCKDOH=0 \ + SCN_FULLROUTED="192.168.50.7" sh "$drv" 2>/dev/null)" + nft delete table inet "selmark_ord_$$" 2>/dev/null + if echo "$out_order" | grep -q "ip saddr 192.168.50.7 meta mark set"; then + pass "selective:fullrouted — fully_routed_ips source-mark rule present" + else + fail "selective:fullrouted — fully_routed_ips source mark missing" "$(echo "$out_order" | grep -i saddr || echo "$out_order")" + fi + + # ── Scenario 6 (THE REGRESSION REPRO): stale mark-all table + respawn ── + # Reproduces the real on-hardware service path the original test missed: a + # NetShiftTable left behind by a previous global_proxy / 0.8.6 mark-all run, + # then create_nft_rules run again WITHOUT a clean stop (procd respawn / + # in-place package upgrade). Before the fix, the stale mark-EVERYTHING rules + # survived at the TOP of the prerouting chain and marked all traffic, making + # the new destination-selective rules dead -> "everything proxied / 100% + # CPU" even though the selective code was present. The fix flushes the table + # first, so the FINAL live chain must be purely selective with NO mark-all. + local out6 + out6="$(SCN_TABLE="selmark_respawn_$$" SCN_IPV6=0 SCN_GLOBALPROXY="" SCN_BLOCKDOH=0 \ + SCN_FULLROUTED="" SCN_PRESEED=1 sh "$drv" 2>/dev/null)" + nft delete table inet "selmark_respawn_$$" 2>/dev/null + + local respawn_markall + respawn_markall="$(echo "$out6" | grep 'meta mark set' | grep 'l4proto' | grep -v 'daddr' | grep -v 'saddr' || true)" + if [ -z "$respawn_markall" ]; then + pass "selective:respawn — NO stale mark-all rule survives a respawn (table flushed)" + else + fail "selective:respawn — stale mark-all rule SURVIVED the rebuild (regression)" "$respawn_markall" + fi + if echo "$out6" | grep -q "@$NFT_COMMON_SET_NAME"; then + pass "selective:respawn — selective @set rule present after respawn" + else + fail "selective:respawn — selective @set rule missing after respawn" "$out6" + fi + if echo "$out6" | sed -n '/---SETS---/,$p' | grep -q "set $NFT_COMMON_SET_NAME"; then + pass "selective:respawn — union set $NFT_COMMON_SET_NAME present after respawn" + else + fail "selective:respawn — union set $NFT_COMMON_SET_NAME missing after respawn" + fi + # The selective rules must not be DUPLICATED (proof the chain was rebuilt, + # not appended): exactly one @set mark rule. + local setrule_count + setrule_count="$(echo "$out6" | sed -n '/chain mangle/,/^\t}/p' | grep -c "daddr @$NFT_COMMON_SET_NAME" || true)" + if [ "$setrule_count" = "1" ]; then + pass "selective:respawn — exactly one @set mark rule (chain rebuilt, not appended)" + else + fail "selective:respawn — expected 1 @set rule, found $setrule_count (append, not rebuild)" "$out6" + fi + + rm -f "$drv" + + # ── Scenario 7: REAL get_global_proxy_section via a real config_load ───── + # The original test STUBBED get_global_proxy_section, so it never exercised + # the actual UCI-reading helper that decides mark-all vs selective. Here we + # use the SHIPPED get_global_proxy_section / _determine_global_proxy_section / + # section_has_configured_outbound / get_subscription_urls_for_section against + # a REAL config_load of a hardware-shaped config (one subscription proxy + # section, global_proxy=0). It MUST return empty -> selective branch. + if [ -r /lib/functions.sh ] && [ -r /lib/config/uci.sh ] && command -v uci > /dev/null 2>&1; then + local rgp_drv rgp_out + rgp_drv="/tmp/netshift-selmark-rgp-$$.sh" + cat > "$rgp_drv" << 'RGPEOF' +BIN="BIN_PATH_PLACEHOLDER" +LIB="LIB_DIR_PLACEHOLDER" +. /lib/functions.sh +. /lib/config/uci.sh 2>/dev/null || true +# shellcheck disable=SC1090 +. "$LIB/constants.sh" +# shellcheck disable=SC1090 +. "$LIB/helpers.sh" +log() { :; } +echolog() { :; } +nolog() { :; } +for fn in get_global_proxy_section _determine_global_proxy_section \ + section_has_configured_outbound get_subscription_urls_for_section \ + _collect_subscription_url_handler; do + eval "$(awk -v f="$fn" '$0 ~ "^"f"\\(\\) \\{"{p=1} p{print} p&&/^\}/{exit}' "$BIN")" +done +mkdir -p /etc/config +cat > /etc/config/netshift_selmarktest <<'CFGEOF' +config settings 'settings' + option block_doh '0' + +config section 'main' + option connection_type 'proxy' + option proxy_config_type 'subscription' + option global_proxy '0' + list subscription_url 'https://example.com/sub' +CFGEOF +# Mirror exactly what bin/netshift does: config_load with the config name. +config_load netshift_selmarktest +printf 'GP=[%s]\n' "$(get_global_proxy_section)" +rm -f /etc/config/netshift_selmarktest +RGPEOF + sed -i "s|LIB_DIR_PLACEHOLDER|$lib|g; s|BIN_PATH_PLACEHOLDER|$bin|g" "$rgp_drv" + rgp_out="$(sh "$rgp_drv" 2>/dev/null)" + rm -f "$rgp_drv" + if echo "$rgp_out" | grep -q '^GP=\[\]$'; then + pass "selective:realgp — real get_global_proxy_section returns empty for global_proxy=0 (selective branch)" + else + fail "selective:realgp — real get_global_proxy_section wrongly non-empty (would force mark-all)" "$rgp_out" + fi + else + skip "selective:realgp — LuCI config_load / uci not available" + fi + + # ── Case 5b: sing-box validates a 2-section selective config ───── + # The generated sing-box config is independent of the nft marking, but the + # spec requires confirming sing-box still accepts a domain+subnet config. + if command -v sing-box > /dev/null 2>&1 && command -v jq > /dev/null 2>&1; then + local sbtmp sbcfg sbres + sbtmp="/tmp/netshift-selmark-sb-$$.json" + sbcfg=$(jq -n \ + --arg direct "$SB_DIRECT_OUTBOUND_TAG" \ + --arg tproxy "$SB_TPROXY_INBOUND_TAG" \ + --arg listen "$SB_TPROXY_INBOUND_ADDRESS" \ + --argjson port "$SB_TPROXY_INBOUND_PORT" \ + '{ + log:{disabled:false,level:"warn",timestamp:true}, + dns:{servers:[],rules:[],final:$direct,strategy:"prefer_ipv4",independent_cache:true}, + ntp:{}, + inbounds:[{type:"tproxy",tag:$tproxy,listen:$listen,listen_port:$port}], + outbounds:[{type:"direct",tag:$direct},{type:"direct",tag:"sec1-out"},{type:"direct",tag:"sec2-out"}], + route:{rules:[ + {ip_cidr:["1.2.3.0/24"],outbound:"sec1-out"}, + {ip_cidr:["198.18.0.0/15"],outbound:"sec2-out"} + ],rule_set:[],final:$direct,auto_detect_interface:true} + }') + printf '%s' "$sbcfg" > "$sbtmp" + sbres="$(sing-box -c "$sbtmp" check 2>&1)" + if [ -z "$sbres" ]; then + pass "selective:singboxcheck — 2-section selective config validates" + else + fail "selective:singboxcheck — sing-box rejected config" "$sbres" + fi + rm -f "$sbtmp" + else + skip "selective:singboxcheck — sing-box / jq not installed" + fi +} + # ───────────────────────────────────────────────────────────────── # Test: Section-isolation invariant (task-033) # @@ -4117,6 +4515,7 @@ main() { test_sing_box_config test_nft test_nft_ipv6 + test_selective_marking test_section_isolation test_diagnostics test_subscription @@ -4138,6 +4537,7 @@ main() { helpers) test_helpers ;; nft) test_nft ;; nftv6) test_nft_ipv6 ;; + selmark) test_selective_marking ;; isolation) test_section_isolation ;; diagnostics) test_diagnostics ;; subscription) test_subscription ;; @@ -4157,7 +4557,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 isolation diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" exit 1 ;; esac From 48fa5d6bed0431c1d05d5df989dae3ce7a763581 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Thu, 11 Jun 2026 18:20:47 +0300 Subject: [PATCH 66/75] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B8=D0=B7=D0=B2=D0=BE=D0=B4=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=B8=20=D1=83=D1=82?= =?UTF-8?q?=D0=B5=D1=87=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 60 ++++ .../memory/shell-backend-developer.md | 133 ++++++++ netshift/files/usr/bin/netshift | 148 +++++++-- netshift/files/usr/lib/constants.sh | 5 + tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 303 +++++++++++++++++- 6 files changed, 629 insertions(+), 22 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index db847f8d..593891c8 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -890,3 +890,63 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> respawn / the monitor re-applying the service ruleset; to test create_nft_rules in isolation you must disable the service (shutdown_correctly=1 + kill monitor) or do it in the smoke container, not on a live procd-managed router. + +## task-035 + task-036: monitor procd-lock hang + monitor leak (2026-06-11) + +- USER (Oleg): changing subscription settings does nothing — dashboard frozen, + no new logs. CONFIRMED ON HARDWARE (clean reboot, not a session artifact): 1st + reload after boot completes, 2nd+ reload HANG forever → settings never applied. +- ROOT (task-035): start_main launches the health monitor as a bare + `monitor_sing_box &`. procd runs init actions holding the service lock on + fd 1000 (/tmp/lock/procd_<name>.lock — confirmed canonical via + openwrt/packages#12807). The bare `&` inherits fd 1000; the monitor is an + infinite loop so it holds the procd lock FOREVER → next reload/restart blocks + on `flock 1000` indefinitely. Hardware proof: the process holding + procd_netshift.lock == netshift_monitor.pid, child `sleep 10`; subsequent + reload wrappers stacked on `flock 1000`. +- FIX (task-035, APPROVED review-001): launch the monitor detached — + `setsid /bin/sh -c 'exec 1000>&- 2>/dev/null; exec /usr/bin/netshift __monitor' + </dev/null >/dev/null 2>&1 &` + a hidden `__monitor` CLI dispatch case + + monitor self-writes $$ to MONITOR_PIDFILE (new constant). CRITICAL: `setsid` + ALONE does NOT close fd 1000 (busybox sets no CLOEXEC) — the explicit + `exec 1000>&-` is load-bearing (the fd-hygiene test must fail the setsid-only + variant too, and it does). smoke 152/0. +- task-035 INTRODUCED A LEAK (caught on hardware): each detached monitor + self-writes $$ to the pidfile, so the pidfile only remembers the LATEST + monitor; stop()/reload kills only that one; monitors from prior reloads + (orphaned to init by setsid) survive → accumulate. +- FIX (task-036, APPROVED review-002): `_kill_stale_sing_box_monitors()` reaps + ALL detached monitors by the unique `__monitor` argv marker + (`pgrep -f "/usr/bin/netshift __monitor"`), with numeric `$$`/`${PPID:-0}` + self/parent exclusion, called from BOTH stop() and start_sing_box_monitor + (which now reaps + rm pidfile + ALWAYS spawns one fresh, replacing the old + "return 0 if alive" guard). Reachable ONLY from start()/stop(), NOT from + monitor recovery (which uses stop_main/start_main) → can't self-kill. smoke + 155/0, discriminator real (old guard → 2/7 FAIL). +- HARDWARE-MEASUREMENT LANDMINE (cost me ~10 probes of false "2-3 monitors"): + counting monitors on a slow armv7 router via `pgrep -f netshift __monitor` or + per-pid `ls /proc + cat /proc/$p/cmdline` is UNRELIABLE — (a) `pgrep -f` + substring-matches YOUR OWN diagnostic `ash -c '...__monitor...'` shell, and + (b) iterating `ls /proc` then reading each `/stat` races transient child + processes of your own command that die mid-read (show as alive in `kill -0` + then "no such file"). USE A SINGLE ATOMIC SNAPSHOT: `ps w > /tmp/s.txt` then + `grep "ash /usr/bin/netshift __monitor" /tmp/s.txt | grep -v grep`. That gave + the truth: exactly ONE real monitor after reboot AND after 3 reloads. Always + verify process counts on-device with an atomic `ps` snapshot, never live + pgrep/proc-walk. +- FINAL HARDWARE STATE (0.8.8, atomic ps): sing-box 1, monitors 1, stuck reloads + 0, procd-lock holders 0, nft mark-all 0 / selective 2 (task-034), + route.default_mark 2097152 (task-033), internet OK, reloads no longer hang. + ALL of task-033/034/035/036 verified working together on OWRT25/apk hardware. +- apk INSTALL on OWRT25 (reconfirmed): to land new files use `apk add + --no-scripts` (skips the post-install/upgrade trigger that hangs on the + network-blocking service start — landmine #B) + bump the version (equal-version + no-overwrite — landmine #A), then start the service manually. `--no-scripts` + is the clean workaround for the hanging trigger. +- OUTSTANDING FOLLOW-UP (review-001 M1 / review-002 M3, NOT fixed): + `start_subscription_startup_retry_worker` (~netshift:770-808) backgrounds an + infinite `( … ) &` from start_main with NO setsid / NO `exec 1000>&-` → SAME + fd-1000 inheritance class; can re-introduce a reload hang when a subscription + is unreachable at reload time. Also its `pid` (~:774) is not local. Worth a + task to apply the same detach (ideally a shared launcher helper to avoid a 3rd + copy of the setsid pattern). diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index f1cfe28e..1bc4598d 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -1061,3 +1061,136 @@ findings; keep under ~200 lines. Pre-existing `rh-case1/2/6:FAIL` red marks persist (documented task-031 quirk; suite still EXIT=0). No registration change needed (selmark already in all)/alias/usage/compose). + +## task-035: health monitor held the procd lock fd (1000) → reload/restart hang + +- ROOT CAUSE (hardware-confirmed to the exact process): `start_sing_box_monitor` + launched the long-lived monitor with a bare `monitor_sing_box &`. procd runs + every init action while holding its exclusive service lock on **fd 1000** + (`/tmp/lock/procd_<name>.lock`) — confirmed canonical (openwrt/packages#12807: + "fd 1000 holds the exclusive concurrency lock for init script execution"). + The bare `&` inherits ALL open fds incl. 1000, so the never-exiting monitor + held the procd lock FOREVER → the NEXT `reload`/`restart` blocked on + `flock 1000` indefinitely → settings never re-applied (dashboard frozen). The + 1st reload "worked" only because it WAS the lock holder. +- FIX (chosen: hidden `__monitor` subcommand + setsid + fd-close, most robust): + `start_sing_box_monitor` now launches + `setsid /bin/sh -c 'exec 1000>&- 2>/dev/null; exec /usr/bin/netshift __monitor' + </dev/null >/dev/null 2>&1 &`. Three independent detachments: (1) `setsid` → + own session, out of procd's process group; (2) `exec 1000>&-` closes the + inherited lock fd in the child BEFORE the re-exec (exec does NOT close + non-CLOEXEC fds — that IS the bug — so the close is mandatory; harmless no-op + on the plain `start`/boot path where fd 1000 isn't open); (3) + `</dev/null >/dev/null 2>&1` drops procd's inherited stdout/stderr pipes. The + hidden `__monitor) monitor_sing_box ;;` dispatch case (bottom of bin/netshift, + NOT in help) runs the UNCHANGED detection/recovery loop. +- WHY re-exec instead of just `{ exec 1000>&-; monitor_sing_box; } &`: re-exec + via setsid is more thorough (new session + fresh process, drops EVERYTHING + inherited), and is robust even if procd ever changes the lock fd number — but + I still close 1000 explicitly as the belt. The detached monitor's recovery + path (`stop_main`/`start_main`) is safe: `start_main` does NOT spawn a monitor + (only `start()` does, via `start_sing_box_monitor`), so a recovery restart + can't re-introduce a lock-held child; and the monitor itself has no fd 1000. +- PID TRACKING through setsid/exec: the launcher can't reliably capture the + final pid of the setsid→sh→exec chain, so the monitor writes its OWN `$$` to + the pidfile at the TOP of `monitor_sing_box` (`echo $$ > "$MONITOR_PIDFILE"`). + `start_sing_box_monitor` then waits briefly (`while [ ! -s pidfile ]`, ~2s cap, + `sleep 0.1 2>/dev/null || sleep 1` busybox-safe) for it to appear, for + diagnostics. `stop()` reads the pidfile + `kill` — unchanged behaviour, still + kills exactly the detached monitor. Single-instance pidfile guard at the top + of `start_sing_box_monitor` (kill-prior / skip-if-alive) is UNCHANGED → still + exactly one monitor after N reloads. `shutdown_correctly` honoring + pidfile + cleanup on monitor exit (`rm -f "$MONITOR_PIDFILE"`) unchanged. +- New constant `MONITOR_PIDFILE="/var/run/netshift_monitor.pid"` (constants.sh, + Common) replaced the 4 inlined literals (start/stop/monitor + launcher) — was + the one repeated magic path. NO sacred value / nft / default_mark touched. +- KNOWN RELATED RISK (out of task-035 scope, flagged): the deferred subscription + retry worker (`start_subscription_startup_retry_worker`) ALSO backgrounds a + `while true` loop with a bare `( ... ) &` and so inherits fd 1000 the same + way. It usually exits after the first successful `subscription_update` + (self-restarts), so it's not the confirmed culprit, but on a box where the sub + stays unreachable it could hold the lock too. Candidate for the same setsid + detach in a follow-up. +- TEST: new top-level `test_monitor_fd_hygiene` (alias `monfd`, 4 asserts), + placed after `test_section_isolation`. awk-extracts the SHIPPED + `start_sing_box_monitor` verbatim, re-pins `MONITOR_PIDFILE`, installs a stub + `/usr/bin/netshift` (back-up/restore the real one) whose `__monitor` writes + `$$` + sleep-loops. Driver opens fd 1000 on a sentinel lock file + `flock -x` + it (exactly like procd), runs the launcher, then asserts: (1) monitor alive + + pidfile correct; (2) NO `/proc/<pid>/fd/*` symlink points at the sentinel lock + (THE direct fd-hygiene proof — spec test #1); (3) fd 1000 specifically isn't + the lock; (4) repeated-reload-no-hang proxy — CLOSE the parent's fd 1000 + (modeling procd ENDING the action; must NOT `flock -u`, which releases the + per-OFD lock for all and masks the bug) then a separate subshell's + `flock -n -x` on the same file must acquire immediately. PROVEN discriminator: + reverting to `/usr/bin/netshift __monitor &` makes asserts 2/3/4 FAIL (1/3), + fix → 4/0. Parsed in the CURRENT shell (`while read < "$out"`, no pipe) so + counts are EXACT. Registered all 5 points (all)/case alias/usage/compose). +- FLOCK SEMANTICS LANDMINE: an advisory flock lives on the open-file-description + (OFD), not the fd. A child that inherits the fd shares the SAME OFD, so the + lock persists until ALL fds to that OFD close. `flock -u` on ANY of them + releases it for everyone — so a test must model procd by CLOSING the fd, never + by `flock -u`, or it can't tell the bug from the fix. busybox `flock` supports + both `flock -sxun FD` and `flock FILE -c CMD`; I used the FD form in a subshell + (`( exec 9>file; flock -n -x 9 )`) for portability. +- GATES: shellcheck -S error clean (bin/netshift + lib/*.sh + install.sh + + tests/entrypoint.sh). `smoke-tests all` = 152 passed / 0 failed (148 baseline + + 4 new monfd). Pre-existing `rh-case1/2/6:FAIL` red marks persist (task-031 + piped-while quirk; suite still EXIT=0). Verified in the NET_ADMIN smoke + container that the detached monitor child holds NO sentinel/lock fd. + +## task-036: monitor PROCESS LEAK after the task-035 detach (2-3 live monitors) + +- ROOT CAUSE (hardware-confirmed): the task-035 setsid detach is correct, but it + introduced a leak. Each monitor self-writes its OWN `$$` to MONITOR_PIDFILE at + the top of `monitor_sing_box`, so the pidfile only ever remembers the LATEST + monitor. `start_sing_box_monitor`'s old replace-guard only `return 0`'d if the + pidfile pid was alive (else `rm`'d it) — it NEVER killed the old monitor. + reload = `stop(); start()`; `stop()` kills only the pidfile pid. A monitor from + a PRIOR reload whose pid was overwritten in the pidfile is invisible to stop() + → and because it's detached (setsid → own session, reparented to init) it + survives forever → monitors accumulate (pid=X ppid=1 orphan AND newer one both + alive; pidfile names only one). +- FIX (chosen Option 1, robust kill-all by unique marker): new private helper + `_kill_stale_sing_box_monitors` kills ALL detached monitors via + `pgrep -f "/usr/bin/netshift __monitor"` (the hidden `__monitor` subcommand is + the unique marker — re-exec'd argv is `/bin/ash /usr/bin/netshift __monitor`; + matches ONLY the monitor, never the main netshift process). EXCLUDES `$$` and + `${PPID:-0}` (numeric-guarded), so it can never self-kill. Wired into TWO + paths: (1) `start_sing_box_monitor` runs it + `rm -f pidfile` then ALWAYS + spawns one fresh monitor (replaced the old return-0-if-alive guard — a stale + monitor from a prior config is not a valid substitute for one bound to the new + sing-box run); (2) `stop()` runs it after the pidfile-pid kill so a clean stop + leaves ZERO monitors. Result: exactly ONE monitor after N reloads/restarts. +- SELF-KILL SAFETY during recovery: `monitor_sing_box` recovery calls + `stop_main`/`start_main` — NOT `stop()`/`start_sing_box_monitor` — so the + kill-all helper is NEVER reached from inside a running monitor. The `$$`/`$PPID` + exclusion is the belt-and-suspenders guarantee regardless. +- KEEP INTACT (re-verified by smoke): task-035 detach (setsid + `exec 1000>&-` + + hidden `__monitor` + monitor self-writes pid), monitor holds NO lock fd + (re-proven on the RESPAWNED monitor too), reload no-hang, recovery via + stop_main/start_main. No sacred value / nft / default_mark touched. +- `pgrep -f` IS available on busybox target (already used at bin:3974 + `pgrep -f "sing-box"` in get_system_info, and bin:1062 `pgrep "sing-box"`); I + still added a `ps w | grep | grep -v grep | awk` fallback guarded by + `command -v pgrep`. Capture pgrep output into a var, iterate with a `for` + (word-split is fine — pids are numeric), not a pipe, so the `killed` counter + survives. +- TEST: extended `test_monitor_fd_hygiene` (monfd) with 3 new asserts (now 7 + total, no new registration — monfd already in all)). awk-extracts BOTH the + shipped `_kill_stale_sing_box_monitors` AND `start_sing_box_monitor`. Models + the REAL leak precondition before the 2nd launch: monitor A still alive but + pidfile pointed at a DEAD pid (`echo 999999 > pidfile`) — exactly the + overwritten-then-cleared pidfile state. New asserts: `monfd-prior-monitor-killed` + (old pid dead), `monfd-exactly-one-monitor` (pgrep -f count == 1, into file + + counted `while read`, no pipe), `monfd-respawn-no-lock-fd` (the NEW monitor + also holds no sentinel fd). PROVEN DISCRIMINATOR: reverting to the old guard + (return-0-if-alive + neutered selector) makes the buggy code show "found 2 live + monitors" + "old pid still alive" (2/7 FAIL); fix → 7/0. The naive + "launch twice with the SAME live pidfile" does NOT discriminate (old guard + `return 0`s, never respawns, so count stays 1) — you MUST simulate the lost + pidfile pointer to expose the leak. +- GATES: shellcheck -S error clean (bin + lib/*.sh + install.sh + + tests/entrypoint.sh). `smoke-tests all` = 155 passed / 0 failed (152 task-035 + baseline + 3 new monfd asserts). Pre-existing `rh-case1/2/6:FAIL` red marks + persist (task-031 piped-while quirk; suite EXIT=0). diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 41ac8e18..eb70f9c9 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -915,39 +915,135 @@ start() { start_sing_box_monitor } -start_sing_box_monitor() { - local pidfile="/var/run/netshift_monitor.pid" - local pid +_kill_stale_sing_box_monitors() { + # task-036: kill ALL detached health-monitor processes, not just the one + # currently named in MONITOR_PIDFILE. + # + # Why this is needed: each monitor self-writes its OWN $$ to MONITOR_PIDFILE + # at the top of monitor_sing_box (task-035), so the pidfile only ever + # remembers the LATEST monitor. A monitor spawned by a PRIOR reload whose pid + # was overwritten in the pidfile is invisible to stop() (which kills only the + # pidfile pid) → since the monitor is detached via setsid (own session, + # reparented to init) it survives stop()/reload forever → monitors leak + # (2-3 live monitors instead of exactly 1) after repeated reloads. + # + # The hidden `__monitor` subcommand is a unique, stable marker that matches + # ONLY the detached monitor (the re-exec'd argv is + # `/bin/ash /usr/bin/netshift __monitor`) and never the main netshift process + # or any other invocation. We MUST exclude our OWN pid ($$) and our parent + # ($PPID): the recovery path inside monitor_sing_box calls stop_main/start_main + # (NOT stop()/start_sing_box_monitor), so this helper is never reached from + # recovery — but excluding self is the belt that guarantees a monitor can + # never kill ITSELF even if a future caller wires this into a monitor-reachable + # path. + local selector="/usr/bin/netshift __monitor" + local self="$$" + local parent="${PPID:-0}" + local mpids mpid killed=0 + + if command -v pgrep > /dev/null 2>&1; then + # busybox pgrep supports -f (full-command-line match); already used by + # get_system_info (`pgrep -f "sing-box"`). Capture into a var and iterate + # with a counted `for` (no pipe) so the killed counter survives. + mpids="$(pgrep -f "$selector" 2> /dev/null)" + else + # Fallback for the rare box without pgrep: ps | grep, busybox-safe. Match + # the unique `__monitor` marker, drop the grep line itself. + mpids="$(ps w 2> /dev/null | grep "$selector" | grep -v grep | awk '{print $1}')" + fi - # Guard against double-start / procd re-trigger: if a monitor is already - # alive, do not spawn a second one (the old pid would be lost, orphaning a - # monitor that stop() can no longer kill). Mirror the pidfile-guard idiom - # used by start_subscription_startup_retry_worker. - if [ -f "$pidfile" ]; then - pid="$(cat "$pidfile" 2> /dev/null)" - if [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null; then - log "sing-box health monitor is already running with PID $pid" "debug" - return 0 + for mpid in $mpids; do + [ -n "$mpid" ] || continue + # Numeric guard (defensive: ps formats vary across busybox builds). + case "$mpid" in + *[!0-9]*) continue ;; + esac + # Never kill ourselves or our parent (the init/reload action). + [ "$mpid" = "$self" ] && continue + [ "$mpid" = "$parent" ] && continue + if kill -0 "$mpid" 2> /dev/null; then + kill "$mpid" 2> /dev/null + killed=$((killed + 1)) fi - rm -f "$pidfile" + done + + if [ "$killed" -gt 0 ]; then + log "Terminated $killed stale sing-box health monitor(s)" "info" fi +} - monitor_sing_box & - echo $! > "$pidfile" - log "Started sing-box health monitor with PID $!" "info" +start_sing_box_monitor() { + local pidfile="$MONITOR_PIDFILE" + local pid waited + + # task-036: reliably terminate EVERY prior monitor before spawning a new one, + # so exactly ONE monitor exists after any number of reloads/restarts. The old + # "return 0 if the pidfile pid is alive" guard was insufficient: a reload runs + # stop() (kills only the pidfile pid) then start() → here. Because each + # monitor overwrites the pidfile with its own pid, monitors from prior reloads + # were never recorded → never killed → leaked (orphaned, reparented to init by + # setsid). Killing ALL `__monitor` procs (excluding self/parent) closes that + # gap. We then ALWAYS spawn a fresh monitor bound to the just-(re)started + # sing-box, instead of skipping — a stale monitor from a prior config is not a + # valid substitute for one bound to the new run. + _kill_stale_sing_box_monitors + rm -f "$pidfile" + + # CRITICAL (task-035): the health monitor is a long-lived `while true` loop. + # When start_main runs on the reload/restart path, procd holds its init + # service lock on fd 1000 (/tmp/lock/procd_<name>.lock). A bare + # `monitor_sing_box &` inherits ALL open fds, including fd 1000, so the + # monitor would hold the procd lock FOREVER and the NEXT reload/restart + # would block on `flock 1000` indefinitely (settings never re-applied). + # + # Detach the monitor from procd entirely: + # - `setsid` puts it in its own session (no controlling terminal, not in + # procd's process group), and re-execs /usr/bin/netshift fresh. + # - `exec 1000>&-` closes the inherited procd lock fd in the child before + # the re-exec, so even though exec does NOT close non-CLOEXEC fds, fd + # 1000 is already gone (harmless no-op on the plain start/boot path + # where the lock fd is absent). + # - `</dev/null >/dev/null 2>&1` drops the inherited stdio (procd's + # stdout/stderr pipes) so the monitor never keeps those open either. + # The re-exec runs the hidden `__monitor` subcommand, which calls the + # existing monitor_sing_box function (detection/recovery logic unchanged) + # and writes its OWN pid to the pidfile (so stop() can still kill it + # regardless of how setsid forks/execs the chain). + setsid /bin/sh -c 'exec 1000>&- 2>/dev/null; exec /usr/bin/netshift __monitor' < /dev/null > /dev/null 2>&1 & + + # The monitor child writes its real pid to the pidfile itself; wait briefly + # for it to appear so callers/diagnostics see a populated pidfile. + waited=0 + while [ ! -s "$pidfile" ] && [ "$waited" -lt 20 ]; do + sleep 0.1 2> /dev/null || sleep 1 + waited=$((waited + 1)) + done + + if [ -s "$pidfile" ]; then + pid="$(cat "$pidfile" 2> /dev/null)" + log "Started sing-box health monitor with PID $pid" "info" + else + log "Started sing-box health monitor (pid pending)" "info" + fi } stop() { - if [ -f /var/run/netshift_monitor.pid ]; then + if [ -f "$MONITOR_PIDFILE" ]; then local monitor_pid - monitor_pid="$(cat /var/run/netshift_monitor.pid 2>/dev/null)" + monitor_pid="$(cat "$MONITOR_PIDFILE" 2>/dev/null)" if [ -n "$monitor_pid" ] && kill -0 "$monitor_pid" 2>/dev/null; then kill "$monitor_pid" 2>/dev/null log "Stopped sing-box health monitor" "info" fi - rm -f /var/run/netshift_monitor.pid + rm -f "$MONITOR_PIDFILE" fi + # task-036: the pidfile only ever names the LATEST monitor, so a leaked + # monitor from a prior reload (detached via setsid, reparented to init) is + # invisible above. Reap ALL detached `__monitor` processes here so a clean + # stop leaves zero monitors running. Excludes self/parent. + _kill_stale_sing_box_monitors + local dont_touch_dhcp config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0 if [ "$dont_touch_dhcp" -eq 0 ]; then @@ -965,6 +1061,11 @@ monitor_sing_box() { local backoff=0 local dont_touch_dhcp + # Record our OWN pid (task-035): the monitor runs detached via setsid, so + # the launcher cannot reliably capture the final pid of the exec/setsid + # chain. Writing $$ here is authoritative for stop()/diagnostics. + echo $$ > "$MONITOR_PIDFILE" + while true; do sleep "$MONITOR_CHECK_INTERVAL" @@ -1017,7 +1118,7 @@ monitor_sing_box() { fi done - rm -f /var/run/netshift_monitor.pid + rm -f "$MONITOR_PIDFILE" } sing_box_process_exists() { @@ -4758,6 +4859,13 @@ restart) main) main ;; +__monitor) + # Hidden subcommand (task-035): runs the detached sing-box health monitor. + # Launched only by start_sing_box_monitor via setsid with the procd lock fd + # (1000) closed, so the long-lived monitor never holds the procd service + # lock. Not part of the public CLI / help. + monitor_sing_box + ;; list_update) list_update ;; diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 173af4cb..0f22c8b3 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -38,6 +38,11 @@ CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampeteli JQ_REQUIRED_VERSION="1.7.1" COREUTILS_BASE64_REQUIRED_VERSION="9.7" RT_TABLE_NAME="netshift" +# Pidfile of the detached sing-box health monitor (task-035). The monitor is a +# long-lived `while true` loop launched via setsid with the procd lock fd (1000) +# closed, so it does NOT hold the procd service lock and consecutive +# reload/restart never block on flock. The monitor writes its own pid here. +MONITOR_PIDFILE="/var/run/netshift_monitor.pid" ## nft NFT_TABLE_NAME="NetShiftTable" diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index f723a4f0..bd177b0d 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -9,7 +9,7 @@ # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, -# nftv6, selmark, isolation, diagnostics, subscription, insecure, rejected, +# nftv6, selmark, isolation, monfd, diagnostics, subscription, insecure, rejected, # jobstate, selfheal, dnsdetour, globalproxy, stablecheck, # extcheck, netshiftcheck, selfupdate, backupguard # ────────────────────────────────────────────────────────────────── diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 23694446..54f87df9 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1011,6 +1011,305 @@ SIEOF rm -f "$drv" "$route_json" } +# ───────────────────────────────────────────────────────────────── +# Test: Monitor procd-lock fd hygiene (task-035) + monitor-leak (task-036) +# +# ROOT CAUSE under test: the long-lived health monitor used to be launched with +# a bare `monitor_sing_box &`, which inherited ALL open fds — including procd's +# init service lock on fd 1000 (/tmp/lock/procd_<name>.lock). The monitor then +# held that lock forever, so the NEXT reload/restart blocked on `flock 1000` +# indefinitely and settings were never re-applied. +# +# The fix launches the monitor via `setsid /bin/sh -c 'exec 1000>&- ...; exec +# /usr/bin/netshift __monitor' </dev/null >/dev/null 2>&1 &` so the detached +# monitor holds NO procd fds. This test reproduces the fd-inheritance scenario +# deterministically: +# 1. The parent opens fd 1000 onto a sentinel lock file and takes an exclusive +# flock on it (exactly how procd serializes init actions). +# 2. We awk-extract the SHIPPED start_sing_box_monitor verbatim and run it with +# MONITOR_PIDFILE re-pinned to a temp path and /usr/bin/netshift replaced by +# a stub whose `__monitor` runs a tiny pid-writing sleep loop (so the real +# launch mechanism — setsid + fd-close + re-exec — is exercised end to end). +# 3. Assert: the monitor child's /proc/<pid>/fd does NOT reference the sentinel +# lock file (fd 1000 was closed), the monitor is alive, the pidfile is +# correct, and a fresh non-blocking flock on the sentinel acquires +# immediately (it WOULD block before the fix because the monitor inherited +# the held lock). +# +# task-036 (monitor-leak follow-up): the task-035 detach is correct, but because +# each monitor self-writes its OWN $$ to MONITOR_PIDFILE, the pidfile only ever +# remembers the LATEST monitor; stop() kills only that pid, so monitors from +# PRIOR reloads (detached, reparented to init) leaked (2-3 live monitors). Fix: +# start_sing_box_monitor (and stop()) now run _kill_stale_sing_box_monitors, +# which kills ALL `__monitor` procs (excluding self/parent) via `pgrep -f`. +# 4. Launch the monitor a SECOND time (modeling a 2nd reload's start phase) and +# assert the prior monitor is dead, EXACTLY ONE __monitor process survives +# (no accumulation), and the respawned monitor also holds no lock fd. +# ───────────────────────────────────────────────────────────────── +test_monitor_fd_hygiene() { + header "Monitor procd-lock fd Hygiene (task-035) + leak (task-036)" + + local bin="${NETSHIFT_SRC}/usr/bin/netshift" + if [ ! -r "$bin" ]; then + skip "netshift bin not found" + return + fi + if ! command -v setsid > /dev/null 2>&1; then + skip "setsid not available" + return + fi + if ! command -v flock > /dev/null 2>&1; then + skip "flock not available" + return + fi + + local work="/tmp/netshift-monfd-$$" + rm -rf "$work" + mkdir -p "$work/bin" + + local pidfile="$work/monitor.pid" + local lockfile="$work/procd_sentinel.lock" + local fakecli="$work/bin/netshift" + local out="$work/out.txt" + local livefile="$work/live.txt" + : > "$lockfile" + + # Stub /usr/bin/netshift: only its hidden `__monitor` path matters here. It + # mimics the real monitor: write its own pid, then sleep-loop (so it is a + # long-lived child we can inspect). It inherits MONITOR_PIDFILE via env. + cat > "$fakecli" << 'FAKECLI' +#!/bin/sh +case "$1" in +__monitor) + echo $$ > "$MONITOR_PIDFILE" + while true; do sleep 1; done + ;; +*) + exit 0 + ;; +esac +FAKECLI + chmod +x "$fakecli" + + # The shipped start_sing_box_monitor hardcodes `/usr/bin/netshift __monitor`. + # Install the stub at that absolute path; back up any existing real binary + # and restore it afterwards (the smoke container ships none, but be safe). + local real_cli="/usr/bin/netshift" + local real_cli_bak="" + if [ -e "$real_cli" ]; then + real_cli_bak="$work/real_cli.bak" + cp -p "$real_cli" "$real_cli_bak" 2>/dev/null || real_cli_bak="" + fi + mkdir -p /usr/bin + cp "$fakecli" "$real_cli" + chmod +x "$real_cli" + + local drv="$work/driver.sh" + cat > "$drv" << 'MONEOF' +# Quiet logger + the constant the extracted function references. +log() { :; } +MONITOR_PIDFILE="DRV_PIDFILE" + +# Pull the SHIPPED helper + launcher out of the live bin so we test the real +# mechanism (task-036 leak fix: start_sing_box_monitor now calls +# _kill_stale_sing_box_monitors before spawning). +eval "$(awk '/^_kill_stale_sing_box_monitors\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "DRV_BIN")" +eval "$(awk '/^start_sing_box_monitor\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "DRV_BIN")" + +# Simulate procd: open fd 1000 onto the sentinel lock file and hold an +# exclusive flock on it (this is exactly what procd does while running an init +# action). The launcher must NOT let the detached monitor inherit this fd. +exec 1000> "DRV_LOCKFILE" +flock -x 1000 + +# Launch the monitor via the SHIPPED code path (setsid + fd-close + re-exec). +start_sing_box_monitor + +# Give the detached child a moment to write its pid (the launcher already waits, +# but the re-exec/setsid chain may lag slightly under the container). +i=0 +while [ ! -s "$MONITOR_PIDFILE" ] && [ "$i" -lt 50 ]; do + sleep 0.1 2>/dev/null || sleep 1 + i=$((i + 1)) +done + +mpid="$(cat "$MONITOR_PIDFILE" 2>/dev/null)" + +# ── Assert 1: pidfile populated with a live pid. ───────────────────────────── +if [ -n "$mpid" ] && kill -0 "$mpid" 2>/dev/null; then + echo 'monfd-monitor-alive-pidfile:OK' +else + echo "monfd-monitor-alive-pidfile:FAIL (pid='$mpid')" +fi + +# ── Assert 2: the monitor child does NOT have the sentinel lock fd open. We +# resolve every /proc/<pid>/fd symlink and assert none points at the lock +# file (the procd lock fd 1000 was closed before the re-exec). ───────────── +held=0 +if [ -n "$mpid" ] && [ -d "/proc/$mpid/fd" ]; then + for fd in /proc/"$mpid"/fd/*; do + [ -e "$fd" ] || continue + tgt="$(readlink "$fd" 2>/dev/null)" + case "$tgt" in + *procd_sentinel.lock*) held=1 ;; + esac + done +fi +if [ "$held" -eq 0 ]; then + echo 'monfd-no-sentinel-lock-fd:OK' +else + echo 'monfd-no-sentinel-lock-fd:FAIL (monitor still holds the lock fd)' +fi + +# ── Assert 3: fd 1000 specifically is not the sentinel lock in the child. ──── +if [ -n "$mpid" ] && [ -e "/proc/$mpid/fd/1000" ]; then + t1000="$(readlink "/proc/$mpid/fd/1000" 2>/dev/null)" + case "$t1000" in + *procd_sentinel.lock*) echo 'monfd-fd1000-not-lock:FAIL' ;; + *) echo 'monfd-fd1000-not-lock:OK' ;; + esac +else + echo 'monfd-fd1000-not-lock:OK' +fi + +# ── Assert 4 (repeated-reload no-hang proxy): a fresh non-blocking flock on the +# SAME sentinel must acquire immediately. Before the fix the inherited fd +# 1000 would keep the lock held by the live monitor and this would block / +# fail. We drop the parent's own flock first (procd releases the lock when +# the action returns), then a separate process tries flock -n. ───────────── +# Model procd ending the init action: it simply CLOSES its fd 1000 (it does NOT +# explicitly unlock). The advisory flock lives on the open-file-description, so +# if the monitor child inherited fd 1000 (the bug) the SAME OFD stays open in +# the child and the lock persists; with the fix the child never had that OFD, so +# closing the parent's fd here releases the lock. We must NOT `flock -u` (that +# would release the per-OFD lock for everyone and mask the bug). +exec 1000>&- 2>/dev/null +# A separate subshell opens its OWN fd onto the same lock file and tries a +# non-blocking exclusive flock. If the detached monitor still held the lock +# (the bug), this blocks/fails; with the fix it acquires instantly. +if ( exec 9> "DRV_LOCKFILE"; flock -n -x 9 ) 2>/dev/null; then + echo 'monfd-second-flock-immediate:OK' +else + echo 'monfd-second-flock-immediate:FAIL (monitor still holds the lock)' +fi + +# ── Assert 5 (task-036 monitor-leak fix): launching the monitor AGAIN (modeling +# a SECOND reload's start phase) must reliably kill the prior monitor and +# leave EXACTLY ONE __monitor process alive — no accumulation. Before the fix +# (return-0-if-alive + pidfile-only kill) the first monitor leaked because the +# pidfile only ever named the latest one. We count survivors via pgrep -f on +# the unique `__monitor` marker, into a temp file + counted `while read` (no +# pipe) so the count is exact. ───────────────────────────────────────────── +prev_mpid="$mpid" + +# Model the REAL leak precondition: monitor A is still alive (from a prior +# reload cycle) but the pidfile no longer points at it — exactly what happens +# because each monitor overwrites the pidfile with its own $$, so A's pid record +# is lost once a later monitor wrote, and stop() then cleared the pidfile while +# killing only the LATEST pid. We simulate that by pointing the pidfile at a +# dead pid. With the OLD guard, the launcher would see a dead pidfile pid, +# `rm -f` it, and spawn B — leaving A orphaned (2 live monitors). The task-036 +# kill-all must terminate A regardless of the pidfile. +echo 999999 > "$MONITOR_PIDFILE" # a pid that is not alive + +# Re-open + re-hold the sentinel lock to model procd serializing the 2nd action, +# so we also re-prove fd hygiene on the freshly spawned monitor. +exec 1000> "DRV_LOCKFILE" +flock -x 1000 + +start_sing_box_monitor + +i=0 +while [ ! -s "$MONITOR_PIDFILE" ] && [ "$i" -lt 50 ]; do + sleep 0.1 2>/dev/null || sleep 1 + i=$((i + 1)) +done +mpid2="$(cat "$MONITOR_PIDFILE" 2>/dev/null)" + +# The previous monitor must be dead (reliably killed before the new spawn). +if [ -n "$prev_mpid" ] && kill -0 "$prev_mpid" 2>/dev/null; then + echo "monfd-prior-monitor-killed:FAIL (old pid $prev_mpid still alive)" +else + echo 'monfd-prior-monitor-killed:OK' +fi + +# Exactly one live __monitor process must remain. Count with pgrep -f (the same +# selector the fix uses) into a file, then a counted loop (no pipe). +livecount=0 +livefile="DRV_LIVEFILE" +pgrep -f "/usr/bin/netshift __monitor" 2>/dev/null > "$livefile" || true +while IFS= read -r lp; do + [ -n "$lp" ] || continue + case "$lp" in *[!0-9]*) continue ;; esac + if kill -0 "$lp" 2>/dev/null; then + livecount=$((livecount + 1)) + fi +done < "$livefile" +if [ "$livecount" -eq 1 ]; then + echo 'monfd-exactly-one-monitor:OK' +else + echo "monfd-exactly-one-monitor:FAIL (found $livecount live monitors)" +fi + +# The freshly spawned (2nd) monitor must also hold NO sentinel lock fd. +held2=0 +if [ -n "$mpid2" ] && [ -d "/proc/$mpid2/fd" ]; then + for fd in /proc/"$mpid2"/fd/*; do + [ -e "$fd" ] || continue + tgt="$(readlink "$fd" 2>/dev/null)" + case "$tgt" in + *procd_sentinel.lock*) held2=1 ;; + esac + done +fi +if [ "$held2" -eq 0 ]; then + echo 'monfd-respawn-no-lock-fd:OK' +else + echo 'monfd-respawn-no-lock-fd:FAIL (respawned monitor holds the lock fd)' +fi + +exec 1000>&- 2>/dev/null + +# Clean up the monitor child(ren). +[ -n "$mpid2" ] && kill "$mpid2" 2>/dev/null +[ -n "$prev_mpid" ] && kill "$prev_mpid" 2>/dev/null +echo 'DONE' +MONEOF + + sed -i \ + -e "s|DRV_PIDFILE|$pidfile|g" \ + -e "s|DRV_BIN|$bin|g" \ + -e "s|DRV_LOCKFILE|$lockfile|g" \ + -e "s|DRV_LIVEFILE|$livefile|g" \ + "$drv" + + MONITOR_PIDFILE="$pidfile" ash "$drv" > "$out" 2>/dev/null || true + + # Parse in the CURRENT shell (no pipe) so PASS/FAIL counts are exact. + while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL*) fail "$line" ;; + *:SKIP) skip "$line" ;; + *) ;; + esac + done < "$out" + + # Belt-and-suspenders: kill any leftover stub monitor and restore the cli. + if [ -s "$pidfile" ]; then + local leftover + leftover="$(cat "$pidfile" 2>/dev/null)" + [ -n "$leftover" ] && kill "$leftover" 2>/dev/null || true + fi + if [ -n "$real_cli_bak" ]; then + cp -p "$real_cli_bak" "$real_cli" 2>/dev/null || true + else + rm -f "$real_cli" 2>/dev/null || true + fi + + rm -rf "$work" +} + # ───────────────────────────────────────────────────────────────── # Test: sing-box Config Generation # ───────────────────────────────────────────────────────────────── @@ -4517,6 +4816,7 @@ main() { test_nft_ipv6 test_selective_marking test_section_isolation + test_monitor_fd_hygiene test_diagnostics test_subscription test_insecure_fetch @@ -4539,6 +4839,7 @@ main() { nftv6) test_nft_ipv6 ;; selmark) test_selective_marking ;; isolation) test_section_isolation ;; + monfd) test_monitor_fd_hygiene ;; diagnostics) test_diagnostics ;; subscription) test_subscription ;; insecure) test_insecure_fetch ;; @@ -4557,7 +4858,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" exit 1 ;; esac From 41a0bfa59d3d46c946ab7e7459a803959fb84644 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Thu, 11 Jun 2026 19:53:27 +0300 Subject: [PATCH 67/75] =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B0=20=D1=85=D0=B8=D1=81=D1=82=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B2=D0=B5=D0=B7=D0=B4=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 63 +++ .../memory/shell-backend-developer.md | 79 +++ netshift/files/usr/bin/netshift | 114 +++- netshift/files/usr/lib/helpers.sh | 55 +- .../files/usr/lib/sing_box_config_facade.sh | 34 +- tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 500 +++++++++++++++++- 7 files changed, 806 insertions(+), 41 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 593891c8..184ce6f6 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -950,3 +950,66 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> is unreachable at reload time. Also its `pid` (~:774) is not local. Worth a task to apply the same detach (ideally a shared launcher helper to avoid a 3rd copy of the setsid pattern). + +## task-037 + task-038: hype-protocol coverage (hysteria2 from Xray + graceful-skip + splithttp) (2026-06-11) + +- USER asked: are hysteria2 + xhttp supported EVERYWHERE (url / subscription / + urltest / outbound_json)? Audit (explore) produced a full matrix. Key findings: + url == urltest == selector (all route through sing_box_cf_add_proxy_outbound — + identical support); vless/trojan/ss/hysteria2 + ws/grpc/xhttp covered on those + + uri-list subscription + outbound_json; xhttp gated on is_sing_box_extended. +- OPERATOR CORRECTION (important): I initially concluded "hysteria2 can't be in + Xray JSON". WRONG. Real subscriptions (a private.json = 49-element ARRAY of Xray + configs, Hiddify/v2rayN-style) carry Hysteria2 as protocol:"hysteria" + + streamSettings.hysteriaSettings.{version:2,auth} + settings.address/port + + tlsSettings. ALWAYS check a real sample before declaring a protocol absent. +- PRIVACY: private.json is local-only with real keys/servers. I extracted ONLY + structure (field names/types) + aggregate counts via python/jq, redacting all + values. Devs/tests used synthetic placeholders only. NEVER leak its values. +- task-037 (APPROVED, smoke 155/0): added a hysteria branch to + xray_json_to_uri_lines — select protocol=="hysteria", gate + (hysteriaSettings.version // 0)==2 (v1/missing skipped, no fatal), peer from + settings.address/port (not vnext/servers), cred from hysteriaSettings.auth, + scheme hysteria->hysteria2, emit hysteria2://auth@host:port?sni&insecure&alpn&obfs + (NO type=), all via the existing safe()/kv() no-Oniguruma helpers. REUSED the + existing generic $conn dedup (no 2nd dedup/no sort) — collapses the heavy + real-world duplication. NO facade change needed (facade already parses + hysteria2:// and reads sni/insecure/alpn/obfs/obfs-password). +- task-038 (APPROVED, smoke 155/0): (a) the `*)` default arm of + sing_box_cf_add_proxy_outbound was `log fatal; exit 1` — one unsupported link + (tuic/wireguard/etc.) in a url/selector/urltest input ABORTED THE WHOLE CONFIG. + Changed to `log warn; echo "$config"; return 1` (graceful skip). CONFIG-WIPE + SAFETY pattern (reused from task-033 lesson): every caller does `local _new` on + its own line + `if _new=$(...) && [ -n "$_new" ]; then config=$_new`; group + member tag added ONLY on success (no dangling urltest/selector member); + all-unsupported section -> mark_section_outbound_unavailable (append-only to + SUBSCRIPTION_UNAVAILABLE_SECTIONS) -> reject route rule. (b) splithttp (pre-rename + name of xhttp) now an alias in the facade transport builder AND + xray_json_to_uri_lines (network splithttp->xhttp, xhttpSettings // splithttpSettings, + emit type=xhttp; never a literal splithttp downstream). Fixed the false + facade comment claiming tuic/hysteria1/anytls/shadowtls were handled. +- HARDWARE-DATA PROOF on the REAL private.json (in smoke container, aggregates + only): xray_json_to_uri_lines emits 363 URIs after dedup (from 3669 outbounds ~ + 10x collapse), of which hysteria2=19 (was 0 before — proves task-037 works on + real data), vless 339, trojan 5; type= distribution tcp 29 / ws 301 / grpc 10 / + xhttp 4; literal "splithttp" = 0 (normalization works). Facade on clean single + calls: vless+tcp -> NO transport (correct), vless+ws -> transport.type=ws. +- HARNESS LANDMINE (don't repeat): driving sing_box_cf_add_proxy_outbound in a + loop over ALL 363 nodes into ONE reused `config`/section var gave a bogus + "all 363 transport=ws" + a sing-box-check FAIL — an ARTIFACT of reusing one + section/config across hundreds of heterogeneous nodes, NOT a code bug (proven by + the clean per-node check above). The REAL subscription path goes through + normalize_subscription_to_singbox + the batch builder, which the SHIPPED smoke + test (fb-caseO end-to-end + sing-box check, green) exercises correctly. Don't + hand-roll a 363-node-into-one-section e2e; trust the smoke harness for config + integrity, use the real path or per-node checks. +- jq-in-shell-string landmines (recorded for backend): an apostrophe inside a jq + comment within `jq -er '...'` CLOSES the shell string (SC1073/etc + runtime + break) — keep jq comments apostrophe-free; `((` at a jq pipe-element start trips + shellcheck as arithmetic — split into single-paren `as` bindings. +- STILL TODO before/with release: these (task-027..038) are all UNCOMMITTED in the + tree, stacked; operator commits manually. The 0.8.6 regressions (033/034) + + reload-hang (035/036) + protocol coverage (037/038) are all + APPROVED+gated+(033/034/035/036 hardware-verified). task-037/038 verified via + smoke 155/0 + real-private.json extraction; full-config sing-box check on the + real sub is covered by the shipped smoke (fb-caseO), not my hand harness. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 1bc4598d..b7113bc9 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -1194,3 +1194,82 @@ findings; keep under ~200 lines. tests/entrypoint.sh). `smoke-tests all` = 155 passed / 0 failed (152 task-035 baseline + 3 new monfd asserts). Pre-existing `rh-case1/2/6:FAIL` red marks persist (task-031 piped-while quirk; suite EXIT=0). + +## task-038: graceful-skip unsupported scheme (fatal→warn) + splithttp→xhttp alias + +- DEFECT 1: `sing_box_cf_add_proxy_outbound`'s `*)` default arm did `log fatal; + exit 1`. That dispatcher is SHARED by url(single)/selector-loop/urltest-loop + callers (bin/netshift) + the subscription fallback parser + (`normalize_subscription_to_singbox` in helpers.sh). One unsupported link + (tuic/wireguard/anytls/shadowtls/http/typo) in a urltest/selector list aborted + the WHOLE config — and worse, `exit 1` inside the caller's `config=$(facade + ...)` only exits the SUBSHELL, so config became EMPTY (the task-033 wipe class), + not a clean abort. +- FIX: `*)` arm now `log "...skipping..." "warn"; echo "$config"; return 1` + (config echoed UNCHANGED, never empty; non-zero = "skipped"). Supported arms + keep their genuine `exit 1` paths (ss base64 fail, vmess decode fail) — those + STILL only exit the subshell, which the callers now treat as skip too (defense: + see the `[ -n "$_new_config" ]` guard below). Did NOT touch supported parsing. +- SKIP CONTRACT (callers): non-zero return + unchanged echo. Caller pattern is + `if _new_config="$(facade ...)" && [ -n "$_new_config" ]; then config="$_new_config"; <use it> else <skip>`. + The `[ -n ]` guard is belt-and-suspenders so a genuine supported-arm `exit 1` + (empty echo) ALSO degrades to skip rather than wiping `config`. +- CALLER AUDIT (all 4): + * url single (bin ~2151): on skip → `echolog ... error` + `mark_section_outbound_unavailable` + so the route emits a REJECT rule (section degrades to unavailable). No exit. + * selector loop (bin ~2182): add the member tag ONLY on facade success → no + dangling selector member. All-skipped → empty `outbound_tags` → mark + unavailable, don't build the selector. + * urltest loop (bin ~2228): same as selector (urltest+selector members clean). + * subscription normalize (helpers ~1489): ALREADY graceful — pre-filters + schemes to `vless|trojan|ss|hysteria2|hy2|socks*` (the `*)` arm is never even + reached there) AND tolerates non-zero via `|| { continue }` + JSON/count + guards. No change needed; my facade change is compatible. +- SINGLE-URL DEGRADE MECHANISM: new tiny helper `mark_section_outbound_unavailable` + (next to `mark_subscription_outbound_unavailable`) just appends the section to + `SUBSCRIPTION_UNAVAILABLE_SECTIONS` (the SAME flag `subscription_outbound_is_unavailable` + reads), so `sing_box_configure_route` emits `sing_box_cm_add_reject_route_rule` + for it (bin ~2818). Does NOT touch any per-URL subscription rejected-hash cache + (that is subscription-only). Reuses the existing unavailable-section plumbing. +- PROVEN: `sing-box check` does NOT reject a route rule whose `outbound` points + at a MISSING outbound, NOR a selector/urltest with a missing member (both rc=0 + in-container). So a dangling tag wouldn't fail `check` — but it would blackhole + traffic at runtime, hence we still keep selector members clean + mark unavailable. +- DEFECT 2 (splithttp = pre-rename xhttp): facade `_add_outbound_transport` now + matches `xhttp | splithttp` (same `sing_box_cm_set_xhttp_transport_for_outbound`, + same `is_sing_box_extended` gate); `_add_outbound_security` ALPN-default branch + also accepts `type=splithttp`. `xray_json_to_uri_lines` (helpers.sh): normalize + `$net` `splithttp`→`xhttp` and read settings from `($ss.xhttpSettings // + $ss.splithttpSettings // {})`. Emitted URI always carries the modern `type=xhttp`. +- **JQ-IN-SHELL-STRING APOSTROPHE LANDMINE (cost a debug cycle):** an apostrophe + in a jq COMMENT inside a `jq -er '...'` single-quoted shell string CLOSES the + shell string. I wrote `# ... the facade's xhttp branch ...` and shellcheck + reported SC1073/SC1056/SC1072 brace errors on the FUNCTION line, not the + comment — because everything after the `'` was parsed as shell. It would ALSO + break the real jq at runtime. NEVER put `'` in a jq comment (or any char that + closes the surrounding shell quote). Rewrote the comments apostrophe-free. +- **`((` at a jq pipe-element start trips shellcheck** (reads it as arithmetic + `$((...))` context → SC1056/1072): `| (($x // "y") | if ...)` failed; split it + into `| ($x // "y") as $tmp | (if $tmp ...) as $net` (single leading `(`). +- TEST: new top-level `test_unsupported_skip` (alias `unsupported`, after + test_monitor_fd_hygiene). awk-extracts the SHIPPED `configure_outbound_handler` + + `mark_section_outbound_unavailable` verbatim, sources real + facade/manager/helpers/constants (symlink to /usr/lib/netshift), table-driven + `config_get` stub via `US_<section>_<opt>` vars, log stub recording level|msg. + Cases: (1) urltest + (1b) selector mix supported(vless/hysteria2)+unsupported + (tuic/wireguard/garbage) → no-abort, supported present, unsupported absent, + members clean, warning logged, config not wiped, live `sing-box check`; (2) + single-URL only-tuic → no crash, no outbound, direct-out survives, section + marked unavailable, error logged; (3a) vless `?type=splithttp` → transport.type + xhttp + path + extended-gate-off respected; (3b) Xray-JSON + network:"splithttp"/splithttpSettings → URI carries `type=xhttp`,`path=/xj`, no + literal `splithttp`. The xhttp `sing-box check` SKIPs on the container's STOCK + core (rejects xhttp) — assert-only-when-accepted, else SKIP. Registered all 5 + points (all)/alias/usage/docker-compose comment). +- GATES: shellcheck -S error clean (install.sh + bin + lib/*.sh + tests). All + files UTF-8 intact (syntax test mojibake guard green). `smoke-tests all` = 155 + passed / 0 failed (same suite total — the new test's per-line passes run in the + documented piped-while subshell, so the ✓ marks are truth: 23 green + 1 SKIP). + task-037's hysteria2 marks (fb-caseL/N/O) all still green. Pre-existing + `rh-case1/2/6:FAIL` red marks persist (task-031 quirk; suite EXIT=0). No sacred + value/port/mark/path/ACL changed. diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index eb70f9c9..f291efbb 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -364,6 +364,24 @@ subscription_outbound_is_unavailable() { return 1 } +# Mark a non-subscription proxy section (url/selector/urltest) as having no +# usable outbound, so sing_box_configure_route emits a reject route rule for it +# (like the subscription-unavailable path) instead of a route rule pointing at a +# never-created outbound tag. Used when every configured link for the section +# was an unsupported scheme and got skipped by the facade. This reuses the +# existing SUBSCRIPTION_UNAVAILABLE_SECTIONS mechanism (subscription_outbound_is_unavailable), +# but does NOT touch any per-URL subscription rejected-hash cache (that is +# subscription-only). The section degrades to "matching traffic rejected" +# without aborting generation of the rest of the config. +mark_section_outbound_unavailable() { + local section="$1" + + case " $SUBSCRIPTION_UNAVAILABLE_SECTIONS " in + *" $section "*) ;; + *) SUBSCRIPTION_UNAVAILABLE_SECTIONS="$SUBSCRIPTION_UNAVAILABLE_SECTIONS $section" ;; + esac +} + get_subscription_download_proxy_address() { local section="$1" local phase="$2" @@ -2124,7 +2142,21 @@ configure_outbound_handler() { log "Proxy string is not set. Aborted." "fatal" exit 1 fi - config=$(sing_box_cf_add_proxy_outbound "$config" "$section" "$proxy_string" "$udp_over_tcp") + # The facade returns non-zero (config echoed UNCHANGED) when the link + # uses an unsupported scheme. For a single-URL section that is the + # only node, so there is no outbound to fall back to: surface a clear + # error and mark the section unavailable (reject route rule) instead + # of exiting — the rest of the config still generates and the service + # still starts. Reassign $config only on a non-empty result so a + # skip (or a defensively-empty echo) never wipes the config. + local _new_config + if _new_config="$(sing_box_cf_add_proxy_outbound "$config" "$section" "$proxy_string" "$udp_over_tcp")" \ + && [ -n "$_new_config" ]; then + config="$_new_config" + else + echolog "Proxy link for section '$section' uses an unsupported scheme and was skipped; this section has no usable outbound and its traffic will be rejected until a supported link is configured" "error" + mark_section_outbound_unavailable "$section" + fi ;; outbound) log "Detected proxy configuration type: outbound" "debug" @@ -2134,7 +2166,7 @@ configure_outbound_handler() { ;; selector) log "Detected proxy configuration type: selector" "debug" - local selector_proxy_links udp_over_tcp i outbound_tags outbound_tag default_outbound + local selector_proxy_links udp_over_tcp i outbound_tags outbound_tag default_outbound _new_config config_get selector_proxy_links "$section" "selector_proxy_links" config_get udp_over_tcp "$section" "enable_udp_over_tcp" @@ -2145,26 +2177,42 @@ configure_outbound_handler() { i=1 for link in $selector_proxy_links; do - config="$(sing_box_cf_add_proxy_outbound "$config" "$section-$i" "$link" "$udp_over_tcp")" - outbound_tag="$(get_outbound_tag_by_section "$section-$i")" - if [ -z "$outbound_tags" ]; then - outbound_tags="$outbound_tag" - default_outbound="$outbound_tag" + # The facade returns non-zero (config echoed UNCHANGED) for an + # unsupported scheme. Only add the member tag when the outbound + # was actually created, so the selector never references a + # non-existent outbound; a single bad link is skipped and the + # remaining links still build. Reassign $config only on a + # non-empty result so a skip never wipes the config. + if _new_config="$(sing_box_cf_add_proxy_outbound "$config" "$section-$i" "$link" "$udp_over_tcp")" \ + && [ -n "$_new_config" ]; then + config="$_new_config" + outbound_tag="$(get_outbound_tag_by_section "$section-$i")" + if [ -z "$outbound_tags" ]; then + outbound_tags="$outbound_tag" + default_outbound="$outbound_tag" + else + outbound_tags="$outbound_tags,$outbound_tag" + fi else - outbound_tags="$outbound_tags,$outbound_tag" + log "Selector section '$section' link #$i uses an unsupported scheme; skipping it" "warn" fi i=$((i + 1)) done - selector_tag="$(get_outbound_tag_by_section "$section")" - selector_outbounds="$(comma_string_to_json_array "$outbound_tags")" - config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" \ - "$default_outbound" "true")" + if [ -z "$outbound_tags" ]; then + echolog "Selector section '$section' has no usable links (all unsupported); its traffic will be rejected until a supported link is configured" "error" + mark_section_outbound_unavailable "$section" + else + selector_tag="$(get_outbound_tag_by_section "$section")" + selector_outbounds="$(comma_string_to_json_array "$outbound_tags")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" \ + "$default_outbound" "true")" + fi ;; urltest) log "Detected proxy configuration type: urltest" "debug" local urltest_proxy_links udp_over_tcp i urltest_tag selector_tag outbound_tag outbound_tags \ - urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance urltest_testing_url + urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance urltest_testing_url _new_config config_get urltest_proxy_links "$section" "urltest_proxy_links" config_get udp_over_tcp "$section" "enable_udp_over_tcp" config_get urltest_check_interval "$section" "urltest_check_interval" "3m" @@ -2178,23 +2226,39 @@ configure_outbound_handler() { i=1 for link in $urltest_proxy_links; do - config="$(sing_box_cf_add_proxy_outbound "$config" "$section-$i" "$link" "$udp_over_tcp")" - outbound_tag="$(get_outbound_tag_by_section "$section-$i")" - if [ -z "$outbound_tags" ]; then - outbound_tags="$outbound_tag" + # The facade returns non-zero (config echoed UNCHANGED) for an + # unsupported scheme. Only add the member tag when the outbound + # was actually created, so the urltest/selector never references + # a non-existent outbound; a single bad link is skipped and the + # remaining links still build. Reassign $config only on a + # non-empty result so a skip never wipes the config. + if _new_config="$(sing_box_cf_add_proxy_outbound "$config" "$section-$i" "$link" "$udp_over_tcp")" \ + && [ -n "$_new_config" ]; then + config="$_new_config" + outbound_tag="$(get_outbound_tag_by_section "$section-$i")" + if [ -z "$outbound_tags" ]; then + outbound_tags="$outbound_tag" + else + outbound_tags="$outbound_tags,$outbound_tag" + fi else - outbound_tags="$outbound_tags,$outbound_tag" + log "URLTest section '$section' link #$i uses an unsupported scheme; skipping it" "warn" fi i=$((i + 1)) done - urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" - selector_tag="$(get_outbound_tag_by_section "$section")" - urltest_outbounds="$(comma_string_to_json_array "$outbound_tags")" - selector_outbounds="$(comma_string_to_json_array "$outbound_tags,$urltest_tag")" - config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ - "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" - config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" + if [ -z "$outbound_tags" ]; then + echolog "URLTest section '$section' has no usable links (all unsupported); its traffic will be rejected until a supported link is configured" "error" + mark_section_outbound_unavailable "$section" + else + urltest_tag="$(get_outbound_tag_by_section "$section-urltest")" + selector_tag="$(get_outbound_tag_by_section "$section")" + urltest_outbounds="$(comma_string_to_json_array "$outbound_tags")" + selector_outbounds="$(comma_string_to_json_array "$outbound_tags,$urltest_tag")" + config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \ + "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" + config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag" "true")" + fi ;; subscription) log "Detected proxy configuration type: subscription" "debug" diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index a298a463..93e5dbe7 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -1184,17 +1184,34 @@ xray_json_to_uri_lines() { | (.outbounds // [])[] | select(type == "object") | select(.protocol == "vless" or .protocol == "trojan" - or .protocol == "shadowsocks") + or .protocol == "shadowsocks" + or .protocol == "hysteria") # Skip chained / multi-hop outbounds: not representable as one URI. | select((.streamSettings.sockopt.dialerProxy // "") == "") + # Hysteria here is always Hysteria2 (hysteriaSettings.version == 2); + # the facade has no Hysteria v1 parser, so skip v1/missing-version + # silently (no fatal). vless/trojan/shadowsocks are unaffected. + | select(.protocol != "hysteria" + or ((.streamSettings.hysteriaSettings.version // 0) == 2)) | . as $ob | (.streamSettings // {}) as $ss - | ($ss.network // "tcp") as $net + # splithttp is the pre-rename name of the xhttp transport (sing-box + # renamed it). Normalize it to xhttp so the emitted URI uses the modern + # name and the facade xhttp branch handles it. No regex. + | ($ss.network // "tcp") as $net_raw + | (if $net_raw == "splithttp" then "xhttp" else $net_raw end) as $net + # xhttp transport settings live under xhttpSettings, or the pre-rename + # splithttpSettings alias. + | ($ss.xhttpSettings // $ss.splithttpSettings // {}) as $xs | ($ss.security // "") as $sec | ($ss.realitySettings // {}) as $reality | ($ss.tlsSettings // $ss.realitySettings // {}) as $tls - # vnext (vless/vmess) vs servers (trojan/shadowsocks) addressing. - | ($ob.settings.vnext[0] // $ob.settings.servers[0] // {}) as $peer + # Addressing: vnext (vless/vmess) vs servers (trojan/shadowsocks); + # hysteria carries the peer directly in settings.address/settings.port + # (no vnext/servers), so branch the peer derivation on protocol. + | (if $ob.protocol == "hysteria" + then {address: $ob.settings.address, port: $ob.settings.port} + else ($ob.settings.vnext[0] // $ob.settings.servers[0] // {}) end) as $peer | ($peer.users[0] // {}) as $user | ($peer.address // "") as $host | ($peer.port // "") as $port @@ -1220,6 +1237,18 @@ xray_json_to_uri_lines() { (if $sec != "" then ("security=" + ($sec)) else "security=tls" end), kv("sni"; ($tls.serverName // "")), kv("fp"; ($tls.fingerprint // "")) ] + elif $ob.protocol == "hysteria" then + # Hysteria2: no stream transport, so DO NOT emit type=. The + # facade defaults security to tls for hysteria2 and reads + # sni/insecure (via _add_outbound_security), obfs/obfs-password. + ($ss.hysteriaSettings // {}) as $hy + | [ kv("sni"; ($tls.serverName // "")), + (if (($tls.allowInsecure // $tls.insecure // false) == true) + then "insecure=1" else empty end) ] + + (if ($hy.obfs // "") != "" then + [ "obfs=salamander", + kv("obfs-password"; ($hy.obfsPassword // $hy.obfs_password // "")) ] + else [] end) else [ ("type=" + $net) ] end @@ -1230,9 +1259,12 @@ xray_json_to_uri_lines() { [ kv("path"; ($ss.wsSettings.path // "")), kv("host"; ($ss.wsSettings.headers.Host // "")) ] elif $net == "xhttp" then - [ kv("path"; ($ss.xhttpSettings.path // "")), - kv("host"; ($ss.xhttpSettings.host // "")), - kv("mode"; ($ss.xhttpSettings.mode // "")) ] + # Accept both the modern xhttpSettings and the pre-rename + # splithttpSettings key (network was normalized to xhttp above). + # $xs binds to whichever settings object is present. + [ kv("path"; ($xs.path // "")), + kv("host"; ($xs.host // "")), + kv("mode"; ($xs.mode // "")) ] elif $net == "grpc" then [ kv("serviceName"; ($ss.grpcSettings.serviceName // "")) ] else [] end @@ -1242,12 +1274,17 @@ xray_json_to_uri_lines() { | ($base + $transport + (if $alpn_str != "" then [ kv("alpn"; $alpn_str) ] else [] end) | map(select(. != null and . != ""))) as $query - # Credential: uuid for vless, password for trojan/shadowsocks. + # Credential: uuid for vless, hysteriaSettings.auth for hysteria, + # password for trojan/shadowsocks. | (if $ob.protocol == "vless" then ($user.id // "") + elif $ob.protocol == "hysteria" then + ($ss.hysteriaSettings.auth // "") else ($peer.password // $ob.settings.password // "") end) as $cred | select($cred != "") | ($ob.protocol - | if . == "shadowsocks" then "ss" else . end) as $scheme + | if . == "shadowsocks" then "ss" + elif . == "hysteria" then "hysteria2" + else . end) as $scheme # The connection part (no #fragment) is the dedup key: providers that # ship one server set across many "profiles" repeat identical nodes # with only the display name differing, which would otherwise inflate diff --git a/netshift/files/usr/lib/sing_box_config_facade.sh b/netshift/files/usr/lib/sing_box_config_facade.sh index 28b41287..d1f77b93 100644 --- a/netshift/files/usr/lib/sing_box_config_facade.sh +++ b/netshift/files/usr/lib/sing_box_config_facade.sh @@ -174,7 +174,17 @@ sing_box_cf_add_proxy_outbound() { # Generation is gated behind sing-box-extended. On a stock sing-box build # we log a clear message and return the config UNCHANGED (no exit 1, no # outbound added) so generation degrades safely and keeps the last-good - # config. tuic/hysteria1/anytls/shadowtls reuse this exact block. + # config. + # + # Schemes this dispatcher PARSES: socks4/socks4a/socks5, vless, ss, + # trojan, hysteria2/hy2, and vmess (vmess is extended-gated above). Any + # OTHER scheme (tuic, hysteria v1, anytls, shadowtls, wireguard, http, + # or a typo) is NOT parsed here: it hits the default `*)` arm below, + # which logs a WARNING and returns the config UNCHANGED (rc 1, skip) so a + # single bad link in a url/selector/urltest input never aborts the whole + # config. Such schemes are reachable only via a raw `outbound_json` + # connection (proxy_config_type=outbound) or a native sing-box-JSON + # subscription, which bypass this URL dispatcher entirely. if ! is_sing_box_extended; then log "VMess requires sing-box-extended. Install sing-box-extended and retry." "error" echo "$config" @@ -221,8 +231,17 @@ sing_box_cf_add_proxy_outbound() { "$vm_tls" "$vm_sni" "$vm_alpn" "$vm_fp" "$vm_server") ;; *) - log "Unsupported proxy $scheme type. Aborted." "fatal" - exit 1 + # Unsupported scheme: downgrade from fatal to a WARNING + skip. This + # dispatcher is shared by the single-URL, selector-loop and urltest-loop + # callers, so a single unsupported/typo'd link must NOT abort generation + # of the whole config. Echo the input config UNCHANGED (never empty — an + # empty echo would wipe the caller's `config=$(...)`) and return non-zero + # so the caller knows no outbound was added and can skip this node and + # continue with the remaining links. The subscription normalize caller + # already understands this non-zero "skip" contract. + log "Unsupported proxy scheme '$scheme'; skipping this link (supported: socks/vless/ss/trojan/hysteria2/vmess)" "warn" + echo "$config" + return 1 ;; esac @@ -255,8 +274,9 @@ _add_outbound_security() { short_id=$(url_get_query_param "$url" "sid") # XHTTP transport defaults its ALPN to h2/http/1.1 when none is provided. + # `splithttp` is the pre-rename alias of `xhttp` (see _add_outbound_transport). transport_type=$(url_get_query_param "$url" "type") - if [ "$transport_type" = "xhttp" ] && [ "$alpn" = "[]" ]; then + if { [ "$transport_type" = "xhttp" ] || [ "$transport_type" = "splithttp" ]; } && [ "$alpn" = "[]" ]; then alpn='["h2","http/1.1"]' fi @@ -325,7 +345,11 @@ _add_outbound_transport() { sing_box_cm_set_grpc_transport_for_outbound "$config" "$outbound_tag" "$grpc_service_name" ) ;; - xhttp) + xhttp | splithttp) + # `splithttp` is the pre-rename name of the `xhttp` transport (sing-box + # renamed it). Accept it as an alias and normalize to xhttp downstream: + # sing_box_cm_set_xhttp_transport_for_outbound emits the modern `xhttp` + # key, so the config sing-box sees always uses the current name. if ! is_sing_box_extended; then log "XHTTP transport requires sing-box-extended. Install sing-box-extended and retry." "error" echo "$config" diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index bd177b0d..e4668f69 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -9,7 +9,7 @@ # docker compose -f tests/docker-compose.yml run --rm netshift-test <test-name> # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, -# nftv6, selmark, isolation, monfd, diagnostics, subscription, insecure, rejected, +# nftv6, selmark, isolation, monfd, unsupported, diagnostics, subscription, insecure, rejected, # jobstate, selfheal, dnsdetour, globalproxy, stablecheck, # extcheck, netshiftcheck, selfupdate, backupguard # ────────────────────────────────────────────────────────────────── diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 54f87df9..0086bdf0 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -1011,6 +1011,267 @@ SIEOF rm -f "$drv" "$route_json" } +# ───────────────────────────────────────────────────────────────── +# Test: graceful-skip of unsupported proxy schemes + splithttp→xhttp (task-038) +# ───────────────────────────────────────────────────────────────── +# Two defects fixed by task-038: +# 1. sing_box_cf_add_proxy_outbound's `*)` default arm used to log fatal + exit 1 +# for an unsupported scheme. Since the dispatcher is shared by the single-URL, +# selector-loop AND urltest-loop callers, ONE bad link (tuic/wireguard/typo) +# aborted generation of the WHOLE config. It now logs a WARNING, echoes the +# config UNCHANGED (never empty) and returns non-zero so the caller skips that +# node and continues. Loop callers add the member tag only on success (no +# dangling selector member); an all-unsupported section is marked unavailable +# (reject route rule) instead of crashing the start. +# 2. `splithttp` (the pre-rename name of `xhttp`) is now accepted as an alias of +# xhttp in BOTH the facade transport builder (?type=splithttp) and the +# xray_json_to_uri_lines converter (network:"splithttp" / splithttpSettings), +# normalized to the modern `xhttp` key downstream. +# +# This test drives the SHIPPED configure_outbound_handler (awk-extracted verbatim) +# for the url/selector/urltest branches with a table-driven config_get stub, the +# REAL facade/manager/helpers, and a log stub that records warnings/errors. All +# values are synthetic placeholders (nothing from private.json). +test_unsupported_skip() { + header "Graceful-skip unsupported protocol + splithttp alias (task-038)" + + if ! command -v sing-box > /dev/null 2>&1; then + skip "sing-box not installed" + return + fi + + local lib="${NETSHIFT_LIB_DIR}" + local bin="${NETSHIFT_SRC}/usr/bin/netshift" + local facade_lib="$lib/sing_box_config_facade.sh" + if [ ! -r "$facade_lib" ] || [ ! -r "$bin" ]; then + fail "facade lib / bin not found" + return + fi + + # The facade hardcodes NETSHIFT_LIB="/usr/lib/netshift" for its own sourcing + # of helpers + manager; bind the bind-mounted sources to that path. + mkdir -p /usr/lib/netshift + ln -sf "$lib/helpers.sh" /usr/lib/netshift/helpers.sh + ln -sf "$lib/sing_box_config_manager.sh" /usr/lib/netshift/sing_box_config_manager.sh + + local drv="/tmp/test-unsupported-skip-$$.sh" + cat > "$drv" << 'USEOF' +. "CONST_LIB" +. "FACADE_LIB" + +WARN_LOG="/tmp/us-warn-$$.log" +: > "$WARN_LOG" +# log/echolog/nolog: record level+message so we can assert a warning fired. +log() { printf '%s|%s\n' "${2:-info}" "$1" >> "$WARN_LOG"; } +echolog() { printf '%s|%s\n' "${2:-info}" "$1" >> "$WARN_LOG"; } +nolog() { :; } + +# Extended ON so vmess/xhttp gates pass where used. +is_sing_box_extended() { return 0; } + +# awk-extract the SHIPPED handler + the unavailable marker verbatim. +eval "$(awk '/^configure_outbound_handler\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")" +eval "$(awk '/^mark_section_outbound_unavailable\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")" + +# Table-driven UCI stub. Per-section options are read from US_<section>_<opt> +# shell vars (dots/dashes in section names normalized to underscores). +_us_key() { printf 'US_%s_%s' "$(printf '%s' "$1" | tr '.-' '__')" "$2"; } +config_get() { + # $1=dest var, $2=section, $3=option, $4=default + local _k _v + _k="$(_us_key "$2" "$3")" + eval "_v=\"\${$_k:-}\"" + [ -n "$_v" ] || _v="$4" + eval "$1=\"\$_v\"" + return 0 +} +config_get_bool() { + local _k _v + _k="$(_us_key "$2" "$3")" + eval "_v=\"\${$_k:-${4:-0}}\"" + eval "$1=\"\$_v\"" + return 0 +} + +# Helper: assert a warn/error log line containing a substring exists. +warn_logged() { grep -q "$1" "$WARN_LOG"; } + +# Build a minimal full sing-box config around the produced outbounds and run a +# real `sing-box check`. $1=config JSON, $2=label. +check_full() { + local cfgjson="$1" label="$2" full + full="/tmp/us-full-$$-${label}.json" + printf '%s' "$cfgjson" | jq '{ + log: { level: "error" }, + dns: { servers: [ { tag: "dns-server", type: "udp", server: "1.1.1.1" } ], final: "dns-server" }, + inbounds: [ { type: "tproxy", tag: "tproxy-in", listen: "127.0.0.1", listen_port: 1602 } ], + outbounds: (.outbounds + [ { type: "direct", tag: "direct-out" } ]), + route: { rules: [], final: "direct-out" } + }' > "$full" 2>/dev/null + if sing-box -c "$full" check > /dev/null 2>&1; then + echo "${label}:OK" + else + echo "${label}:FAIL" + fi + rm -f "$full" +} + +# ── (1) URLTEST list mixing supported (vless/hysteria2) + unsupported ──────── +# (tuic:// / wireguard:// / garbage://). Generation must NOT abort, the +# supported members must be present, the unsupported ones skipped, and a +# warning logged. config must NOT be wiped. +: > "$WARN_LOG" +config='{"outbounds":[]}' +SUBSCRIPTION_UNAVAILABLE_SECTIONS="" +US_mix_connection_type="proxy" +US_mix_proxy_config_type="urltest" +US_mix_urltest_proxy_links="vless://11111111-2222-3333-4444-555555555555@v.example.com:443?security=tls&sni=v.example.com tuic://uuid:pw@t.example.com:443 hysteria2://hpass@h.example.com:8443?sni=h.example.com wireguard://x@w.example.com:51820 garbage://nope" +configure_outbound_handler "mix" +mix_rc=$? + +[ "$mix_rc" = "0" ] && echo 'us-urltest-no-abort:OK' || echo "us-urltest-no-abort:FAIL (rc=$mix_rc)" +[ -n "$config" ] && printf '%s' "$config" | jq -e . >/dev/null 2>&1 \ + && echo 'us-urltest-config-not-wiped:OK' || echo 'us-urltest-config-not-wiped:FAIL' + +# The two supported member outbounds exist (vless = mix-1-out, hysteria2 = mix-3-out). +printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="mix-1-out" and .type=="vless")] | length==1' >/dev/null 2>&1 \ + && echo 'us-urltest-vless-present:OK' || echo 'us-urltest-vless-present:FAIL' +printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="mix-3-out" and .type=="hysteria2")] | length==1' >/dev/null 2>&1 \ + && echo 'us-urltest-hy2-present:OK' || echo 'us-urltest-hy2-present:FAIL' + +# The unsupported members were NOT created. +printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="mix-2-out" or .tag=="mix-4-out" or .tag=="mix-5-out")] | length==0' >/dev/null 2>&1 \ + && echo 'us-urltest-unsupported-absent:OK' || echo 'us-urltest-unsupported-absent:FAIL' + +# The urltest + selector reference ONLY the two real members (no dangling tag). +printf '%s' "$config" | jq -e '[.outbounds[] | select(.type=="urltest")][0].outbounds | (index("mix-1-out")!=null and index("mix-3-out")!=null and index("mix-2-out")==null and index("mix-4-out")==null and index("mix-5-out")==null)' >/dev/null 2>&1 \ + && echo 'us-urltest-members-clean:OK' || echo 'us-urltest-members-clean:FAIL' + +# A warning was logged for the skipped schemes. +warn_logged "unsupported scheme" && echo 'us-urltest-warning-logged:OK' || echo 'us-urltest-warning-logged:FAIL' + +# Whole-chain: the assembled config passes a real sing-box check. +check_full "$config" "us-urltest-singbox-check" + +# ── (1b) SELECTOR list, same mix ───────────────────────────────────────────── +: > "$WARN_LOG" +config='{"outbounds":[]}' +SUBSCRIPTION_UNAVAILABLE_SECTIONS="" +US_sel_connection_type="proxy" +US_sel_proxy_config_type="selector" +US_sel_selector_proxy_links="garbage://nope vless://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee@v2.example.com:443?security=tls&sni=v2.example.com tuic://u:p@t2.example.com:443" +configure_outbound_handler "sel" +sel_rc=$? +[ "$sel_rc" = "0" ] && echo 'us-selector-no-abort:OK' || echo "us-selector-no-abort:FAIL (rc=$sel_rc)" +# Only the vless (sel-2-out) member exists; selector references just it. +printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="sel-2-out" and .type=="vless")] | length==1' >/dev/null 2>&1 \ + && echo 'us-selector-vless-present:OK' || echo 'us-selector-vless-present:FAIL' +printf '%s' "$config" | jq -e '[.outbounds[] | select(.type=="selector")][0].outbounds | (index("sel-2-out")!=null and index("sel-1-out")==null and index("sel-3-out")==null)' >/dev/null 2>&1 \ + && echo 'us-selector-members-clean:OK' || echo 'us-selector-members-clean:FAIL' +check_full "$config" "us-selector-singbox-check" + +# ── (2) SINGLE-URL section with ONLY an unsupported scheme → degrade ───────── +# No crash, no outbound, section marked unavailable, rest of config +# continues to generate. +: > "$WARN_LOG" +config='{"outbounds":[{"type":"direct","tag":"direct-out"}]}' +SUBSCRIPTION_UNAVAILABLE_SECTIONS="" +US_solo_connection_type="proxy" +US_solo_proxy_config_type="url" +US_solo_proxy_string="tuic://uuid:pw@only.example.com:443" +configure_outbound_handler "solo" +solo_rc=$? +[ "$solo_rc" = "0" ] && echo 'us-single-no-crash:OK' || echo "us-single-no-crash:FAIL (rc=$solo_rc)" +# No solo-out outbound was created. +printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="solo-out")] | length==0' >/dev/null 2>&1 \ + && echo 'us-single-no-outbound:OK' || echo 'us-single-no-outbound:FAIL' +# The pre-existing direct-out (rest of config) survived (config not wiped). +printf '%s' "$config" | jq -e '[.outbounds[] | select(.tag=="direct-out")] | length==1' >/dev/null 2>&1 \ + && echo 'us-single-rest-continues:OK' || echo 'us-single-rest-continues:FAIL' +# Section marked unavailable so the route emits a reject rule. +case " $SUBSCRIPTION_UNAVAILABLE_SECTIONS " in +*" solo "*) echo 'us-single-marked-unavailable:OK' ;; +*) echo 'us-single-marked-unavailable:FAIL' ;; +esac +warn_logged "no usable outbound" && echo 'us-single-error-logged:OK' || echo 'us-single-error-logged:FAIL' + +# ── (3a) splithttp recognized as xhttp via a vless URL ?type=splithttp ─────── +base='{"outbounds":[]}' +out_split=$(sing_box_cf_add_proxy_outbound "$base" "spl" "vless://77777777-8888-9999-aaaa-bbbbbbbbbbbb@s.example.com:8443?type=splithttp&security=tls&sni=s.example.com&path=/sp&host=s.example.com&mode=auto" "0") +printf '%s' "$out_split" | jq -e '.outbounds[0].transport.type=="xhttp"' >/dev/null 2>&1 \ + && echo 'us-splithttp-url-xhttp:OK' || echo 'us-splithttp-url-xhttp:FAIL' +printf '%s' "$out_split" | jq -e '.outbounds[0].transport.path=="/sp"' >/dev/null 2>&1 \ + && echo 'us-splithttp-url-path:OK' || echo 'us-splithttp-url-path:FAIL' +# Extended gate respected: with extended OFF the transport is NOT applied. +is_sing_box_extended() { return 1; } +out_split_off=$(sing_box_cf_add_proxy_outbound "$base" "splo" "vless://77777777-8888-9999-aaaa-bbbbbbbbbbbb@s.example.com:8443?type=splithttp&security=tls&sni=s.example.com&path=/sp&host=s.example.com&mode=auto" "0") +printf '%s' "$out_split_off" | jq -e '.outbounds[0] | has("transport") | not' >/dev/null 2>&1 \ + && echo 'us-splithttp-gate-off:OK' || echo 'us-splithttp-gate-off:FAIL' +is_sing_box_extended() { return 0; } +# Whole-chain: the splithttp(→xhttp) outbound passes a real sing-box check on +# extended (the container core may be stock, so only assert when it accepts +# xhttp; otherwise emit SKIP). +spl_full="/tmp/us-split-full-$$.json" +printf '%s' "$out_split" | jq '{ + log: { level: "error" }, + inbounds: [], + outbounds: (.outbounds + [ { type: "direct", tag: "direct-out" } ]), + route: { final: "direct-out" } +}' > "$spl_full" 2>/dev/null +if sing-box -c "$spl_full" check > /dev/null 2>&1; then + echo 'us-splithttp-singbox-check:OK' +else + echo 'us-splithttp-singbox-check:SKIP' +fi +rm -f "$spl_full" + +# ── (3b) splithttp recognized in xray_json_to_uri_lines (Xray JSON) ────────── +xray_src="/tmp/us-xray-split-$$.json" +cat > "$xray_src" << 'XJSON' +{ "outbounds": [ { + "protocol": "vless", + "tag": "xray-split", + "settings": { "vnext": [ { "address": "xj.example.com", "port": 8443, "users": [ { "id": "cccccccc-dddd-eeee-ffff-000000000000" } ] } ] }, + "streamSettings": { + "network": "splithttp", + "security": "tls", + "tlsSettings": { "serverName": "xj.example.com" }, + "splithttpSettings": { "path": "/xj", "host": "xj.example.com", "mode": "auto" } + } +} ] } +XJSON +xray_uri="$(xray_json_to_uri_lines "$xray_src" 2>/dev/null)" +case "$xray_uri" in +*"type=xhttp"*) echo 'us-xray-splithttp-type-xhttp:OK' ;; +*) echo "us-xray-splithttp-type-xhttp:FAIL ($xray_uri)" ;; +esac +case "$xray_uri" in +*"path=/xj"*) echo 'us-xray-splithttp-path:OK' ;; +*) echo "us-xray-splithttp-path:FAIL ($xray_uri)" ;; +esac +case "$xray_uri" in +*"splithttp"*) echo "us-xray-splithttp-normalized:FAIL ($xray_uri)" ;; +*) echo 'us-xray-splithttp-normalized:OK' ;; +esac +rm -f "$xray_src" + +rm -f "$WARN_LOG" +echo 'DONE' +USEOF + sed -i "s|CONST_LIB|$lib/constants.sh|g; s|FACADE_LIB|$facade_lib|g; s|BIN_PATH|$bin|g" "$drv" + + sh "$drv" 2>/dev/null | while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + *:SKIP) skip "$line" ;; + DONE) ;; + *) ;; + esac + done + rm -f "$drv" +} + # ───────────────────────────────────────────────────────────────── # Test: Monitor procd-lock fd hygiene (task-035) + monitor-leak (task-036) # @@ -2492,6 +2753,241 @@ else fi rm -f "$caseK_sub" +# ── CASE L: Xray JSON Hysteria2 (protocol "hysteria", version 2) ──── +# Real subscriptions ship Hysteria2 inside the Xray-JSON array as +# protocol:"hysteria" + streamSettings.network:"hysteria" + +# hysteriaSettings.{version:2, auth:<password>}; addressing in +# settings.address/port; TLS in tlsSettings.{serverName, alpn, allowInsecure}. +# xray_json_to_uri_lines must emit a hysteria2:// URI carrying the auth as +# userinfo, host:port from settings, and sni/alpn/insecure query params. +# (Synthetic placeholder values only — nothing from any real subscription.) +caseL_in="/tmp/netshift-fb-caseL-$$.json" +cat > "$caseL_in" << 'XRAYJSON' +[ + { + "remarks": "HY2 node", + "outbounds": [ + { + "protocol": "hysteria", + "tag": "hy2-tag", + "settings": {"address": "hy.example.com", "port": 8443}, + "streamSettings": {"network": "hysteria", "security": "tls", + "tlsSettings": {"serverName": "hy.example.com", + "alpn": ["h3"], "allowInsecure": true}, + "hysteriaSettings": {"version": 2, "auth": "testpass"}} + } + ] + } +] +XRAYJSON +caseL_uris="$(xray_json_to_uri_lines "$caseL_in" 2>/dev/null)" +# Exactly one URI emitted, and it is a hysteria2:// scheme. +caseL_n="$(printf '%s\n' "$caseL_uris" | grep -c .)" +if [ "$caseL_n" = "1" ] && printf '%s\n' "$caseL_uris" | grep -q '^hysteria2://'; then + echo 'fb-caseL-hy2-scheme:OK' +else + echo "fb-caseL-hy2-scheme(n=$caseL_n uris='$caseL_uris'):FAIL" +fi +# Auth as userinfo, host:port from settings. +if printf '%s\n' "$caseL_uris" | grep -q '^hysteria2://testpass@hy.example.com:8443'; then + echo 'fb-caseL-hy2-auth-host-port:OK' +else + echo "fb-caseL-hy2-auth-host-port(uris='$caseL_uris'):FAIL" +fi +# sni + alpn + insecure query params present; NO type= param. +if printf '%s\n' "$caseL_uris" | grep -q 'sni=hy.example.com' \ + && printf '%s\n' "$caseL_uris" | grep -q 'alpn=h3' \ + && printf '%s\n' "$caseL_uris" | grep -q 'insecure=1' \ + && ! printf '%s\n' "$caseL_uris" | grep -q 'type='; then + echo 'fb-caseL-hy2-query-params:OK' +else + echo "fb-caseL-hy2-query-params(uris='$caseL_uris'):FAIL" +fi +rm -f "$caseL_in" + +# ── CASE M: Hysteria v1 / missing version → skipped, no fatal ─────── +# The facade has no Hysteria v1 parser; the converter must select out any +# hysteria node whose hysteriaSettings.version is not 2 (and emit nothing), +# WITHOUT aborting/fatal. A v2 node in the same doc must still be emitted. +caseM_in="/tmp/netshift-fb-caseM-$$.json" +cat > "$caseM_in" << 'XRAYJSON' +[ + { + "remarks": "HY1 + missing + v2", + "outbounds": [ + { + "protocol": "hysteria", + "tag": "hy1", + "settings": {"address": "v1.example.com", "port": 443}, + "streamSettings": {"network": "hysteria", "security": "tls", + "tlsSettings": {"serverName": "v1.example.com"}, + "hysteriaSettings": {"version": 1, "auth": "testpass"}} + }, + { + "protocol": "hysteria", + "tag": "hy-noversion", + "settings": {"address": "nov.example.com", "port": 443}, + "streamSettings": {"network": "hysteria", "security": "tls", + "tlsSettings": {"serverName": "nov.example.com"}, + "hysteriaSettings": {"auth": "testpass"}} + }, + { + "protocol": "hysteria", + "tag": "hy2", + "settings": {"address": "v2.example.com", "port": 443}, + "streamSettings": {"network": "hysteria", "security": "tls", + "tlsSettings": {"serverName": "v2.example.com"}, + "hysteriaSettings": {"version": 2, "auth": "testpass"}} + } + ] + } +] +XRAYJSON +caseM_uris="$(xray_json_to_uri_lines "$caseM_in" 2>/dev/null)" +caseM_rc=$? +# Only the v2 node survives; v1 and missing-version are dropped silently. +caseM_n="$(printf '%s\n' "$caseM_uris" | grep -c .)" +if [ "$caseM_n" = "1" ] \ + && printf '%s\n' "$caseM_uris" | grep -q '@v2.example.com:443' \ + && ! printf '%s\n' "$caseM_uris" | grep -q 'v1.example.com' \ + && ! printf '%s\n' "$caseM_uris" | grep -q 'nov.example.com'; then + echo 'fb-caseM-v1-and-missing-skipped:OK' +else + echo "fb-caseM-v1-and-missing-skipped(rc=$caseM_rc n=$caseM_n uris='$caseM_uris'):FAIL" +fi +rm -f "$caseM_in" + +# ── CASE N: mixed vless+trojan+ss+hysteria2 each duplicated → dedup ── +# Four distinct nodes (one per protocol), each repeated across three configs +# with only the display tag/remarks differing. The existing $conn dedup must +# collapse them to exactly the four unique connections, first-seen order +# preserved (vless, trojan, ss, hysteria2). +caseN_in="/tmp/netshift-fb-caseN-$$.json" +cat > "$caseN_in" << 'XRAYJSON' +[ + {"remarks": "p1", "outbounds": [ + {"protocol": "vless", "tag": "vl-1", "settings": {"vnext": [{"address": "vl.example.com", "port": 443, + "users": [{"id": "00000000-0000-0000-0000-000000000000", "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "PK", "shortId": "ab", "serverName": "vl.example.com", "fingerprint": "firefox"}}}, + {"protocol": "trojan", "tag": "tj-1", "settings": {"servers": [{"address": "tj.example.com", "port": 8443, "password": "testpass"}]}, + "streamSettings": {"network": "tcp", "security": "tls", "tlsSettings": {"serverName": "tj.example.com"}}}, + {"protocol": "shadowsocks", "tag": "ss-1", "settings": {"servers": [{"address": "ss.example.com", "port": 8388, "password": "testpass", "method": "aes-256-gcm"}]}, + "streamSettings": {"network": "tcp"}}, + {"protocol": "hysteria", "tag": "hy-1", "settings": {"address": "hy.example.com", "port": 443}, + "streamSettings": {"network": "hysteria", "security": "tls", "tlsSettings": {"serverName": "hy.example.com"}, + "hysteriaSettings": {"version": 2, "auth": "testpass"}}} + ]}, + {"remarks": "p2", "outbounds": [ + {"protocol": "vless", "tag": "vl-2", "settings": {"vnext": [{"address": "vl.example.com", "port": 443, + "users": [{"id": "00000000-0000-0000-0000-000000000000", "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "PK", "shortId": "ab", "serverName": "vl.example.com", "fingerprint": "firefox"}}}, + {"protocol": "trojan", "tag": "tj-2", "settings": {"servers": [{"address": "tj.example.com", "port": 8443, "password": "testpass"}]}, + "streamSettings": {"network": "tcp", "security": "tls", "tlsSettings": {"serverName": "tj.example.com"}}}, + {"protocol": "shadowsocks", "tag": "ss-2", "settings": {"servers": [{"address": "ss.example.com", "port": 8388, "password": "testpass", "method": "aes-256-gcm"}]}, + "streamSettings": {"network": "tcp"}}, + {"protocol": "hysteria", "tag": "hy-2", "settings": {"address": "hy.example.com", "port": 443}, + "streamSettings": {"network": "hysteria", "security": "tls", "tlsSettings": {"serverName": "hy.example.com"}, + "hysteriaSettings": {"version": 2, "auth": "testpass"}}} + ]}, + {"remarks": "p3", "outbounds": [ + {"protocol": "vless", "tag": "vl-3", "settings": {"vnext": [{"address": "vl.example.com", "port": 443, + "users": [{"id": "00000000-0000-0000-0000-000000000000", "flow": "xtls-rprx-vision", "encryption": "none"}]}]}, + "streamSettings": {"network": "tcp", "security": "reality", + "realitySettings": {"publicKey": "PK", "shortId": "ab", "serverName": "vl.example.com", "fingerprint": "firefox"}}}, + {"protocol": "trojan", "tag": "tj-3", "settings": {"servers": [{"address": "tj.example.com", "port": 8443, "password": "testpass"}]}, + "streamSettings": {"network": "tcp", "security": "tls", "tlsSettings": {"serverName": "tj.example.com"}}}, + {"protocol": "shadowsocks", "tag": "ss-3", "settings": {"servers": [{"address": "ss.example.com", "port": 8388, "password": "testpass", "method": "aes-256-gcm"}]}, + "streamSettings": {"network": "tcp"}}, + {"protocol": "hysteria", "tag": "hy-3", "settings": {"address": "hy.example.com", "port": 443}, + "streamSettings": {"network": "hysteria", "security": "tls", "tlsSettings": {"serverName": "hy.example.com"}, + "hysteriaSettings": {"version": 2, "auth": "testpass"}}} + ]} +] +XRAYJSON +caseN_uris="$(xray_json_to_uri_lines "$caseN_in" 2>/dev/null)" +# 12 raw nodes (4 protocols x 3 profiles) collapse to exactly 4 unique conns. +caseN_n="$(printf '%s\n' "$caseN_uris" | grep -c .)" +if [ "$caseN_n" = "4" ]; then + echo 'fb-caseN-dedup-count(==4):OK' +else + echo "fb-caseN-dedup-count(==4 got $caseN_n uris='$caseN_uris'):FAIL" +fi +# First-seen order preserved: vless, trojan, ss, hysteria2 (scheme prefixes). +caseN_schemes="$(printf '%s\n' "$caseN_uris" | sed -e 's#://.*##' | tr '\n' ',' )" +if [ "$caseN_schemes" = "vless,trojan,ss,hysteria2," ]; then + echo 'fb-caseN-first-seen-order:OK' +else + echo "fb-caseN-first-seen-order(got '$caseN_schemes'):FAIL" +fi +rm -f "$caseN_in" + +# ── CASE O: end-to-end Hysteria2 through the facade + sing-box check ─ +# Feed the emitted hysteria2:// URI through normalize_subscription_to_singbox +# (the real subscription path) and assert a hysteria2 outbound is produced +# with the expected server/port/password and TLS. Then wrap the produced +# outbounds into a minimal full sing-box config and assert `sing-box check` +# passes (whole-chain validation; project-core.md §4). +caseO_in="/tmp/netshift-fb-caseO-$$.json" +caseO_out="/tmp/netshift-fb-caseO-out-$$.json" +cat > "$caseO_in" << 'XRAYJSON' +[ + { + "remarks": "HY2 e2e", + "outbounds": [ + { + "protocol": "hysteria", + "tag": "hy2-e2e", + "settings": {"address": "e2e.example.com", "port": 8443}, + "streamSettings": {"network": "hysteria", "security": "tls", + "tlsSettings": {"serverName": "e2e.example.com", "alpn": ["h3"]}, + "hysteriaSettings": {"version": 2, "auth": "testpass"}} + } + ] + } +] +XRAYJSON +if normalize_subscription_to_singbox "$caseO_in" "$caseO_out" "testsub"; then + echo 'fb-caseO-rc:OK' +else + echo 'fb-caseO-rc:FAIL' +fi +if validate_subscription_file "$caseO_out"; then + echo 'fb-caseO-validate:OK' +else + echo 'fb-caseO-validate:FAIL' +fi +# Exactly one hysteria2 outbound with the expected server/port/password + sni. +if jq -e '[.outbounds[] | select(.type == "hysteria2" + and .server == "e2e.example.com" + and .server_port == 8443 + and .password == "testpass" + and .tls.server_name == "e2e.example.com")] | length == 1' \ + "$caseO_out" > /dev/null 2>&1; then + echo 'fb-caseO-hy2-outbound-fields:OK' +else + echo 'fb-caseO-hy2-outbound-fields:FAIL' +fi +# Whole-chain: wrap the produced outbounds into a minimal full config and run +# the real `sing-box check`. Skipped cleanly if the binary is unavailable. +if command -v sing-box > /dev/null 2>&1; then + caseO_full="/tmp/netshift-fb-caseO-full-$$.json" + jq '{log: {level: "error"}, + inbounds: [], + outbounds: (.outbounds + [{type: "direct", tag: "direct-out"}]), + route: {}}' "$caseO_out" > "$caseO_full" 2>/dev/null + if sing-box -c "$caseO_full" check > /dev/null 2>&1; then + echo 'fb-caseO-singbox-check:OK' + else + echo 'fb-caseO-singbox-check:FAIL' + fi + rm -f "$caseO_full" +else + echo 'fb-caseO-singbox-check:SKIP' +fi +rm -f "$caseO_in" "$caseO_out" + echo 'DONE' FBEOF @@ -4817,6 +5313,7 @@ main() { test_selective_marking test_section_isolation test_monitor_fd_hygiene + test_unsupported_skip test_diagnostics test_subscription test_insecure_fetch @@ -4840,6 +5337,7 @@ main() { selmark) test_selective_marking ;; isolation) test_section_isolation ;; monfd) test_monitor_fd_hygiene ;; + unsupported) test_unsupported_skip ;; diagnostics) test_diagnostics ;; subscription) test_subscription ;; insecure) test_insecure_fetch ;; @@ -4858,7 +5356,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd unsupported diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" exit 1 ;; esac From 5b55b3e93519333a43578d0399346dd6e719cc66 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Thu, 11 Jun 2026 20:49:21 +0300 Subject: [PATCH 68/75] =?UTF-8?q?=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=B5=D1=88=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B8=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 39 +++ .../memory/luci-frontend-developer.md | 70 +++++ .../memory/shell-backend-developer.md | 61 +++++ fe-app-netshift/locales/calls.json | 70 +++-- fe-app-netshift/locales/netshift.pot | 62 +++-- fe-app-netshift/locales/netshift.ru.po | 16 +- .../src/netshift/methods/shell/index.ts | 50 ++++ .../src/netshift/services/store.service.ts | 1 + .../tabs/diagnostic/diagnostic.store.ts | 3 + .../tabs/diagnostic/initController.ts | 48 ++++ .../partials/renderAvailableActions.ts | 11 + .../resources/view/netshift/main.js | 99 +++++++- luci-app-netshift/po/ru/netshift.po | 16 +- luci-app-netshift/po/templates/netshift.pot | 62 +++-- netshift/files/usr/bin/netshift | 69 ++++- netshift/files/usr/lib/updater.sh | 9 + tests/entrypoint.sh | 240 ++++++++++++++++++ 17 files changed, 856 insertions(+), 70 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 184ce6f6..0fe00429 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -1013,3 +1013,42 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> APPROVED+gated+(033/034/035/036 hardware-verified). task-037/038 verified via smoke 155/0 + real-private.json extraction; full-config sing-box check on the real sub is covered by the shipped smoke (fb-caseO), not my hand harness. + +## task-039 + task-040: "Clear subscription cache" button in Diagnostics (2026-06-11) + +- USER: a Diagnostics-tab button that wipes ALL subscription caches and + re-downloads fresh ("частенько нужно"). OPERATOR DECISIONS: async + (component_action_async + poll, like core-switch/self-update); FULL reset + (delete all 4 per-feed files .json/.url/.rejected/.user_agent — the whole + SUBSCRIPTION_CACHE_FOLDER contents). +- KEY REUSE: there is a GENERIC async component-action mechanism already — + `component_action()` router in updater.sh (case "$component:$action"), + `component_action_async <c> <a>` (forks worker, echoes job_id), + `component_action_status <job_id>` (JSON result). Adding a new long-running + async backend op = ONE case arm + ONE worker (echo {json}; return N, NEVER exit + — the fork at updater.sh:429-433 captures one JSON line then writes finished + state from $?). NO new async framework, NO ACL change (/usr/bin/netshift exec + already granted, not per-arg). Frontend reuses the existing + pollSingBoxComponentAction helper — no new poll loop. +- task-039 (backend, APPROVED W/ COND→met, smoke 166/0): worker + subscription_clear_cache_and_redownload (bin/netshift, where subscription_update + + path builders + SUBSCRIPTION_CACHE_FOLDER are in scope) guard-deletes the cache + dir CONTENTS then runs subscription_update verbatim (redownload+restart-on-change). + RM-SAFETY (make-or-break): dual guard `[ -n "$SUBSCRIPTION_CACHE_FOLDER" ] && [ -d + ... ]` before `for f in "$DIR"/*; do [ -e ]||continue; rm -f "$f"`, never rm -rf + the dir, never a path where empty/unset constant → rm /*. Router arm + `subscription:clear_cache)` in updater.sh. Action string EXACTLY + subscription/clear_cache. +- task-040 (frontend, APPROVED): "Clear subscription cache" button in the + Diagnostics Available-actions card; NetShiftShellMethods.clearSubscriptionCache + starts component_action_async subscription clear_cache + REUSES + pollSingBoxComponentAction; handleClearSubscriptionCache mirrors handleRestart + (loading→info toast→success/error toast→finally: fetchServicesInfo + + loading:false + store.reset(['diagnosticsChecks'])); rotate-ccw icon; store slice + in services/store.service.ts + diagnostic.store.ts; 4 new i18n msgids (en+ru), + fe↔luci byte-identical. main.js rebuilt idempotent, export block unchanged + (clearSubscriptionCache is a property, no barrel leak). +- INTEGRATION VERIFIED: backend router arm ↔ frontend + ["component_action_async","subscription","clear_cache"] in built main.js match; + smoke all 166/0 (11 new cc-case all green); 472 vitest; no yarn pollution. All + UNCOMMITTED, stacked with tasks 027..038 — operator commits manually. diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 485b2cdc..44878731 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -741,3 +741,73 @@ append findings; keep under ~200 lines. diff on src = ONLY my 1 types.ts line (no churn). yarn.lock unchanged, no .yarn/.yarnrc.yml. FLAG (no browser): dropdown rendering/auto-hide not screenshotted — verified structurally. + +## task-040 — "Clear subscription cache" button in Diagnostics (async) + +- BACKEND CONTRACT (task-039, APPROVED): `component_action subscription + clear_cache` deletes all subscription caches + redownloads (restarts service + on change), driven via the EXISTING async job machinery + `component_action_async subscription clear_cache` → `{success,job_id,message}`, + poll `component_action_status <job>`. ACL already allows `/usr/bin/netshift` + exec — NO ACL change. Action strings are EXACTLY component='subscription', + action='clear_cache'. +- SHELL METHOD: added `clearSubscriptionCache()` to `methods/shell/index.ts` as + a COPY of `netshiftSelfUpdate`'s start-then-poll shape BUT with the STRICT + (non-lenient) poll callback used by `singBoxComponentAction` install path + (return `null` on empty stdout — no binary swap here, so a parse/exec failure + IS terminal). args `['component_action_async','subscription','clear_cache']`, + REUSES the component-agnostic `pollSingBoxComponentAction` (NO new poll loop). + Returns `SingBoxComponentActionResult {success,version?,message?}`. It's a + PROPERTY on `NetShiftShellMethods` → NO new top-level export symbol (the + baseclass.extend export block is byte-identical to HEAD). NO new + `AvailableMethods` enum entry needed — the existing async actions pass + `'component_action_async'`/`'component_action_status'` + the component/action + as RAW string-literal args (not enum members), so I mirrored that exactly. +- HANDLER: `handleClearSubscriptionCache` in diagnostic/initController.ts mirrors + `handleRestart`'s service-mutation idiom + globalCheck's toast idiom: set + `clearSubscriptionCache.loading=true` → `showToast(_('Clearing subscription + cache and re-downloading… this may take a minute'),'info')` → await the async + method → success→`showToast(...,'success')` else logger.error+error toast → + catch→logger.error+error toast → finally→`await fetchServicesInfo()` + + loading=false + `store.reset(['diagnosticsChecks'])`. NB: did NOT use + handleRestart's `setTimeout(...,5000)` — the async method ALREADY polls to + completion (service restart finished by the time it resolves), so refresh + immediately in finally. Wired into `renderDiagnosticAvailableActionsWidget` + (visible:true, disabled:atLeastOneServiceCommandLoading). +- BUTTON: added `clearSubscriptionCache: ActionProps` to renderAvailableActions.ts + + an `insertIf(visible,[renderButton(...)])` block using `renderRotateCcwIcon24` + (already imported for Restart — rotate/refresh fits "clear+redownload"; the + icon set has NO trash icon). Label `_('Clear subscription cache')`. No custom + classNames (neutral btn, like globalCheck/viewLogs/showSingBoxConfig). +- STORE: added `clearSubscriptionCache: { loading: boolean }` to + `diagnosticsActions` in store.service.ts type AND + `clearSubscriptionCache: { loading: false }` to initialDiagnosticStore in + diagnostic.store.ts (after showSingBoxConfig in both). +- i18n: 4 NEW msgids (PURELY additive): 'Clear subscription cache', 'Clearing + subscription cache and re-downloading… this may take a minute', 'Failed to + clear subscription cache' (used in BOTH the shell method fallback + handler → + same msgid), 'Subscription cache cleared and re-downloaded'. NB the ellipsis is + a real `…` char (U+2026), not three dots — kept literal-consistent fe↔ru. Ran + `node {extract-calls,generate-pot,generate-po ru,distribute-locales}.js` (NOT + yarn). generate-po reported 343/346 (its count metric undercounts; there were + 4 truly-new empty msgstr + the header). Filled ru in SOURCE + locales/netshift.ru.po (Очистить кеш подписок / Очистка кеша подписок и + повторная загрузка… это может занять минуту / Не удалось очистить кеш подписок + / Кеш подписок очищен и загружен заново), re-ran distribute → po/ru + + po/templates byte-identical to source (diff -q). Only header msgstr empty. +- main.js: REAL +98/-1 runtime diff (new method block + handler + button + + widget wiring). IDEMPOTENT (md5 aa89dfc… across 2 builds), banner + + `return baseclass.extend({` intact, top-level export block byte-identical to + HEAD (no barrel leak — clearSubscriptionCache is a NetShiftShellMethods + property). Confirmed action args in main.js are exactly + `["component_action_async","subscription","clear_cache"]` + poll via + `component_action_status`. +- NO new test: reused existing `pollSingBoxComponentAction` (already + table-tested); the method+handler is wiring (DOM/store untestable in node + env). vitest 472 pass unchanged. +- yarn classic 1.22.22; ran gate via node_modules/.bin (prettier --check src + clean / eslint src --ext .ts,.tsx --max-warnings=0 / vitest 472 / tsup). + yarn.lock unchanged, no .yarn/.yarnrc.yml. Working tree also carried UNRELATED + task-039 backend changes (netshift bin, updater.sh, tests/entrypoint.sh) + + .opencode/agent edits — NOT mine. FLAG (no browser): button render + toast + sequence verified by reasoning + the gate, NOT screenshotted. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index b7113bc9..2fe3f99e 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -1273,3 +1273,64 @@ findings; keep under ~200 lines. task-037's hysteria2 marks (fb-caseL/N/O) all still green. Pre-existing `rh-case1/2/6:FAIL` red marks persist (task-031 quirk; suite EXIT=0). No sacred value/port/mark/path/ACL changed. + +## task-039: `component_action subscription clear_cache` (wipe caches + redownload) + +- NEW worker `subscription_clear_cache_and_redownload` in **bin/netshift** (placed + right after `subscription_update`, where the cache-path builders + + `SUBSCRIPTION_CACHE_FOLDER` + `subscription_update` are in scope). Wired into + `updater.sh component_action()` via a new arm `subscription:clear_cache) + subscription_clear_cache_and_redownload ;;` beside sing_box:*/netshift:*. Works + via BOTH sync `component_action subscription clear_cache` AND async + `component_action_async subscription clear_cache`→job_id + + `component_action_status <job_id>` — NO new plumbing. Help line added in + show_help. NO ACL change (component_action is wholesale exec-allowed). +- **Cross-package reachability proof**: updater.sh is SOURCED by bin/netshift, and + the async fork does `"$0" component_action "$c" "$a"` (re-execs bin/netshift), so + a worker DEFINED in bin/netshift is always in scope when the `component_action` + arm (FROM updater.sh) dispatches it. A worker can live in either file as long as + it's reachable at dispatch time. +- **Guarded full-reset delete (the only new logic)**: `[ -n + "$SUBSCRIPTION_CACHE_FOLDER" ] && [ -d "$SUBSCRIPTION_CACHE_FOLDER" ]` BEFORE any + glob, then a `for cache_file in "$SUBSCRIPTION_CACHE_FOLDER"/*; do [ -e ] || continue; + rm -f ...; done` (counts removed files, logs the count at info). The two guards + make a mistyped/empty constant → `rm -f /*` IMPOSSIBLE. Only ever removes dir + CONTENTS, never `rm -rf` the dir. No error on empty/missing dir (the `[ -e ]` + continue handles the literal-glob-when-empty case). Deleting `.json` defeats the + unchanged guard, deleting `.rejected` defeats the rejected-hash veto → genuine + full re-download. +- **Reused subscription_update VERBATIM** for redownload+revalidate+restart — the + ONLY new code is the deletion + the JSON wrapper. Echo+return discipline (NEVER + `exit` — runs inside the async fork; an exit would kill it before the finished + state is written, same rule as updates_*): success → `{"success":true, + "message":"..."}` return 0; redownload fail → `{"success":false,"message":"..."}` + return $rc. No subscription sections configured → graceful + `{"success":true,"message":"No subscriptions configured;..."}` return 0 (detected + via a `config_foreach` callback identical to subscription_update's own + has_subscription probe). Action string is EXACTLY `subscription` / `clear_cache` + (frontend task-040 must match). +- TEST: extended `test_subscription` (NO registration change — already in all)) with + a `Clear Subscription Cache` driver block (11 cc-case assertions). awk-extracts + the SHIPPED worker VERBATIM + the SHIPPED `component_action` from updater.sh, + stubs `subscription_update` to a no-op (records call count + controllable rc), + table-driven `config_foreach`/`config_get` via `CC_SECTIONS` + ("sec|ct|pct" rows). Cases: 1 ≥2-feeds-all-deleted+dir-preserved+success:true+ + redownload-invoked, 2 empty-dir-graceful, 2b missing-dir-graceful, 3 no-subs→ + success+no-redownload+files-cleared, 4 redownload-fail→success:false+message, + 5 guard scoped to cache dir (unrelated sentinel survives) + empty-constant no-op, + 6 router `component_action subscription clear_cache` dispatches to the worker. +- **CAPTURE LANDMINE (bit me once, the documented `$()`-subshell variant)**: + `cc_json="$(worker)"` runs the worker in a SUBSHELL, so a stub's call-counter + (`SUB_UPDATE_CALLS`) or `ROUTER_HIT` flag mutation is TRAPPED and never reaches + the parent → the assertion reads the parent's stale reset value (false pass/fail). + When a test asserts on a side-effect VARIABLE set inside the worker, run it + WITHOUT `$()`: `worker > "$out"; json="$(cat "$out")"`. Cases that assert only on + the echoed JSON can use `$()` safely. CASE 6's `component_action()` is awk-extracted + function-only — its other arms reference undefined `updates_*` fns but those are + just `case` branches (never executed), so no need to source the whole updater.sh. +- GATES: shellcheck -S error clean (bin/netshift + updater.sh + lib/*.sh + install.sh). + `smoke-tests all` = 166 passed / 0 failed (155 baseline + 11 new cc-case, suite + total reflects them as the driver parses in the CURRENT shell `while read < + "$out"` so counts are EXACT). Pre-existing `rh-case1/2/6:FAIL` red marks persist + (task-031 piped-while quirk; suite EXIT=0). No sacred value/port/mark/path/ACL/ + frontend/async-machinery/download-guard change. diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index e1a35f01..35653bbb 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -116,7 +116,7 @@ "call": "Available actions", "key": "Available actions", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:43" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:45" ] }, { @@ -233,6 +233,20 @@ "src/validators/validateSubnet.ts:25" ] }, + { + "call": "Clear subscription cache", + "key": "Clear subscription cache", + "places": [ + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:127" + ] + }, + { + "call": "Clearing subscription cache and re-downloading… this may take a minute", + "key": "Clearing subscription cache and re-downloading… this may take a minute", + "places": [ + "src/netshift/tabs/diagnostic/initController.ts:329" + ] + }, { "call": "Close", "key": "Close", @@ -400,7 +414,7 @@ "call": "Disable autostart", "key": "Disable autostart", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:79" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:81" ] }, { @@ -583,7 +597,7 @@ "call": "Enable autostart", "key": "Enable autostart", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:89" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:91" ] }, { @@ -768,6 +782,15 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219" ] }, + { + "call": "Failed to clear subscription cache", + "key": "Failed to clear subscription cache", + "places": [ + "src/netshift/methods/shell/index.ts:254", + "src/netshift/tabs/diagnostic/initController.ts:344", + "src/netshift/tabs/diagnostic/initController.ts:348" + ] + }, { "call": "Failed to copy!", "key": "Failed to copy!", @@ -816,7 +839,7 @@ "call": "Get global check", "key": "Get global check", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:98" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:100" ] }, { @@ -1479,11 +1502,11 @@ "call": "Not running", "key": "Not running", "places": [ - "src/netshift/tabs/diagnostic/diagnostic.store.ts:56", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:64", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:72", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:80", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:88" + "src/netshift/tabs/diagnostic/diagnostic.store.ts:59", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:67", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:75", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:83", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:91" ] }, { @@ -1561,11 +1584,11 @@ "call": "Pending", "key": "Pending", "places": [ - "src/netshift/tabs/diagnostic/diagnostic.store.ts:104", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:112", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:120", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:128", - "src/netshift/tabs/diagnostic/diagnostic.store.ts:136" + "src/netshift/tabs/diagnostic/diagnostic.store.ts:107", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:115", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:123", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:131", + "src/netshift/tabs/diagnostic/diagnostic.store.ts:139" ] }, { @@ -1628,7 +1651,7 @@ "call": "Restart NetShift", "key": "Restart NetShift", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:49" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:51" ] }, { @@ -1853,7 +1876,7 @@ "call": "Self-update failed", "key": "Self-update failed", "places": [ - "src/netshift/methods/shell/index.ts:253" + "src/netshift/methods/shell/index.ts:303" ] }, { @@ -1882,7 +1905,7 @@ "key": "Show sing-box config", "places": [ "src/netshift/tabs/diagnostic/initController.ts:292", - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:116" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:118" ] }, { @@ -1996,14 +2019,14 @@ "call": "Start NetShift", "key": "Start NetShift", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:69" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:71" ] }, { "call": "Stop NetShift", "key": "Stop NetShift", "places": [ - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:59" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:61" ] }, { @@ -2014,6 +2037,13 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:57" ] }, + { + "call": "Subscription cache cleared and re-downloaded", + "key": "Subscription cache cleared and re-downloaded", + "places": [ + "src/netshift/tabs/diagnostic/initController.ts:337" + ] + }, { "call": "Subscription feeds, server filters and URLTest tuning", "key": "Subscription feeds, server filters and URLTest tuning", @@ -2411,7 +2441,7 @@ "key": "View logs", "places": [ "src/netshift/tabs/diagnostic/initController.ts:258", - "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:107" + "src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:109" ] }, { diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index 7d0153fd..f9a0a843 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 17:04+0300\n" -"PO-Revision-Date: 2026-06-07 17:04+0300\n" +"POT-Creation-Date: 2026-06-11 17:31+0300\n" +"PO-Revision-Date: 2026-06-11 17:31+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -81,7 +81,7 @@ msgstr "" msgid "Auto" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:43 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:45 msgid "Available actions" msgstr "" @@ -154,6 +154,14 @@ msgstr "" msgid "CIDR must be between 0 and 32" msgstr "" +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:127 +msgid "Clear subscription cache" +msgstr "" + +#: src/netshift/tabs/diagnostic/initController.ts:329 +msgid "Clearing subscription cache and re-downloading… this may take a minute" +msgstr "" + #: src/partials/modal/renderModal.ts:26 msgid "Close" msgstr "" @@ -248,7 +256,7 @@ msgstr "" msgid "Diagnostics" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:79 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:81 msgid "Disable autostart" msgstr "" @@ -356,7 +364,7 @@ msgstr "" msgid "Dynamic List" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:89 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:91 msgid "Enable autostart" msgstr "" @@ -464,6 +472,12 @@ msgstr "" msgid "Exclude servers by keyword" msgstr "" +#: src/netshift/methods/shell/index.ts:254 +#: src/netshift/tabs/diagnostic/initController.ts:344 +#: src/netshift/tabs/diagnostic/initController.ts:348 +msgid "Failed to clear subscription cache" +msgstr "" + #: src/helpers/copyToClipboard.ts:12 msgid "Failed to copy!" msgstr "" @@ -496,7 +510,7 @@ msgstr "" msgid "Fully Routed IPs" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:98 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:100 msgid "Get global check" msgstr "" @@ -880,11 +894,11 @@ msgstr "" msgid "Not responding" msgstr "" -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:56 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:64 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:72 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:80 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:88 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:59 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:67 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:75 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:83 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:91 msgid "Not running" msgstr "" @@ -929,11 +943,11 @@ msgstr "" msgid "Path must end with cache.db" msgstr "" -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:104 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:112 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:120 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:128 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:136 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:107 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:115 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:123 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:131 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:139 msgid "Pending" msgstr "" @@ -969,7 +983,7 @@ msgstr "" msgid "Resolve real IP for routing" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:49 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:51 msgid "Restart NetShift" msgstr "" @@ -1098,7 +1112,7 @@ msgstr "" msgid "Selector Proxy Links" msgstr "" -#: src/netshift/methods/shell/index.ts:253 +#: src/netshift/methods/shell/index.ts:303 msgid "Self-update failed" msgstr "" @@ -1115,7 +1129,7 @@ msgid "Settings" msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:292 -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:116 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:118 msgid "Show sing-box config" msgstr "" @@ -1181,11 +1195,11 @@ msgstr "" msgid "Specify the path to the list file located on the router filesystem" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:69 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:71 msgid "Start NetShift" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:59 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:61 msgid "Stop NetShift" msgstr "" @@ -1194,6 +1208,10 @@ msgstr "" msgid "Subscription" msgstr "" +#: src/netshift/tabs/diagnostic/initController.ts:337 +msgid "Subscription cache cleared and re-downloaded" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:22 msgid "Subscription feeds, server filters and URLTest tuning" msgstr "" @@ -1434,7 +1452,7 @@ msgid "Version" msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:258 -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:107 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:109 msgid "View logs" msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index 87046a7b..b7b22a20 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 20:04+0300\n" -"PO-Revision-Date: 2026-06-07 20:04+0300\n" +"POT-Creation-Date: 2026-06-11 20:31+0300\n" +"PO-Revision-Date: 2026-06-11 20:31+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -113,6 +113,12 @@ msgstr "Проверки пройдены" msgid "CIDR must be between 0 and 32" msgstr "CIDR должен быть между 0 и 32" +msgid "Clear subscription cache" +msgstr "Очистить кеш подписок" + +msgid "Clearing subscription cache and re-downloading… this may take a minute" +msgstr "Очистка кеша подписок и повторная загрузка… это может занять минуту" + msgid "Close" msgstr "Закрыть" @@ -338,6 +344,9 @@ msgstr "Исключите трафик протокола NTP из туннел msgid "Exclude servers by keyword" msgstr "Исключать серверы по ключевому слову" +msgid "Failed to clear subscription cache" +msgstr "Не удалось очистить кеш подписок" + msgid "Failed to copy!" msgstr "Не удалось скопировать!" @@ -854,6 +863,9 @@ msgstr "Остановить NetShift" msgid "Subscription" msgstr "Подписка" +msgid "Subscription cache cleared and re-downloaded" +msgstr "Кеш подписок очищен и загружен заново" + msgid "Subscription feeds, server filters and URLTest tuning" msgstr "Источники подписок, фильтры серверов и настройка URLTest" diff --git a/fe-app-netshift/src/netshift/methods/shell/index.ts b/fe-app-netshift/src/netshift/methods/shell/index.ts index e12322fd..2e0d02ce 100644 --- a/fe-app-netshift/src/netshift/methods/shell/index.ts +++ b/fe-app-netshift/src/netshift/methods/shell/index.ts @@ -220,6 +220,56 @@ export const NetShiftShellMethods = { message: response.stderr || '', }; }, + // Clear subscription cache (async) — task-039/040 contract: + // component_action_async subscription clear_cache + component_action_status + // <job>. Deletes all subscription caches then re-downloads, which restarts + // the service and can exceed the rpcd ~30s wall — so it MUST run through the + // SAME async start+poll mechanism as the sing-box core switch (reusing the + // component-agnostic `pollSingBoxComponentAction`). The component/action + // strings are EXACTLY 'subscription'/'clear_cache' (match task-039's router). + clearSubscriptionCache: async (): Promise<SingBoxComponentActionResult> => { + const startResponse = await executeShellCommand({ + command: '/usr/bin/netshift', + args: ['component_action_async', 'subscription', 'clear_cache'], + }); + + let start: ComponentActionStartResponse | null = null; + + if (startResponse.stdout) { + try { + start = JSON.parse( + startResponse.stdout, + ) as ComponentActionStartResponse; + } catch (_e) { + start = null; + } + } + + if (!start || start.success !== true || !start.job_id) { + return { + success: false, + message: + start?.message || + startResponse.stderr || + _('Failed to clear subscription cache'), + }; + } + + const jobId = start.job_id; + + return pollSingBoxComponentAction(async () => { + const statusResponse = await executeShellCommand({ + command: '/usr/bin/netshift', + args: ['component_action_status', jobId], + }); + + if (!statusResponse.stdout) { + return null; + } + + return parseComponentActionStatus(statusResponse.stdout); + }); + }, // NetShift self-update (async) — STABLE task-017 contract: // component_action_async netshift self_update + component_action_status <job>. // Reuses the component-agnostic poll. Because the package install swaps diff --git a/fe-app-netshift/src/netshift/services/store.service.ts b/fe-app-netshift/src/netshift/services/store.service.ts index d0476554..47aa65f4 100644 --- a/fe-app-netshift/src/netshift/services/store.service.ts +++ b/fe-app-netshift/src/netshift/services/store.service.ts @@ -186,6 +186,7 @@ export interface StoreType { globalCheck: { loading: boolean }; viewLogs: { loading: boolean }; showSingBoxConfig: { loading: boolean }; + clearSubscriptionCache: { loading: boolean }; }; diagnosticsSystemInfo: { loading: boolean; diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts index 6cede441..c62e3946 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/diagnostic.store.ts @@ -46,6 +46,9 @@ export const initialDiagnosticStore: Pick< showSingBoxConfig: { loading: false, }, + clearSubscriptionCache: { + loading: false, + }, }, diagnosticsRunAction: { loading: false }, diagnosticsChecks: [ diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts index 01b994ca..ef811537 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/initController.ts @@ -316,6 +316,48 @@ async function handleShowSingBoxConfig() { } } +async function handleClearSubscriptionCache() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + clearSubscriptionCache: { loading: true }, + }, + }); + + showToast( + _('Clearing subscription cache and re-downloading… this may take a minute'), + 'info', + ); + + try { + const result = await NetShiftShellMethods.clearSubscriptionCache(); + + if (result.success) { + showToast(_('Subscription cache cleared and re-downloaded'), 'success'); + } else { + logger.error( + '[DIAGNOSTIC]', + 'handleClearSubscriptionCache - result', + result, + ); + showToast(_('Failed to clear subscription cache'), 'error'); + } + } catch (e) { + logger.error('[DIAGNOSTIC]', 'handleClearSubscriptionCache - e', e); + showToast(_('Failed to clear subscription cache'), 'error'); + } finally { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + clearSubscriptionCache: { loading: false }, + }, + }); + store.reset(['diagnosticsChecks']); + } +} + function renderWikiDisclaimerWidget() { const diagnosticsChecks = store.get().diagnosticsChecks; @@ -404,6 +446,12 @@ function renderDiagnosticAvailableActionsWidget() { onClick: handleShowSingBoxConfig, disabled: atLeastOneServiceCommandLoading, }, + clearSubscriptionCache: { + loading: diagnosticsActions.clearSubscriptionCache.loading, + visible: true, + onClick: handleClearSubscriptionCache, + disabled: atLeastOneServiceCommandLoading, + }, }); return preserveScrollForPage(() => { diff --git a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts index 2e9cf66f..fc48775e 100644 --- a/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts +++ b/fe-app-netshift/src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts @@ -27,6 +27,7 @@ interface IRenderAvailableActionsProps { globalCheck: ActionProps; viewLogs: ActionProps; showSingBoxConfig: ActionProps; + clearSubscriptionCache: ActionProps; } export function renderAvailableActions({ @@ -38,6 +39,7 @@ export function renderAvailableActions({ globalCheck, viewLogs, showSingBoxConfig, + clearSubscriptionCache, }: IRenderAvailableActionsProps) { return E('div', { class: 'card pdk_diagnostic-page__right-bar__actions' }, [ E('b', {}, _('Available actions')), @@ -118,5 +120,14 @@ export function renderAvailableActions({ disabled: showSingBoxConfig.disabled, }), ]), + ...insertIf(clearSubscriptionCache.visible, [ + renderButton({ + onClick: clearSubscriptionCache.onClick, + icon: renderRotateCcwIcon24, + text: _('Clear subscription cache'), + loading: clearSubscriptionCache.loading, + disabled: clearSubscriptionCache.disabled, + }), + ]), ]); } diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js index 4a046044..e1c9b687 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/main.js @@ -1056,6 +1056,46 @@ var NetShiftShellMethods = { message: response.stderr || "" }; }, + // Clear subscription cache (async) — task-039/040 contract: + // component_action_async subscription clear_cache + component_action_status + // <job>. Deletes all subscription caches then re-downloads, which restarts + // the service and can exceed the rpcd ~30s wall — so it MUST run through the + // SAME async start+poll mechanism as the sing-box core switch (reusing the + // component-agnostic `pollSingBoxComponentAction`). The component/action + // strings are EXACTLY 'subscription'/'clear_cache' (match task-039's router). + clearSubscriptionCache: async () => { + const startResponse = await executeShellCommand({ + command: "/usr/bin/netshift", + args: ["component_action_async", "subscription", "clear_cache"] + }); + let start = null; + if (startResponse.stdout) { + try { + start = JSON.parse( + startResponse.stdout + ); + } catch (_e) { + start = null; + } + } + if (!start || start.success !== true || !start.job_id) { + return { + success: false, + message: start?.message || startResponse.stderr || _("Failed to clear subscription cache") + }; + } + const jobId = start.job_id; + return pollSingBoxComponentAction(async () => { + const statusResponse = await executeShellCommand({ + command: "/usr/bin/netshift", + args: ["component_action_status", jobId] + }); + if (!statusResponse.stdout) { + return null; + } + return parseComponentActionStatus(statusResponse.stdout); + }); + }, // NetShift self-update (async) — STABLE task-017 contract: // component_action_async netshift self_update + component_action_status <job>. // Reuses the component-agnostic poll. Because the package install swaps @@ -1657,6 +1697,9 @@ var initialDiagnosticStore = { }, showSingBoxConfig: { loading: false + }, + clearSubscriptionCache: { + loading: false } }, diagnosticsRunAction: { loading: false }, @@ -3996,7 +4039,8 @@ function renderAvailableActions({ disable, globalCheck, viewLogs, - showSingBoxConfig + showSingBoxConfig, + clearSubscriptionCache }) { return E("div", { class: "card pdk_diagnostic-page__right-bar__actions" }, [ E("b", {}, _("Available actions")), @@ -4076,6 +4120,15 @@ function renderAvailableActions({ loading: showSingBoxConfig.loading, disabled: showSingBoxConfig.disabled }) + ]), + ...insertIf(clearSubscriptionCache.visible, [ + renderButton({ + onClick: clearSubscriptionCache.onClick, + icon: renderRotateCcwIcon24, + text: _("Clear subscription cache"), + loading: clearSubscriptionCache.loading, + disabled: clearSubscriptionCache.disabled + }) ]) ]); } @@ -4738,6 +4791,44 @@ async function handleShowSingBoxConfig() { }); } } +async function handleClearSubscriptionCache() { + const diagnosticsActions = store.get().diagnosticsActions; + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + clearSubscriptionCache: { loading: true } + } + }); + showToast( + _("Clearing subscription cache and re-downloading\u2026 this may take a minute"), + "info" + ); + try { + const result = await NetShiftShellMethods.clearSubscriptionCache(); + if (result.success) { + showToast(_("Subscription cache cleared and re-downloaded"), "success"); + } else { + logger.error( + "[DIAGNOSTIC]", + "handleClearSubscriptionCache - result", + result + ); + showToast(_("Failed to clear subscription cache"), "error"); + } + } catch (e) { + logger.error("[DIAGNOSTIC]", "handleClearSubscriptionCache - e", e); + showToast(_("Failed to clear subscription cache"), "error"); + } finally { + await fetchServicesInfo(); + store.set({ + diagnosticsActions: { + ...diagnosticsActions, + clearSubscriptionCache: { loading: false } + } + }); + store.reset(["diagnosticsChecks"]); + } +} function renderWikiDisclaimerWidget() { const diagnosticsChecks = store.get().diagnosticsChecks; function getWikiKind() { @@ -4811,6 +4902,12 @@ function renderDiagnosticAvailableActionsWidget() { visible: true, onClick: handleShowSingBoxConfig, disabled: atLeastOneServiceCommandLoading + }, + clearSubscriptionCache: { + loading: diagnosticsActions.clearSubscriptionCache.loading, + visible: true, + onClick: handleClearSubscriptionCache, + disabled: atLeastOneServiceCommandLoading } }); return preserveScrollForPage(() => { diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index 87046a7b..b7b22a20 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 20:04+0300\n" -"PO-Revision-Date: 2026-06-07 20:04+0300\n" +"POT-Creation-Date: 2026-06-11 20:31+0300\n" +"PO-Revision-Date: 2026-06-11 20:31+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -113,6 +113,12 @@ msgstr "Проверки пройдены" msgid "CIDR must be between 0 and 32" msgstr "CIDR должен быть между 0 и 32" +msgid "Clear subscription cache" +msgstr "Очистить кеш подписок" + +msgid "Clearing subscription cache and re-downloading… this may take a minute" +msgstr "Очистка кеша подписок и повторная загрузка… это может занять минуту" + msgid "Close" msgstr "Закрыть" @@ -338,6 +344,9 @@ msgstr "Исключите трафик протокола NTP из туннел msgid "Exclude servers by keyword" msgstr "Исключать серверы по ключевому слову" +msgid "Failed to clear subscription cache" +msgstr "Не удалось очистить кеш подписок" + msgid "Failed to copy!" msgstr "Не удалось скопировать!" @@ -854,6 +863,9 @@ msgstr "Остановить NetShift" msgid "Subscription" msgstr "Подписка" +msgid "Subscription cache cleared and re-downloaded" +msgstr "Кеш подписок очищен и загружен заново" + msgid "Subscription feeds, server filters and URLTest tuning" msgstr "Источники подписок, фильтры серверов и настройка URLTest" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index 7d0153fd..f9a0a843 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-07 17:04+0300\n" -"PO-Revision-Date: 2026-06-07 17:04+0300\n" +"POT-Creation-Date: 2026-06-11 17:31+0300\n" +"PO-Revision-Date: 2026-06-11 17:31+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -81,7 +81,7 @@ msgstr "" msgid "Auto" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:43 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:45 msgid "Available actions" msgstr "" @@ -154,6 +154,14 @@ msgstr "" msgid "CIDR must be between 0 and 32" msgstr "" +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:127 +msgid "Clear subscription cache" +msgstr "" + +#: src/netshift/tabs/diagnostic/initController.ts:329 +msgid "Clearing subscription cache and re-downloading… this may take a minute" +msgstr "" + #: src/partials/modal/renderModal.ts:26 msgid "Close" msgstr "" @@ -248,7 +256,7 @@ msgstr "" msgid "Diagnostics" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:79 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:81 msgid "Disable autostart" msgstr "" @@ -356,7 +364,7 @@ msgstr "" msgid "Dynamic List" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:89 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:91 msgid "Enable autostart" msgstr "" @@ -464,6 +472,12 @@ msgstr "" msgid "Exclude servers by keyword" msgstr "" +#: src/netshift/methods/shell/index.ts:254 +#: src/netshift/tabs/diagnostic/initController.ts:344 +#: src/netshift/tabs/diagnostic/initController.ts:348 +msgid "Failed to clear subscription cache" +msgstr "" + #: src/helpers/copyToClipboard.ts:12 msgid "Failed to copy!" msgstr "" @@ -496,7 +510,7 @@ msgstr "" msgid "Fully Routed IPs" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:98 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:100 msgid "Get global check" msgstr "" @@ -880,11 +894,11 @@ msgstr "" msgid "Not responding" msgstr "" -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:56 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:64 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:72 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:80 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:88 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:59 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:67 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:75 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:83 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:91 msgid "Not running" msgstr "" @@ -929,11 +943,11 @@ msgstr "" msgid "Path must end with cache.db" msgstr "" -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:104 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:112 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:120 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:128 -#: src/netshift/tabs/diagnostic/diagnostic.store.ts:136 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:107 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:115 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:123 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:131 +#: src/netshift/tabs/diagnostic/diagnostic.store.ts:139 msgid "Pending" msgstr "" @@ -969,7 +983,7 @@ msgstr "" msgid "Resolve real IP for routing" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:49 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:51 msgid "Restart NetShift" msgstr "" @@ -1098,7 +1112,7 @@ msgstr "" msgid "Selector Proxy Links" msgstr "" -#: src/netshift/methods/shell/index.ts:253 +#: src/netshift/methods/shell/index.ts:303 msgid "Self-update failed" msgstr "" @@ -1115,7 +1129,7 @@ msgid "Settings" msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:292 -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:116 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:118 msgid "Show sing-box config" msgstr "" @@ -1181,11 +1195,11 @@ msgstr "" msgid "Specify the path to the list file located on the router filesystem" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:69 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:71 msgid "Start NetShift" msgstr "" -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:59 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:61 msgid "Stop NetShift" msgstr "" @@ -1194,6 +1208,10 @@ msgstr "" msgid "Subscription" msgstr "" +#: src/netshift/tabs/diagnostic/initController.ts:337 +msgid "Subscription cache cleared and re-downloaded" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:22 msgid "Subscription feeds, server filters and URLTest tuning" msgstr "" @@ -1434,7 +1452,7 @@ msgid "Version" msgstr "" #: src/netshift/tabs/diagnostic/initController.ts:258 -#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:107 +#: src/netshift/tabs/diagnostic/partials/renderAvailableActions.ts:109 msgid "View logs" msgstr "" diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index f291efbb..0a488a1d 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -1976,6 +1976,72 @@ subscription_update() { fi } +# Worker for `component_action subscription clear_cache` (updater.sh router). +# Wipes ALL per-feed subscription cache files under SUBSCRIPTION_CACHE_FOLDER +# (the four "${section}.<md5(url)>.{json,url,rejected,user_agent}" sidecars per +# feed), then re-runs subscription_update verbatim so every feed is re-downloaded +# fresh, re-validated, and the service restarts on change. Deleting the .json +# defeats the unchanged guard and deleting the .rejected defeats the rejected-hash +# veto, so the redownload is a genuine full reset. +# +# CRITICAL: this runs inside the component_action async fork (component_action_async +# → "$0" component_action subscription clear_cache). It MUST echo a single JSON +# result and `return N` — NEVER `exit` (an exit would kill the fork before the +# finished-state file is written). Mirrors the updates_* workers' echo+return +# discipline. +# +# SAFETY: the globbed delete is guarded so a mistyped/empty SUBSCRIPTION_CACHE_FOLDER +# can never become `rm -f /*`. We require a non-empty constant AND an existing +# directory before `rm -f "$SUBSCRIPTION_CACHE_FOLDER"/*`, and only ever remove the +# directory CONTENTS — never `rm -rf` the directory itself. +subscription_clear_cache_and_redownload() { + local has_subscription removed cache_file update_rc + + # Detect whether any subscription section is configured at all. If none, + # there is nothing to clear or redownload — succeed gracefully. + has_subscription=0 + _detect_subscription_section_for_clear() { + local section="$1" + local connection_type proxy_config_type + + config_get connection_type "$section" "connection_type" + [ "$connection_type" = "proxy" ] || return 0 + + config_get proxy_config_type "$section" "proxy_config_type" + [ "$proxy_config_type" = "subscription" ] && has_subscription=1 + } + config_foreach _detect_subscription_section_for_clear "section" + + # Guarded full-reset delete of the cache directory CONTENTS. The two guards + # (non-empty constant AND existing directory) make a wild `rm -f /*` + # impossible. No error when the directory is empty or missing. + removed=0 + if [ -n "$SUBSCRIPTION_CACHE_FOLDER" ] && [ -d "$SUBSCRIPTION_CACHE_FOLDER" ]; then + for cache_file in "$SUBSCRIPTION_CACHE_FOLDER"/*; do + [ -e "$cache_file" ] || continue + rm -f "$cache_file" 2>/dev/null && removed=$((removed + 1)) + done + fi + log "Cleared subscription cache: removed $removed file(s) from '$SUBSCRIPTION_CACHE_FOLDER'" "info" + + if [ "$has_subscription" -eq 0 ]; then + echo "{\"success\":true,\"message\":\"No subscriptions configured; cleared $removed cache file(s), nothing to redownload\"}" + return 0 + fi + + # Reuse subscription_update ENTIRELY for the redownload + re-validate + restart. + subscription_update + update_rc=$? + + if [ "$update_rc" -eq 0 ]; then + echo "{\"success\":true,\"message\":\"Cleared $removed subscription cache file(s) and re-downloaded all feeds\"}" + return 0 + fi + + echo "{\"success\":false,\"message\":\"Cleared $removed subscription cache file(s) but the redownload failed (rc=$update_rc); kept the last working cache\"}" + return "$update_rc" +} + # sing-box funcs sing_box_configure_service() { local sing_box_enabled sing_box_user sing_box_config_path sing_box_conffile @@ -4900,7 +4966,8 @@ Available commands: check_dns_available Check DNS server availability global_check Run global system check component_action Run component action: <component> <action> - (e.g. sing_box install_extended|install_stable|check_update) + (e.g. sing_box install_extended|install_stable|check_update, + subscription clear_cache to wipe all caches + redownload) component_action_async Start component_action in background; echoes a job_id (use with component_action_status to poll the outcome) component_action_status Report an async component action by job_id: <job_id> diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 2718de3a..4e5012e5 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -1795,6 +1795,15 @@ component_action() { netshift:self_update) updates_self_update_netshift ;; + subscription:clear_cache) + # Worker lives in bin/netshift (where subscription_update + the cache-path + # builders + SUBSCRIPTION_CACHE_FOLDER are in scope). updater.sh is sourced + # by bin/netshift, and the async fork re-execs "$0" component_action ..., + # so the function is always defined when this arm dispatches. Reachable via + # BOTH the sync `component_action subscription clear_cache` and the async + # component_action_async/component_action_status paths. + subscription_clear_cache_and_redownload + ;; *) echo '{"success":false,"message":"Unknown component action"}' return 1 diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 0086bdf0..4a51e5f7 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -3296,6 +3296,246 @@ MUEOF done rm -f "$mu" + + # ── Clear-subscription-cache worker (task-039) ─────────────────── + # Exercises subscription_clear_cache_and_redownload (bin/netshift) which + # backs `component_action subscription clear_cache`. The worker is + # awk-extracted VERBATIM from the shipped bin; subscription_update is STUBBED + # to a no-op so the test is hermetic (no network/restart). The driver is + # parsed in the CURRENT shell (`while read < "$out"`, NO pipe) so the + # assertions get EXACT state — and we verify both the guarded deletion and + # the JSON shape the async status layer consumes. + printf "\n ${BOLD}Clear Subscription Cache${NC}\n" + + local ccbin="${NETSHIFT_SRC}/usr/bin/netshift" + local ccupd="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$ccbin" ] || [ ! -r "$ccupd" ]; then + skip "clear-cache worker (bin / updater.sh not found)" + return + fi + + local cc="/tmp/netshift-sub-clearcache-$$.sh" + cat > "$cc" << 'CCEOF' +# Isolated synthetic cache dir — NEVER the real /etc/netshift/subscriptions. +SUBSCRIPTION_CACHE_FOLDER="/tmp/netshift-cc-cache-$$" + +# Quiet logger; record subscription_update invocation count + control its rc. +SUB_UPDATE_CALLS=0 +SUB_UPDATE_RC=0 +log() { :; } +echolog() { :; } +nolog() { :; } +# Hermetic no-op stub for the redownload+restart path (verbatim reuse is what +# the production worker does; here we only assert the worker CALLS it). +subscription_update() { SUB_UPDATE_CALLS=$((SUB_UPDATE_CALLS + 1)); return "$SUB_UPDATE_RC"; } + +# config_foreach / config_get stubs driven by the CC_SECTIONS table: +# CC_SECTIONS = newline list of "<section>|<connection_type>|<proxy_config_type>" +config_foreach() { + # $1=callback $2=type ; iterate sections in the CURRENT shell (no pipe) so + # the callback can mutate accumulator globals like has_subscription. + _cf_tmp="/tmp/netshift-cc-cf-$$" + printf '%s\n' "$CC_SECTIONS" > "$_cf_tmp" + while IFS= read -r _row || [ -n "$_row" ]; do + [ -n "$_row" ] || continue + CC_CUR_SECTION="${_row%%|*}" + _rest="${_row#*|}" + CC_CUR_CT="${_rest%%|*}" + CC_CUR_PCT="${_rest##*|}" + "$1" "$CC_CUR_SECTION" + done < "$_cf_tmp" + rm -f "$_cf_tmp" +} +config_get() { + # $1=varname $2=section $3=option [default] + case "$3" in + connection_type) eval "$1=\"\$CC_CUR_CT\"" ;; + proxy_config_type) eval "$1=\"\$CC_CUR_PCT\"" ;; + *) eval "$1=\"\${4:-}\"" ;; + esac +} + +# Extract the worker VERBATIM from the shipped bin (column-0 opener → column-0 '}'). +eval "$(awk '/^subscription_clear_cache_and_redownload\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "BIN_PATH")" + +# Seed helper: write the four per-feed sidecars for a synthetic (section,hash). +seed_feed() { + _s="$1"; _h="$2" + printf 'json' > "$SUBSCRIPTION_CACHE_FOLDER/${_s}.${_h}.json" + printf 'url' > "$SUBSCRIPTION_CACHE_FOLDER/${_s}.${_h}.url" + printf 'rej' > "$SUBSCRIPTION_CACHE_FOLDER/${_s}.${_h}.rejected" + printf 'ua' > "$SUBSCRIPTION_CACHE_FOLDER/${_s}.${_h}.user_agent" +} + +# ── CASE 1: ≥2 feeds seeded, sections configured → all files deleted, dir +# preserved, subscription_update called, JSON success:true ─────── +rm -rf "$SUBSCRIPTION_CACHE_FOLDER" +mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" +seed_feed "sec1" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +seed_feed "sec1" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +seed_feed "sec2" "cccccccccccccccccccccccccccccccc" +before_count=$(ls -1 "$SUBSCRIPTION_CACHE_FOLDER" 2>/dev/null | wc -l) +CC_SECTIONS="sec1|proxy|subscription +sec2|proxy|subscription" +SUB_UPDATE_CALLS=0 +SUB_UPDATE_RC=0 +# Run WITHOUT $()-capture so SUB_UPDATE_CALLS (set by the stub) survives — a +# $() subshell would trap the mutation (the documented capture landmine). +cc1_out="/tmp/netshift-cc-json1-$$" +subscription_clear_cache_and_redownload > "$cc1_out" +cc1_rc=$? +cc1_json="$(cat "$cc1_out")" +rm -f "$cc1_out" +after_count=$(ls -1 "$SUBSCRIPTION_CACHE_FOLDER" 2>/dev/null | wc -l) +if [ "$before_count" -ge 8 ] && [ "$after_count" -eq 0 ]; then + echo "cc-case1-all-deleted(before=$before_count after=$after_count):OK" +else + echo "cc-case1-all-deleted(before=$before_count after=$after_count):FAIL" +fi +if [ -d "$SUBSCRIPTION_CACHE_FOLDER" ]; then + echo 'cc-case1-dir-preserved:OK' +else + echo 'cc-case1-dir-preserved:FAIL' +fi +if printf '%s' "$cc1_json" | jq -e '.success == true' >/dev/null 2>&1; then + echo 'cc-case1-json-success-true:OK' +else + echo "cc-case1-json-success-true(got '$cc1_json'):FAIL" +fi +if [ "$SUB_UPDATE_CALLS" -eq 1 ] && [ "$cc1_rc" -eq 0 ]; then + echo 'cc-case1-redownload-invoked:OK' +else + echo "cc-case1-redownload-invoked(calls=$SUB_UPDATE_CALLS rc=$cc1_rc):FAIL" +fi + +# ── CASE 2: empty cache dir → graceful success:true ────────────────── +rm -rf "$SUBSCRIPTION_CACHE_FOLDER" +mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" +CC_SECTIONS="sec1|proxy|subscription" +SUB_UPDATE_CALLS=0 +cc2_json="$(subscription_clear_cache_and_redownload)" +if printf '%s' "$cc2_json" | jq -e '.success == true' >/dev/null 2>&1; then + echo 'cc-case2-empty-dir-success:OK' +else + echo "cc-case2-empty-dir-success(got '$cc2_json'):FAIL" +fi + +# ── CASE 2b: missing cache dir → graceful success:true, no error ───── +rm -rf "$SUBSCRIPTION_CACHE_FOLDER" +CC_SECTIONS="sec1|proxy|subscription" +cc2b_json="$(subscription_clear_cache_and_redownload 2>/dev/null)" +if printf '%s' "$cc2b_json" | jq -e '.success == true' >/dev/null 2>&1; then + echo 'cc-case2b-missing-dir-success:OK' +else + echo "cc-case2b-missing-dir-success(got '$cc2b_json'):FAIL" +fi +mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" + +# ── CASE 3: no subscription sections → graceful success:true, no redownload ─ +rm -rf "$SUBSCRIPTION_CACHE_FOLDER" +mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" +seed_feed "sec1" "dddddddddddddddddddddddddddddddd" +CC_SECTIONS="sec1|proxy|url" +SUB_UPDATE_CALLS=0 +cc3_out="/tmp/netshift-cc-json3-$$" +subscription_clear_cache_and_redownload > "$cc3_out" +cc3_json="$(cat "$cc3_out")" +rm -f "$cc3_out" +cc3_after=$(ls -1 "$SUBSCRIPTION_CACHE_FOLDER" 2>/dev/null | wc -l) +if printf '%s' "$cc3_json" | jq -e '.success == true' >/dev/null 2>&1 \ + && [ "$SUB_UPDATE_CALLS" -eq 0 ] && [ "$cc3_after" -eq 0 ]; then + echo 'cc-case3-no-subs-graceful:OK' +else + echo "cc-case3-no-subs-graceful(calls=$SUB_UPDATE_CALLS after=$cc3_after json='$cc3_json'):FAIL" +fi + +# ── CASE 4: redownload failure → success:false, message surfaced ───── +rm -rf "$SUBSCRIPTION_CACHE_FOLDER" +mkdir -p "$SUBSCRIPTION_CACHE_FOLDER" +seed_feed "sec1" "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +CC_SECTIONS="sec1|proxy|subscription" +SUB_UPDATE_RC=1 +cc4_json="$(subscription_clear_cache_and_redownload)" +SUB_UPDATE_RC=0 +if printf '%s' "$cc4_json" | jq -e '.success == false and (.message | length > 0)' >/dev/null 2>&1; then + echo 'cc-case4-redownload-fail-surfaced:OK' +else + echo "cc-case4-redownload-fail-surfaced(got '$cc4_json'):FAIL" +fi + +# ── CASE 5: guarded delete — empty constant can NEVER `rm -f /*` ───── +# Structural proof: the worker only deletes when the constant is non-empty AND +# the dir exists. Point the constant at a guarded sentinel tree and confirm an +# UNRELATED file outside SUBSCRIPTION_CACHE_FOLDER survives, and that an empty +# constant is a no-op (guard short-circuits before any glob). +guard_root="/tmp/netshift-cc-guard-$$" +rm -rf "$guard_root" +mkdir -p "$guard_root/sub" "$guard_root/other" +printf 'keep' > "$guard_root/other/sentinel" +printf 'wipe' > "$guard_root/sub/feed.json" +SUBSCRIPTION_CACHE_FOLDER="$guard_root/sub" +CC_SECTIONS="sec1|proxy|subscription" +subscription_clear_cache_and_redownload >/dev/null 2>&1 +if [ -f "$guard_root/other/sentinel" ] && [ ! -f "$guard_root/sub/feed.json" ] \ + && [ -d "$guard_root/sub" ]; then + echo 'cc-case5-guard-scoped-to-cache-dir:OK' +else + echo 'cc-case5-guard-scoped-to-cache-dir:FAIL' +fi +# Empty constant → guard short-circuits, sentinel still alive, no error. +SUBSCRIPTION_CACHE_FOLDER="" +CC_SECTIONS="sec1|proxy|subscription" +subscription_clear_cache_and_redownload >/dev/null 2>&1 +if [ -f "$guard_root/other/sentinel" ]; then + echo 'cc-case5-empty-constant-noop:OK' +else + echo 'cc-case5-empty-constant-noop:FAIL' +fi +rm -rf "$guard_root" + +# ── CASE 6: router dispatch — `component_action subscription clear_cache` +# reaches the worker (also the path the async fork uses) ────────── +# Source the SHIPPED updater.sh component_action(); the worker is already defined +# above, so the arm must dispatch to it. Re-point the cache dir + a fresh stub +# that records the call so we prove the arm reached our worker. +ROUTER_HIT=0 +subscription_clear_cache_and_redownload() { + ROUTER_HIT=1 + echo '{"success":true,"message":"router-hit"}' + return 0 +} +# Silence updater.sh's own logger if it defines one after sourcing. +eval "$(awk '/^component_action\(\) \{/{p=1} p{print} p&&/^\}/{exit}' "UPD_PATH")" +# No $()-capture (would subshell-trap ROUTER_HIT); write JSON to a file. +router_out="/tmp/netshift-cc-router-$$" +component_action subscription clear_cache > "$router_out" +router_json="$(cat "$router_out")" +rm -f "$router_out" +if [ "$ROUTER_HIT" -eq 1 ] && printf '%s' "$router_json" | jq -e '.success == true' >/dev/null 2>&1; then + echo 'cc-case6-router-dispatch:OK' +else + echo "cc-case6-router-dispatch(hit=$ROUTER_HIT json='$router_json'):FAIL" +fi + +rm -rf "/tmp/netshift-cc-cache-$$" +echo 'DONE' +CCEOF + + sed -i "s|BIN_PATH|$ccbin|g; s|UPD_PATH|$ccupd|g" "$cc" + + local cc_out="/tmp/netshift-cc-out-$$" + sh "$cc" > "$cc_out" 2>/dev/null + while IFS= read -r line; do + case "$line" in + *:OK) pass "$line" ;; + *:FAIL) fail "$line" ;; + *:SKIP) skip "$line" ;; + DONE) ;; + *) ;; + esac + done < "$cc_out" + + rm -f "$cc" "$cc_out" } # ───────────────────────────────────────────────────────────────── From a062f41a2eb6594647b00aaaaadb5461ad2ec4ed Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Thu, 11 Jun 2026 21:57:57 +0300 Subject: [PATCH 69/75] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=B0=D0=BF?= =?UTF-8?q?=D0=B4=D0=B5=D0=B9=D1=82=D0=B0=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B0=20ipk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 52 ++++++++++ docs/agent-rules/memory/code-reviewer.md | 3 + .../memory/packaging-ci-engineer.md | 9 ++ .../memory/shell-backend-developer.md | 57 +++++++++++ install.sh | 2 +- netshift/files/usr/lib/updater.sh | 62 +++++++++++- tests/entrypoint.sh | 94 +++++++++++++++++-- 7 files changed, 270 insertions(+), 9 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 0fe00429..d6a8b69d 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -1052,3 +1052,55 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> ["component_action_async","subscription","clear_cache"] in built main.js match; smoke all 166/0 (11 new cc-case all green); 472 vitest; no yarn pollution. All UNCOMMITTED, stacked with tasks 027..038 — operator commits manually. + +## DIAGNOSED: self-update silently no-ops on opkg/ipk routers with a v-prefixed installed build (2026-06-11) + +- USER (main router ssh root@192.168.1.1, OWRT 24.10.5 mediatek/filogic aarch64, + OPKG/ipk, Xiaomi AX3000T): NetShift self-update from the web UI reports + "updated to 0.8.7" but the installed version stays v0.8.6. Logs show the whole + self_update worker succeeding ("installing netshift-0.8.7-r1-all.ipk" ... + "NetShift updated to 0.8.7"). +- ROOT CAUSE (empirically proven on the router): the installed build is the OLD + ipk that carried a leading `v` (constants `NETSHIFT_VERSION="v0.8.6"`, opkg + pkg version `v0.8.6-r1`). The new release is `0.8.7-r1` (no v, post task-028). + `opkg install <file.ipk>` REFUSES to "downgrade": it prints + `Not downgrading package ... from v0.8.6-r1 to 0.8.7-r1.` and RETURNS rc=0 + (NOT an error for opkg). So updates_pkg_install_file (updater.sh:1410, + `opkg install "$f" >/dev/null 2>&1`) sees rc=0 -> the worker logs success and + emits {"success":true,...} while NOTHING was installed. +- WHY opkg thinks it's a downgrade: `opkg compare-versions "v0.8.6-r1" ">>" + "0.8.7-r1"` => rc=0 (TRUE). The leading `v` sorts ABOVE the digit, so + v0.8.6 > 0.8.7 in opkg's dpkg-style compare. Without the v, + `0.8.6-r1 << 0.8.7-r1` => true (correct). This is the packaging.md §3 / task-028 + fragility realized: routers still running a pre-028 v-build can never self-update + to a no-v release. +- TWO distinct bugs to fix in updater.sh: + 1. PRIMARY (silent success): `updates_pkg_install_file` swallows opkg's + "Not downgrading" no-op as rc=0. The install path never VERIFIES the + post-install version actually changed. FIX directions: + (a) opkg path add `--force-downgrade` (and/or `--force-reinstall`) so a + v->no-v transition actually installs; AND/OR + (b) post-install VERIFY: after install, re-read the installed pkg version + and compare to the target; if unchanged, treat as failure (honest JSON + {"success":false}) instead of reporting success. Verify-after-install is + the robust belt — opkg "already installed"/"not downgrading"/"up to date" + all return rc=0, so rc alone is NOT a reliable success signal on opkg. + 2. CONTRIBUTING: the v-prefix legacy build. The compare in + _updates_self_update_netshift_core ALREADY v-strips both sides + (`${installed#v}` = `${latest#v}`, :1679) so it correctly decides "need + update"; the failure is purely at the opkg install step. +- MANUAL FIX applied on the router (recovery, verified): downloaded all 3 ipks + from the latest release and `opkg install --force-downgrade <file>` each -> + netshift + luci-app-netshift now 0.8.7-r1, constants NETSHIFT_VERSION="0.8.7" + (no v), get_system_info netshift_version 0.8.7. Future 0.8.7->0.8.8 upgrades + will work normally (both sides no-v). Note: install drops + /etc/config/netshift-opkg conffile-diff artifact (harmless; rm'd). +- apk SIDE NOTE (not the user's box but same helper): apk uses `-r` for release, + treats a dashed UPSTREAM version specially (task-034 landmine #A: equal-version + no-overwrite). The same verify-after-install belt would harden apk too. Whatever + fix is chosen must be tested on BOTH opkg and apk paths (packaging gate). +- LANDMINE for the fix: `updates_pkg_install_file` redirects stdout+stderr to + /dev/null, so the "Not downgrading" message is invisible — never rely on opkg + text; rely on rc PLUS an explicit post-install version re-check. opkg + compare-versions is available on-device for a robust numeric compare if needed + (but the installer should not need the leading v at all once verify is added). diff --git a/docs/agent-rules/memory/code-reviewer.md b/docs/agent-rules/memory/code-reviewer.md index cee972eb..b3474865 100644 --- a/docs/agent-rules/memory/code-reviewer.md +++ b/docs/agent-rules/memory/code-reviewer.md @@ -64,3 +64,6 @@ append recurring findings; keep under ~200 lines. - Frontend barrel exposure: anything added to src/helpers/index.ts (or any export* barrel reaching main.ts) AND actually used appears in the generated main.js baseclass.extend block as a main.* symbol; unused re-exports get tree-shaken. So internal-only helper + added to barrel + used = it WILL leak to main.*. To keep a helper truly internal, place it in the consuming module, not the barrel. - OpenWrt jq ascii_downcase only folds ASCII A-Z; case-insensitive matching on Cyrillic/Unicode names needs an inline codepoint fold (explode/map/implode: ASCII 65-90 +32, Cyrillic 1040-1071 +32, Yo 1025->1105). When reviewing such a fold: (a) already-lowercase ranges excluded (no double-fold), (b) def before first use when the program does NOT import helpers.jq, (c) a pure-emoji-keyword exact-match test proves non-folded codepoints pass through unchanged on both sides. (task-010) + +- Package-manager rc is NOT a reliable success signal on opkg: rc=0 for "Not downgrading"/"already installed"/"up to date". A self-update/install that trusts only rc silently no-ops (the v→no-v rename trap: legacy `v0.8.6` sorts ABOVE `0.8.7` in opkg's compare, so `opkg install` refuses the "downgrade" and returns 0). When reviewing a package-install path, require: (a) `--force-downgrade --force-reinstall` on the opkg branch (apk overwrites by default); AND (b) verify-after-install — RE-READ the installed version (opkg `list-installed | grep "^pkg "`, apk `list --installed`; grep/awk only, NO Oniguruma jq) and compare v-stripped semver (`${x#v}`, `${x%%-*}`) with `==` OR `is_min_package_version installed target`; empty-installed must fail-safe to success:false. Keep install.sh `pkg_install` and updater.sh `updates_pkg_install_file` opkg branches ALIGNED. (task-041/042) +- Async self-update worker landmine: the `_*_core` worker MUST `return 1` (NEVER `exit`) on failure so the public wrapper's always-run `updates_restore_after_swap` epilogue + finished-job-state write still execute. Verify the wrapper captures core rc/JSON to a temp file then unconditionally restores. Smoke assertions for these must be in the MAIN shell body (direct `if…pass/fail`), never inside `cmd | while read` (subshell swallows PASS/FAIL — harness-wide landmine). (task-041) diff --git a/docs/agent-rules/memory/packaging-ci-engineer.md b/docs/agent-rules/memory/packaging-ci-engineer.md index 60b2e18e..1953962e 100644 --- a/docs/agent-rules/memory/packaging-ci-engineer.md +++ b/docs/agent-rules/memory/packaging-ci-engineer.md @@ -105,3 +105,12 @@ artifacts out of the container -> **ipk underscore->dash rename** unsupported; needs >=15 MB on `/overlay`; NO uninstall path (removal lives in package `prerm`). GitHub API rate-limit is a known fragility (wget path has no guard). +- `pkg_install` opkg branch uses `opkg install --force-downgrade + --force-reinstall "$pkg_file"` (task-042). Plain `opkg install` silently + no-op'd (rc=0) when re-run on a router with an older build: the legacy + v-prefixed version (`v0.8.6-r1`) sorts ABOVE the no-v release (`0.8.7-r1`) so + opkg "won't downgrade", and equal versions report "up to date". Both force + flags make opkg remove+reinstall (proven on OWRT 24.10.5 aarch64). apk branch + unchanged — `apk add --allow-untrusted` overwrites by default. This is the + install.sh twin of the task-041 `updates_pkg_install_file` updater.sh fix; + keep both upgrade paths (README script + in-app self-update) aligned. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 2fe3f99e..fbf293d2 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -1334,3 +1334,60 @@ findings; keep under ~200 lines. "$out"` so counts are EXACT). Pre-existing `rh-case1/2/6:FAIL` red marks persist (task-031 piped-while quirk; suite EXIT=0). No sacred value/port/mark/path/ACL/ frontend/async-machinery/download-guard change. + +## task-041: self-update verify-after-install (opkg silent no-op fix) + +- **Root cause (proven on router):** opkg returns rc=0 for "Not downgrading"/ + "already installed"/"up to date". A legacy v-prefixed build (`v0.8.6-r1`) sorts + ABOVE the no-v target (`0.8.7-r1`) in opkg's dpkg-style compare, so a plain + `opkg install` refuses AND returns rc=0. The self-update worker trusted rc and + emitted `{"success":true}` while NOTHING installed. **opkg install rc is NOT a + reliable success signal** — the only robust check is to RE-READ the installed + version after the call. +- Fix #1 (`updates_pkg_install_file` opkg branch): `opkg install + --force-downgrade --force-reinstall "$pkg_file"`. `--force-downgrade` lands the + v→no-v transition; `--force-reinstall` covers the same-exact-version no-op. apk + branch unchanged (`apk add --allow-untrusted`; apk overwrites by default). +- Fix #2 new helper `updates_pkg_installed_version <pkg>` (mirrors + `updates_pkg_is_installed`/`_candidate_version` but reads INSTALLED not feed): + opkg `list-installed | grep "^<pkg> " | head -n1 | awk -F' - ' '{print $2}'`; + apk `list --installed <pkg> | awk '{print $1}' | head -n1` then strip `<pkg>-` + prefix via `${line#"$pkg"-}`. grep/awk only, NO Oniguruma. +- Fix #3 verify-after-install belt in `_updates_self_update_netshift_core` (after + the install loop, BEFORE the defensive restore + success cleanup): re-read the + CORE installed version, normalize with the SAME rules the version-decision uses + (`${x#v}` then `${x%%-*}` to drop `-rN`), and gate on + `[ "$inst_semver" != "$latest_semver" ] && ! is_min_package_version + "$inst_semver" "$latest_semver"` (i.e. fail unless installed == target OR + installed >= target). On fail: `_updates_self_update_restore_config + "$backup_made"` + `rm -rf "$UPDATES_NETSHIFT_DOWNLOAD_DIR"` + `updates_log ... + "error"` + `echo '{"success":false,"message":"NetShift core package did not + upgrade ...; configuration preserved"}'` + **`return 1` (NEVER exit** — async + worker; exit skips the wrapper's `updates_restore_after_swap` epilogue + the + finished-job-state write). Only a verified change reaches the existing success + JSON. Gates the CORE pkg only (luci/ru stay non-critical). +- `is_min_package_version current required` returns 0 when `current >= required` + (it's a `sort -V | head -1 == required` test) — so call it as + `is_min_package_version "$installed" "$target"`. +- All new vars declared at the TOP of the core fn (`core_installed + core_installed_semver latest_semver`) — shellcheck `-S error` clean. +- **Smoke (test_self_update_netshift, alias `selfupdate`, no new top-level test):** + extended the fake opkg stub: `install` arm now skips leading `--*` flags + (`while ...; case --*) shift;; *) break;; esac`) so `$1` is the file path even + with the new force flags; on a REAL success it rewrites the `netshift -` line in + `$SU_INSTALLED_LIST` to `$SU_TARGET_INSTALLED_VER` (so verify passes) UNLESS + `$SU_NOOP` is set (then list keeps the OLD version → simulates "Not + downgrading"). luci-* files excluded from the rewrite via nested case. Each + scenario now seeds `installed.list` with `netshift - 0.8.0-r1` (the install arm + mutates it, so reset per-scenario). New Scenario 6 (`SU_NOOP=1`, all other + markers ok): asserts `selfupdate-noop-detected-successfalse` (success:false), + `-noop-install-attempted`, `-noop-config-intact`, `-noop-download-dir-cleaned`. + Happy path Scenario 3 still `success:true version 0.8.1` (verify passes because + the stub reports the target post-install). SELF-PROVED the guard: temporarily + prefixed the verify `if` with `false &&` → `selfupdate-noop-detected-successfalse` + FAILED (worker falsely reported success on the no-op), then restored → passes. +- shellcheck -S error clean (bin+libs+install.sh); `smoke-tests all` = 170 passed + / 0 failed (166 baseline + 4 new no-op assertions; selfupdate category 13→17). + No constants.sh / frontend / async-machinery / version-decision(:1679) / + sing-box-install-path / sacred-value change. apk path: the same verify belt + covers apk's equal-version no-overwrite quirk (reasoned; opkg covered in smoke). diff --git a/install.sh b/install.sh index 24498ec5..903c2d06 100755 --- a/install.sh +++ b/install.sh @@ -57,7 +57,7 @@ pkg_install() { # If you're installing a non-standard (self-built) package, use the --allow-untrusted option: apk add --allow-untrusted "$pkg_file" else - opkg install "$pkg_file" + opkg install --force-downgrade --force-reinstall "$pkg_file" fi } diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 4e5012e5..6243f089 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -1407,7 +1407,15 @@ updates_pkg_install_file() { if updates_pkg_is_apk; then apk add --allow-untrusted "$pkg_file" </dev/null >/dev/null 2>&1 else - opkg install "$pkg_file" </dev/null >/dev/null 2>&1 + # --force-downgrade: a legacy v-prefixed build (e.g. v0.8.6) sorts ABOVE + # the no-v target (0.8.7) in opkg's dpkg-style compare, so a plain + # `opkg install` returns rc=0 and refuses ("Not downgrading ..."). The + # flag forces the v->no-v transition to actually land. + # --force-reinstall: covers the "already installed at this exact version" + # no-op. opkg rc is NOT a reliable success signal either way — the + # verify-after-install belt in _updates_self_update_netshift_core is the + # authoritative check. + opkg install --force-downgrade --force-reinstall "$pkg_file" </dev/null >/dev/null 2>&1 fi } @@ -1447,6 +1455,32 @@ updates_pkg_candidate_version() { printf '%s' "$version" } +# Echoes the INSTALLED version of a package (what is on the system right now), +# or nothing if the package is not installed. Distinct from +# updates_pkg_candidate_version (that reads the FEED candidate). Parsed with +# grep/awk only — NEVER Oniguruma jq. Mirrors updates_pkg_is_installed. +# opkg list-installed -> "<name> - <version>" (field after " - ") +# apk list --installed <pkg> -> "<name>-<version> <arch> {...} ..." (strip "<name>-") +updates_pkg_installed_version() { + local pkg_name="$1" + local line version="" + + if updates_pkg_is_apk; then + # First installed-list token is "<name>-<version>"; strip the leading + # "<pkg>-" so only the version (e.g. "0.8.7-r1") remains. + line="$(apk list --installed "$pkg_name" 2>/dev/null | awk '{print $1}' | head -n1)" + case "$line" in + "$pkg_name"-*) version="${line#"$pkg_name"-}" ;; + esac + else + # opkg list-installed prints "<name> - <version>"; take the field after + # " - " for the exact package name. + version="$(opkg list-installed 2>/dev/null | grep "^${pkg_name} " | head -n1 | awk -F' - ' '{print $2}')" + fi + + printf '%s' "$version" +} + # Checks whether a newer STOCK (stable) sing-box is available via the system # package manager. SYNC (quick call → stays on the synchronous component_action # path). Graceful on an unreachable feed / parse failure: echoes @@ -1663,6 +1697,7 @@ _updates_self_update_download_assets() { # failure so the wrapper still runs the restore epilogue). _updates_self_update_netshift_core() { local installed latest pkg file_path candidate_file + local core_installed core_installed_semver latest_semver local backup_made=0 installed="$NETSHIFT_VERSION" @@ -1741,6 +1776,31 @@ _updates_self_update_netshift_core() { fi done + # Verify-after-install for the CORE package (authoritative success signal). + # opkg returns rc=0 for "already installed"/"up to date"/"Not downgrading", + # so the install rc above is NOT trustworthy. RE-READ the installed version + # and confirm it actually became the target before declaring success. apk's + # equal-version no-overwrite quirk is caught by this same belt. + core_installed="$(updates_pkg_installed_version "$UPDATES_NETSHIFT_PKG_CORE")" + # Normalize with the SAME rules the version-decision uses: drop a leading "v" + # and any "-rN"/"-suffix" so we compare semver-to-semver. + core_installed_semver="${core_installed#v}" + core_installed_semver="${core_installed_semver%%-*}" + latest_semver="${latest#v}" + latest_semver="${latest_semver%%-*}" + + # The install took iff the installed semver equals the target, OR the + # installed semver is now >= the target (is_min_package_version current + # required → 0 when current >= required). + if [ "$core_installed_semver" != "$latest_semver" ] \ + && ! is_min_package_version "$core_installed_semver" "$latest_semver"; then + _updates_self_update_restore_config "$backup_made" + rm -rf "$UPDATES_NETSHIFT_DOWNLOAD_DIR" 2>/dev/null + updates_log "Self-update: core package version did not change after install (package manager reported success but no upgrade occurred)" "error" + echo '{"success":false,"message":"NetShift core package did not upgrade (package manager refused or no-op); configuration preserved"}' + return 1 + fi + # Defensive: if the config got clobbered/emptied, restore from the backup. _updates_self_update_restore_config "$backup_made" diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 4a51e5f7..d9337f3f 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -5142,6 +5142,14 @@ DIGEOF exit 1 CURLEOF # Fake opkg: `install <file>` succeeds per marker and records the install. + # `list-installed` cats $SU_INSTALLED_LIST (the AUTHORITATIVE installed set). + # On a REAL (non-no-op) install the install arm rewrites the netshift line in + # that list to the target version (SU_TARGET_INSTALLED_VER), so the + # verify-after-install belt sees the upgrade. When $SU_NOOP is set the install + # arm returns rc=0 but does NOT touch the list (simulates opkg "Not + # downgrading"/"already installed"), so list-installed keeps reporting the OLD + # version. opkg ignores the extra --force-* flags the production code now + # passes (they come before the file path). cat > "$work/bin/opkg" << 'OPKGEOF' #!/bin/sh case "$1" in @@ -5149,9 +5157,33 @@ update) exit 0 ;; list-installed) cat "$SU_INSTALLED_LIST" 2>/dev/null; exit 0 ;; install) shift + # Skip the leading --force-* flags so $1 is the package file path. + while [ "$#" -gt 0 ]; do + case "$1" in + --*) shift ;; + *) break ;; + esac + done printf '%s\n' "$1" >> "$SU_INSTALL_LOG" - [ -f "$SU_PKG_OK" ] && exit 0 - exit 1 + [ -f "$SU_PKG_OK" ] || exit 1 + # A real success updates the installed list to the target version for the + # core package, UNLESS we are simulating a no-op ($SU_NOOP set). + if [ -z "$SU_NOOP" ]; then + case "$1" in + *netshift-* | *netshift_*) + # Only the core "netshift" file, not luci-app-/luci-i18n- ones. + case "$1" in + *luci-* ) : ;; + *) + grep -v '^netshift ' "$SU_INSTALLED_LIST" 2>/dev/null > "$SU_INSTALLED_LIST.tmp" + printf 'netshift - %s\n' "$SU_TARGET_INSTALLED_VER" >> "$SU_INSTALLED_LIST.tmp" + mv "$SU_INSTALLED_LIST.tmp" "$SU_INSTALLED_LIST" + ;; + esac + ;; + esac + fi + exit 0 ;; esac exit 0 @@ -5248,6 +5280,9 @@ DRVEOF export SU_INSTALL_LOG="$work/install.log" export SU_INSTALLED_LIST="$work/installed.list" export SU_LATEST_TAG="0.8.1" + # Version the fake opkg writes for "netshift" after a REAL (non-no-op) + # install, so the verify-after-install belt (task-041) sees the upgrade. + export SU_TARGET_INSTALLED_VER="0.8.1-r1" local out="$work/out.json" run_scenario() { @@ -5255,8 +5290,9 @@ DRVEOF PATH="$work/bin:/usr/bin:/bin" ash "$drv" > "$out" 2>/dev/null || true } - # RU i18n NOT installed (so it is never downloaded/installed). - : > "$work/installed.list" + # RU i18n NOT installed (so it is never downloaded/installed). The installed + # list starts with the OLD core version; a real install rewrites it. + printf 'netshift - 0.8.0-r1\n' > "$work/installed.list" # ── Scenario 1: connectivity fails → abort BEFORE any change ────────────── rm -f "$SU_DNS_OK" "$SU_HTTP_OK" "$SU_GH_OK" "$SU_DL_OK" "$SU_PKG_OK" @@ -5306,9 +5342,12 @@ DRVEOF fi # ── Scenario 3: happy path → success:true with version, restore ran ─────── + # The fake opkg rewrites the installed list to the target after a real + # install, so the task-041 verify-after-install belt confirms the upgrade. : > "$SU_DNS_OK"; : > "$SU_HTTP_OK"; : > "$SU_GH_OK"; : > "$SU_DL_OK"; : > "$SU_PKG_OK" printf 'CONFIG-ORIG\n' > "$work/etc-config-netshift" printf 'original-resolver\n' > "$work/resolv.conf" + printf 'netshift - 0.8.0-r1\n' > "$work/installed.list" run_scenario if jq -e '.success == true and .version == "0.8.1"' "$out" > /dev/null 2>&1; then pass "selfupdate-happy-successtrue-version:OK" @@ -5355,9 +5394,8 @@ DRVEOF # ── Scenario 5: RU i18n IS installed → it is upgraded too (3 installs) ───── : > "$SU_DNS_OK"; : > "$SU_HTTP_OK"; : > "$SU_GH_OK"; : > "$SU_DL_OK"; : > "$SU_PKG_OK" printf 'CONFIG-ORIG\n' > "$work/etc-config-netshift" - printf 'luci-i18n-netshift-ru - 0.8.0\n' > "$work/installed.list" + printf 'netshift - 0.8.0-r1\nluci-i18n-netshift-ru - 0.8.0\n' > "$work/installed.list" run_scenario - : > "$work/installed.list" if [ -f "$work/install.log" ] && [ "$(grep -c . "$work/install.log" 2>/dev/null)" = "3" ] \ && grep -q 'i18n' "$work/install.log" 2>/dev/null; then pass "selfupdate-ru-installed-upgraded:OK" @@ -5365,6 +5403,48 @@ DRVEOF fail "selfupdate-ru-installed-upgraded:FAIL" "install.log=$(cat "$work/install.log" 2>/dev/null)" fi + # ── Scenario 6: opkg silent no-op (task-041) → success:false, config intact + # All connectivity/GitHub/download/PKG markers "ok" AND the install returns + # rc=0, but $SU_NOOP makes the fake opkg NOT change what list-installed + # reports (the core stays at the OLD version) — simulating opkg "Not + # downgrading"/"already installed". The verify-after-install belt must catch + # this and report success:false WITHOUT touching the config. + : > "$SU_DNS_OK"; : > "$SU_HTTP_OK"; : > "$SU_GH_OK"; : > "$SU_DL_OK"; : > "$SU_PKG_OK" + export SU_NOOP=1 + printf 'CONFIG-ORIG\n' > "$work/etc-config-netshift" + printf 'original-resolver\n' > "$work/resolv.conf" + printf 'netshift - 0.8.0-r1\n' > "$work/installed.list" + run_scenario + unset SU_NOOP + # The worker MUST report success:false (the silent no-op is detected), NOT + # the false "updated" success it used to emit on rc=0. + if jq -e '.success == false' "$out" > /dev/null 2>&1; then + pass "selfupdate-noop-detected-successfalse:OK" + else + fail "selfupdate-noop-detected-successfalse:FAIL" "$(cat "$out" 2>/dev/null)" + fi + # The install was ATTEMPTED (rc=0) but the version never changed. + if [ -f "$work/install.log" ] && grep -q . "$work/install.log" 2>/dev/null; then + pass "selfupdate-noop-install-attempted:OK" + else + fail "selfupdate-noop-install-attempted:FAIL" "install.log=$(cat "$work/install.log" 2>/dev/null)" + fi + # Configuration preserved (verify-fail runs the defensive restore; nothing + # clobbered the live file). + if [ "$(cat "$work/etc-config-netshift" 2>/dev/null)" = "CONFIG-ORIG" ]; then + pass "selfupdate-noop-config-intact:OK" + else + fail "selfupdate-noop-config-intact:FAIL" "$(cat "$work/etc-config-netshift" 2>/dev/null)" + fi + # Download dir cleaned even on the no-op failure path. + if [ ! -d "$work/dl" ]; then + pass "selfupdate-noop-download-dir-cleaned:OK" + else + fail "selfupdate-noop-download-dir-cleaned:FAIL" "dl dir remains" + fi + + : > "$work/installed.list" + # ── Restore the real init script (if any) and clean up. ────────────────── if [ -n "$init_saved" ] && [ -e "$init_saved" ]; then cp -p "$init_saved" "$init_target" 2>/dev/null || true @@ -5372,7 +5452,7 @@ DRVEOF rm -f "$init_target" 2>/dev/null || true fi unset SU_DNS_OK SU_HTTP_OK SU_GH_OK SU_DL_OK SU_PKG_OK SU_INIT_LOG \ - SU_INSTALL_LOG SU_INSTALLED_LIST SU_LATEST_TAG + SU_INSTALL_LOG SU_INSTALLED_LIST SU_LATEST_TAG SU_TARGET_INSTALLED_VER SU_NOOP rm -rf "$work" } From a7a9f720e1693e894123533983702f0785303298 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Thu, 11 Jun 2026 22:28:15 +0300 Subject: [PATCH 70/75] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D1=8E=D0=B7?= =?UTF-8?q?=D0=B5=D1=80=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=B0=20Happ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 41 ++++++++++++++ .../memory/shell-backend-developer.md | 32 +++++++++++ netshift/files/usr/lib/constants.sh | 12 ++-- tests/entrypoint.sh | 55 ++++++++++++------- 4 files changed, 117 insertions(+), 23 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index d6a8b69d..2a4a954d 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -1104,3 +1104,44 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> text; rely on rc PLUS an explicit post-install version re-check. opkg compare-versions is available on-device for a robust numeric compare if needed (but the installer should not need the leading v at all once verify is added). + +## task-043 Xray subscription UA — versioned UAs (2026-06-11) + +- USER reported: with subscription_format_preference=xray, the panel returns 502 + and NetShift falls back to singbox/<ver> (sing-box JSON WITHOUT xhttp/hysteria2 + — the original task-031 complaint). PRIVACY: operator required NO + subscription-identifying data anywhere (no panel host/IP/URL/path/query/server/ + keys) in code/tests/comments/memory — only generic client UA strings are safe. + Diagnosis lived in the ephemeral SSH session only; never committed. +- ROOT CAUSE (reproduced on a live panel, abstractly): the Xray-probe constant + SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES was bare/version-less ("v2rayN Happ"). + That panel UA-gates its Xray branch on a <client>/<version> shape: a BARE Happ + (and v2rayN in any form) → 502 Bad Gateway; a VERSIONED Happ/<x.y.z> → 200 with + the wanted Xray-JSON ARRAY (multi-profile). The download path/headers + (_wget_subscription_request, helpers.sh:803) are correct & unchanged — only the + UA VALUE decides 502-vs-200. +- DIAGNOSIS METHOD (reusable, privacy-safe): on the router, replay the exact + request per UA with `curl -A "$UA" -H <same X-* headers netshift sends> -w + "HTTP %{http_code}"` to isolate UA-vs-headers; the headers were identical for + all UAs, so the UA alone is the variable. Sniff the 200 body's FORMAT (first + char `[`/`{`, jq top-keys) to confirm it's the Xray-JSON array + (dns,inbounds,log,outbounds,remarks,routing per element) — NOT to read values. + Clean up /tmp on the router after. +- FIX (task-043, APPROVED W/ CONDITIONS, smoke 170/0): constants-only — + SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES -> versioned ("Happ/1.0.0 v2rayN/7.0.0 + v2rayNG/1.9.0", versioned Happ FIRST). The xray-mode probe order in + build_subscription_user_agent_candidates (helpers.sh:765-771) front-loads these + before default+cached+whitelist, dedup whole-entry, so the working versioned + Happ wins first; non-working UAs just fall through. Coupled smoke + fb-caseI-xraypref-* updated to DERIVE expected first/second/third from the + constant (won't rot) + a generic guard that the first candidate contains "/" + (bare-UA regression catch). Operator REJECTED making it a UI option (overkill / + system-level) and rejected touching headers / auto-mode list. +- DEDUP NOTE: versioned "Happ/1.0.0" is DISTINCT from the bare "Happ" still in the + main auto-mode SUBSCRIPTION_USER_AGENT_CANDIDATES whitelist, so neither is + dropped by the whole-entry dedup. The bare v2rayN/Happ in the MAIN list still + 502 on this panel but are only reached AFTER the versioned UAs already win, so + no wasted probes in practice. Did NOT change the main list (broader/auto-mode). +- The CONDITION (M1) is commit-hygiene only: keep the pre-existing unrelated + .opencode/agent/*.md churn (model rename + bash permission) OUT of the task-043 + commit — not a code issue. Those were already in the tree at session start. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index fbf293d2..9b3e1ec9 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -1391,3 +1391,35 @@ findings; keep under ~200 lines. No constants.sh / frontend / async-machinery / version-decision(:1679) / sing-box-install-path / sacred-value change. apk path: the same verify belt covers apk's equal-version no-overwrite quirk (reasoned; opkg covered in smoke). + +## task-043 — versioned Xray-JSON probe User-Agents (constants-only) + +- Finding (abstract, no identifiers): some panels gate their Xray-JSON branch on + a `<client>/<version>` UA SHAPE. A bare/version-less UA can be rejected (server + 502); a VERSIONED UA of the same client yields the wanted Xray-JSON array body + (the multi-profile format `xray_json_to_uri_lines` parses, carrying + xhttp/hysteria2). Fix = make the probe send versioned UAs first. +- `SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES` (constants.sh) is the ordered set + probed FIRST in `xray` format-preference mode by + `build_subscription_user_agent_candidates` (helpers.sh:765-771; + order = XRAY_CANDIDATES → default(singbox/<ver>) → cached-winner → main + whitelist, deduped). Changed it from bare `"v2rayN Happ"` to versioned + `"Happ/1.0.0 v2rayN/7.0.0 v2rayNG/1.9.0"` (versioned client first). The + separate auto-mode `SUBSCRIPTION_USER_AGENT_CANDIDATES` (still has bare names) + and the request headers in `_wget_subscription_request` were untouched — only + the UA VALUE decides the 502-vs-200 outcome, no header change needed. +- Coupled smoke test: `fb-caseI-xraypref-*` (tests/entrypoint.sh CASE I, run via + the `subscription` category) hardcoded the old bare literals. Rewrote them to + DERIVE the expected first/second/third candidates from + `$SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES` (sourced in the CASE-I subshell via + `set -- $SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES`) so a future version bump + won't rot the test. Added one generic guard `fb-caseI-xraypref-first-versioned` + (`case "$first" in */*)`) so we never regress to a bare UA. The auto-mode + `fb-caseI-auto-has-v2rayN` assertion is unrelated (tests the auto whitelist). +- shellcheck -S error clean (entrypoint.sh is OUT of the lint scope — only + bin/lib/install.sh). `smoke-tests all` = 170 passed / 0 failed (same total + before+after; the standalone `subscription` category went 86→87 from the +1 + guard, but the aggregate `all` "Results:" counter reported 170 either way — a + harness counting quirk, not a regression; all four `fb-caseI-xraypref-*` lines + print :OK in both runs). No ports/marks/paths/schema touched; runtime contract + intact. diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 0f22c8b3..5c833901 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -28,12 +28,16 @@ TMP_SUBSCRIPTION_MERGE_FOLDER="$TMP_SING_BOX_FOLDER/subscription-merge" # "singbox/<version>" candidate is prepended at runtime (it depends on the # installed sing-box). Order matters: most-likely-to-work first. SUBSCRIPTION_USER_AGENT_CANDIDATES="v2rayN Happ Hiddify Clash.Meta ClashMetaForAndroid" -# Subset of SUBSCRIPTION_USER_AGENT_CANDIDATES that well-known panels answer with -# an Xray JSON body (which carries xhttp/transport nodes the default sing-box JSON -# may omit). Used by build_subscription_user_agent_candidates when a section's +# Versioned client UAs that well-known panels answer with an Xray JSON body +# (which carries xhttp/transport nodes the default sing-box JSON may omit). Used +# by build_subscription_user_agent_candidates when a section's # subscription_format_preference is "xray": these UAs are probed FIRST so an # Xray-JSON feed is recovered before a sing-box JSON under the default UA wins. -SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES="v2rayN Happ" +# Panels commonly gate their Xray branch on a "<client>/<version>" UA shape, so +# these are VERSIONED (a bare/version-less UA can be rejected, e.g. with a 502). +# Order matters: a versioned Happ is first (empirically yields the Xray-JSON +# array body), then versioned v2rayN/v2rayNG forms as panel-agnostic fallbacks. +SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES="Happ/1.0.0 v2rayN/7.0.0 v2rayNG/1.9.0" CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker JQ_REQUIRED_VERSION="1.7.1" COREUTILS_BASE64_REQUIRED_VERSION="9.7" diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index d9337f3f..f4506e1e 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -2512,44 +2512,61 @@ else echo "fb-caseI-singboxpref-default-first(got '$caseI_sbp_first'):FAIL" fi -# (f) xray preference: the Xray-JSON UAs (Happ/v2rayN) come FIRST — before the +# (f) xray preference: the versioned Xray-JSON UAs come FIRST — before the # default singbox/<ver> AND before the cached preferred winner. Pass a cached -# preferred ('Hiddify') to prove the xray UAs outrank it. +# preferred ('Hiddify') to prove the xray UAs outrank it. The expected first two +# candidates are DERIVED from the SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES constant +# (sourced above) so a future constant tweak doesn't rot this test. caseI_xray="$(build_subscription_user_agent_candidates "" "Hiddify" "xray")" caseI_xray_first="$(printf '%s\n' "$caseI_xray" | sed -n '1p')" caseI_xray_second="$(printf '%s\n' "$caseI_xray" | sed -n '2p')" +# Expected first/second/third xray UAs, split from the constant (in order). +# shellcheck disable=SC2086 # word-splitting of the candidate list is intentional +set -- $SUBSCRIPTION_USER_AGENT_XRAY_CANDIDATES +caseI_xray_exp1="$1" +caseI_xray_exp2="$2" +caseI_xray_exp3="$3" # Position helper: line number of an exact match (empty if absent). caseI_pos() { printf '%s\n' "$1" | grep -Fxn "$2" | head -n1 | cut -d: -f1; } -caseI_xray_p_v2rayn="$(caseI_pos "$caseI_xray" 'v2rayN')" -caseI_xray_p_happ="$(caseI_pos "$caseI_xray" 'Happ')" +caseI_xray_p_1="$(caseI_pos "$caseI_xray" "$caseI_xray_exp1")" +caseI_xray_p_2="$(caseI_pos "$caseI_xray" "$caseI_xray_exp2")" +caseI_xray_p_3="$(caseI_pos "$caseI_xray" "$caseI_xray_exp3")" caseI_xray_p_default="$(caseI_pos "$caseI_xray" "$caseI_default")" caseI_xray_p_pref="$(caseI_pos "$caseI_xray" 'Hiddify')" -# First two lines are the xray subset "v2rayN Happ" (in constant order). -if [ "$caseI_xray_first" = 'v2rayN' ] && [ "$caseI_xray_second" = 'Happ' ]; then +# First two lines are the first two xray candidates (in constant order). +if [ "$caseI_xray_first" = "$caseI_xray_exp1" ] && [ "$caseI_xray_second" = "$caseI_xray_exp2" ]; then echo 'fb-caseI-xraypref-xray-first:OK' else echo "fb-caseI-xraypref-xray-first(1st='$caseI_xray_first' 2nd='$caseI_xray_second'):FAIL" fi -# xray UAs precede the default and the cached preferred winner. -if [ -n "$caseI_xray_p_v2rayn" ] && [ -n "$caseI_xray_p_happ" ] && +# Guard: the first xray candidate is VERSIONED (contains a '/'), not a bare UA. +case "$caseI_xray_first" in +*/*) echo 'fb-caseI-xraypref-first-versioned:OK' ;; +*) echo "fb-caseI-xraypref-first-versioned(got '$caseI_xray_first'):FAIL" ;; +esac +# Every xray UA precedes the default and the cached preferred winner. +if [ -n "$caseI_xray_p_1" ] && [ -n "$caseI_xray_p_2" ] && [ -n "$caseI_xray_p_3" ] && [ -n "$caseI_xray_p_default" ] && [ -n "$caseI_xray_p_pref" ] && - [ "$caseI_xray_p_v2rayn" -lt "$caseI_xray_p_default" ] && - [ "$caseI_xray_p_happ" -lt "$caseI_xray_p_default" ] && - [ "$caseI_xray_p_v2rayn" -lt "$caseI_xray_p_pref" ] && - [ "$caseI_xray_p_happ" -lt "$caseI_xray_p_pref" ]; then + [ "$caseI_xray_p_1" -lt "$caseI_xray_p_default" ] && + [ "$caseI_xray_p_2" -lt "$caseI_xray_p_default" ] && + [ "$caseI_xray_p_3" -lt "$caseI_xray_p_default" ] && + [ "$caseI_xray_p_1" -lt "$caseI_xray_p_pref" ] && + [ "$caseI_xray_p_2" -lt "$caseI_xray_p_pref" ] && + [ "$caseI_xray_p_3" -lt "$caseI_xray_p_pref" ]; then echo 'fb-caseI-xraypref-outranks-default-and-cache:OK' else - echo "fb-caseI-xraypref-outranks-default-and-cache(v2rayN=$caseI_xray_p_v2rayn happ=$caseI_xray_p_happ def=$caseI_xray_p_default pref=$caseI_xray_p_pref):FAIL" + echo "fb-caseI-xraypref-outranks-default-and-cache(1=$caseI_xray_p_1 2=$caseI_xray_p_2 3=$caseI_xray_p_3 def=$caseI_xray_p_default pref=$caseI_xray_p_pref):FAIL" fi -# Dedup holds: no UA emitted twice (each of v2rayN/Happ/default appears once). -caseI_xray_v2rayn_count="$(printf '%s\n' "$caseI_xray" | grep -Fxc 'v2rayN')" -caseI_xray_happ_count="$(printf '%s\n' "$caseI_xray" | grep -Fxc 'Happ')" +# Dedup holds: no UA emitted twice (each xray UA and the default appears once). +caseI_xray_1_count="$(printf '%s\n' "$caseI_xray" | grep -Fxc "$caseI_xray_exp1")" +caseI_xray_2_count="$(printf '%s\n' "$caseI_xray" | grep -Fxc "$caseI_xray_exp2")" +caseI_xray_3_count="$(printf '%s\n' "$caseI_xray" | grep -Fxc "$caseI_xray_exp3")" caseI_xray_def_count="$(printf '%s\n' "$caseI_xray" | grep -Fxc "$caseI_default")" -if [ "$caseI_xray_v2rayn_count" = "1" ] && [ "$caseI_xray_happ_count" = "1" ] && - [ "$caseI_xray_def_count" = "1" ]; then +if [ "$caseI_xray_1_count" = "1" ] && [ "$caseI_xray_2_count" = "1" ] && + [ "$caseI_xray_3_count" = "1" ] && [ "$caseI_xray_def_count" = "1" ]; then echo 'fb-caseI-xraypref-dedup:OK' else - echo "fb-caseI-xraypref-dedup(v2rayN=$caseI_xray_v2rayn_count happ=$caseI_xray_happ_count def=$caseI_xray_def_count):FAIL" + echo "fb-caseI-xraypref-dedup(1=$caseI_xray_1_count 2=$caseI_xray_2_count 3=$caseI_xray_3_count def=$caseI_xray_def_count):FAIL" fi # (g) Unrecognised preference falls back to auto order: default first. From 8e40c49fa4186907c4ef606527d935bd93ced223 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Fri, 12 Jun 2026 09:00:28 +0300 Subject: [PATCH 71/75] =?UTF-8?q?=D1=83=D0=BD=D0=B8=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=BF=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=BA=D1=81=D0=B8=20=D0=B2=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 46 ++++ .../memory/luci-frontend-developer.md | 50 ++++ fe-app-netshift/locales/calls.json | 237 ++++++++++-------- fe-app-netshift/locales/netshift.pot | 216 ++++++++-------- fe-app-netshift/locales/netshift.ru.po | 29 ++- fe-app-netshift/src/netshift/types.ts | 3 +- .../resources/view/netshift/section.js | 29 ++- luci-app-netshift/po/ru/netshift.po | 29 ++- luci-app-netshift/po/templates/netshift.pot | 216 ++++++++-------- netshift/files/etc/config/netshift | 6 + netshift/files/usr/bin/netshift | 142 ++++++++--- netshift/files/usr/lib/constants.sh | 3 + tests/entrypoint.sh | 118 +++++++++ 13 files changed, 776 insertions(+), 348 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 2a4a954d..1575e203 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -1145,3 +1145,49 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> - The CONDITION (M1) is commit-hygiene only: keep the pre-existing unrelated .opencode/agent/*.md churn (model rename + bash permission) OUT of the task-043 commit — not a code issue. Those were already in the tree at session start. + +## task-044/045 Universal subscription grouper (2026-06-12) + +- FEATURE: generalize the country-flag subscription grouper into modes + off/country/prefix. Operator wanted "group by first N chars of the remark, N + configurable, default 2"; chose Variant A (single mode dropdown off/country/ + prefix + length, count by Unicode CODEPOINTS), full backend+frontend. +- ARCHITECTURE SEAM (reusable insight): the urltest-per-group + main-selector + tree builder in bin/netshift `subscription)` branch is mode-agnostic — only the + KEY EXTRACTOR + the on/off decision needed generalizing. + `sing_box_build_subscription_country_groups` -> `sing_box_build_subscription_ + groups <tags_json> <mode> <prefix_len>` returning {group_order,groups,ungrouped}. + prefix key = `(.|explode)|.[0:$n]|implode` (codepoint slice, Unicode-safe, no + Oniguruma). country path kept byte-identical (regional-indicator gate). N=2 + over a flag tag == the flag, so country is a consistent special case. +- LANDMINE CAUGHT IN REVIEW LOOP (architect spotted, not the dev): the inherited + group loop `for k in $(jq -r '.group_order[]')` WORD-SPLITS on IFS. Safe for + country flags (no spaces) but BREAKS prefix mode whose keys can contain spaces + (e.g. tag "A 1" prefix-2 -> key "A "). Fix: mktemp + `while IFS= read -r k ... + done < "$tmp"` in the CURRENT shell (NOT `cmd | while read` — that subshell + would lose the `config=`/`selector_outbounds_json=` mutations; same landmine + class as the smoke `cmd|while read` trap). Mirror the existing + `subscription_urls_tmp` mktemp+read<file pattern already in that branch. mktemp + failure must fatal+exit 1. ALWAYS audit reused loops when the key domain widens. +- prefix_len hardening: jq side `try tonumber catch default | <1 -> default | + floor`; shell side `case '' | *[!0-9]* -> default ;; *) [ x -ge 1 ] 2>/dev/null`. + short tag (<N) keys by whole self (NOT ungrouped); empty tag -> ungrouped. +- LEGACY MIGRATION done BACKEND-side (no JS migration): subscription_group_mode + present OUTRANKS legacy; absent -> read subscription_group_by_countries (then + alias group_by_countries) truthy->country else off. Frontend just writes the + new options. New constant SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN=2. +- FRONTEND: form.Flag -> form.ListValue subscription_group_mode (off/country/ + prefix) + form.Value subscription_group_prefix_len (datatype and(uinteger, + min(1)), depends subscription_group_mode:"prefix"); both taboption() in the + `subscription` tab (tabbed-CBI completeness). types.ts old key removed (no TS + reader). 7 i18n msgids + RU, fe<->luci byte-identical. main.js NO diff + (type-only change + hand-written view) — so the rendered tab needs a HUMAN + visual check (carried as APPROVED-WITH-CONDITIONS [M1]). +- GATES: shellcheck error clean; smoke 170->174/0 (country regression kept + + prefix len-2/consistency/bad-len/space-key); whole-chain sing-box check PASS + for mode=prefix; yarn ci green (472 vitest), main.js no-diff. Both APPROVED + (045 with the human-eyeball condition). PRIVACY: synthetic tags only, full + diff privacy-scan clean. Subagents truncated their final messages TWICE this + session (frontend stopped before section.js; reviewer stopped mid-analysis) — + ALWAYS verify on-disk state + run/inspect gates yourself rather than trusting + a truncated "done". diff --git a/docs/agent-rules/memory/luci-frontend-developer.md b/docs/agent-rules/memory/luci-frontend-developer.md index 44878731..3547baa0 100644 --- a/docs/agent-rules/memory/luci-frontend-developer.md +++ b/docs/agent-rules/memory/luci-frontend-developer.md @@ -811,3 +811,53 @@ append findings; keep under ~200 lines. task-039 backend changes (netshift bin, updater.sh, tests/entrypoint.sh) + .opencode/agent edits — NOT mine. FLAG (no browser): button render + toast sequence verified by reasoning + the gate, NOT screenshotted. + +## task-045 — universal subscription grouper (mode dropdown + prefix length) + +- REPLACED the single `subscription_group_by_countries` form.Flag (section.js + ~190-201) with TWO taboptions in the SAME `subscription` tab (mandatory — + tabbed section, a plain option() renders nothing): + (1) `form.ListValue subscription_group_mode` — values off/country/prefix + (`_("Off")`/`_("By country flag")`/`_("By name prefix")`), `o.default="off"`, + `o.rmempty=false`, depends `{connection_type:"proxy",proxy_config_type: + "subscription"}`; title `_("Subscription grouping")` + single-literal help. + (2) `form.Value subscription_group_prefix_len` — title `_("Prefix length")`, + `o.default="2"`, `o.datatype="and(uinteger,min(1))"`, `o.rmempty=false`, + depends ADDS `subscription_group_mode:"prefix"` (3-key object) so it shows + ONLY when mode=prefix. CBI cross-field depends within the same section/tab + works fine; a fully-hidden field is OK. +- CROSS-LAYER CONTRACT (task-044 backend, DONE): UCI options EXACTLY + `subscription_group_mode` ∈ {off,country,prefix} default off, and + `subscription_group_prefix_len` positive-int string default 2 (meaningful + only when mode=prefix). Backend falls back to the LEGACY + `subscription_group_by_countries` boolean ONLY when the new option is ABSENT + → the UI writes only the NEW options; NO JS migration written. +- types.ts: swapped `subscription_group_by_countries?: '0'|'1'` → + `subscription_group_mode?: 'off'|'country'|'prefix'` + + `subscription_group_prefix_len?: string`. Grepped src first — NOTHING in TS + reads the old key (only the type decl), so removing it is safe (backend reads + the legacy UCI key directly, not via UI). Pure type-only → erased at build. +- main.js: ZERO diff (section.js hand-written + not bundled; types.ts type-only). + md5 unchanged across the build (aa89dfc5…). Confirmed via + `git diff --exit-code main.js`. This is the EXPECTED/correct outcome — a diff + there would mean an unexpected src change. +- i18n: ran `node {extract-calls,generate-pot,generate-po ru,distribute- + locales}.js` (yarn classic 1.22.22, but used node to avoid corepack). msgid + delta = clean SWAP: removed 2 (`Group by countries` + its long description), + added 7 (Off / By country flag / By name prefix / Subscription grouping / + Prefix length / the grouping description / the prefix-length description). + Filled 7 RU msgstr in SOURCE locales/netshift.ru.po then re-ran distribute → + po/ru + po/templates byte-identical to source (diff -q both pairs). 0 empty + non-header msgstr after. RU: Off→Выключено, By country flag→По флагу страны, + By name prefix→По префиксу имени, Subscription grouping→Группировка подписки, + Prefix length→Длина префикса. +- yarn ci GREEN: format no-diff, eslint --max-warnings=0, vitest 472 pass, tsup + build. yarn.lock unchanged, no .yarn/.yarnrc.yml. No new vitest (no new pure + TS logic — datatype validation is LuCI client-side). +- PRIVACY: no subscription-identifying data (hosts/IPs/URLs/keys/node names) in + any code/comment/i18n/test/memory — generic "proxy name"/"country flag" + wording only. +- FLAG (no browser in env): the rendered Subscription tab (dropdown + + conditional prefix-length field appearing only on mode=prefix, taboption + auto-hide) needs a HUMAN VISUAL CHECK before merge — verified structurally + only (taboption completeness, depends preserved). diff --git a/fe-app-netshift/locales/calls.json b/fe-app-netshift/locales/calls.json index 35653bbb..51179121 100644 --- a/fe-app-netshift/locales/calls.json +++ b/fe-app-netshift/locales/calls.json @@ -45,14 +45,14 @@ "call": "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", "key": "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:607" ] }, { "call": "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", "key": "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:695" ] }, { @@ -88,21 +88,21 @@ "call": "Applicable for SOCKS and Shadowsocks proxy", "key": "Applicable for SOCKS and Shadowsocks proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:371" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390" ] }, { "call": "At least one valid domain must be specified. Comments-only content is not allowed.", "key": "At least one valid domain must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:667" ] }, { "call": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "key": "At least one valid subnet or IP must be specified. Comments-only content is not allowed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:737" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:756" ] }, { @@ -161,6 +161,20 @@ "src/netshift/tabs/diagnostic/checks/runFakeIPCheck.ts:57" ] }, + { + "call": "By country flag", + "key": "By country flag", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:200" + ] + }, + { + "call": "By name prefix", + "key": "By name prefix", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:201" + ] + }, { "call": "Cache File Path", "key": "Cache File Path", @@ -258,7 +272,7 @@ "call": "Community Lists", "key": "Community Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:494" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513" ] }, { @@ -350,14 +364,14 @@ "call": "Custom domains", "key": "Custom domains", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:606" ] }, { "call": "Custom subnets", "key": "Custom subnets", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:694" ] }, { @@ -435,8 +449,8 @@ "call": "Disabled", "key": "Disabled", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:592", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:680" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:699" ] }, { @@ -471,7 +485,7 @@ "call": "DNS over HTTPS (DoH)", "key": "DNS over HTTPS (DoH)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:460", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48" ] }, @@ -479,7 +493,7 @@ "call": "DNS over TLS (DoT)", "key": "DNS over TLS (DoT)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:461", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:480", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49" ] }, @@ -487,7 +501,7 @@ "call": "DNS Protocol Type", "key": "DNS Protocol Type", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:476", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45" ] }, @@ -502,7 +516,7 @@ "call": "DNS Server", "key": "DNS Server", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:471", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:490", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58" ] }, @@ -531,7 +545,7 @@ "call": "Domain Resolver", "key": "Domain Resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465" ] }, { @@ -582,15 +596,15 @@ "call": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "key": "Drop subscription servers whose name contains any of these keywords (case-insensitive).", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:239" ] }, { "call": "Dynamic List", "key": "Dynamic List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:593", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:700" ] }, { @@ -604,14 +618,14 @@ "call": "Enable built-in DNS resolver for domains handled by this section", "key": "Enable built-in DNS resolver for domains handled by this section", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:466" ] }, { "call": "Enable DNS resolve to get real IP when routing", "key": "Enable DNS resolve to get real IP when routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:933" ] }, { @@ -632,7 +646,7 @@ "call": "Enable Mixed Proxy", "key": "Enable Mixed Proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:883" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:902" ] }, { @@ -646,7 +660,7 @@ "call": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "key": "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:884" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:903" ] }, { @@ -674,28 +688,28 @@ "call": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "key": "Enter domain names separated by commas, spaces, or newlines. You can add comments using //", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:630" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:649" ] }, { "call": "Enter domain names without protocols, e.g. example.com or sub.example.com", "key": "Enter domain names without protocols, e.g. example.com or sub.example.com", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:622" ] }, { "call": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "key": "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:691" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:710" ] }, { "call": "Every 1 minute", "key": "Every 1 minute", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:287" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:306" ] }, { @@ -716,7 +730,7 @@ "call": "Every 3 minutes", "key": "Every 3 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:288" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:307" ] }, { @@ -730,14 +744,14 @@ "call": "Every 30 seconds", "key": "Every 30 seconds", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:286" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:305" ] }, { "call": "Every 5 minutes", "key": "Every 5 minutes", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:289" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:308" ] }, { @@ -779,7 +793,7 @@ "call": "Exclude servers by keyword", "key": "Exclude servers by keyword", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:238" ] }, { @@ -802,12 +816,6 @@ "call": "Failed to execute!", "key": "Failed to execute!", "places": [ - "src/netshift/tabs/diagnostic/initController.ts:229", - "src/netshift/tabs/diagnostic/initController.ts:233", - "src/netshift/tabs/diagnostic/initController.ts:263", - "src/netshift/tabs/diagnostic/initController.ts:267", - "src/netshift/tabs/diagnostic/initController.ts:304", - "src/netshift/tabs/diagnostic/initController.ts:308", "src/netshift/tabs/manager/initController.ts:122", "src/netshift/tabs/manager/initController.ts:132", "src/netshift/tabs/manager/initController.ts:150", @@ -815,7 +823,13 @@ "src/netshift/tabs/manager/initController.ts:188", "src/netshift/tabs/manager/initController.ts:192", "src/netshift/tabs/manager/initController.ts:225", - "src/netshift/tabs/manager/initController.ts:229" + "src/netshift/tabs/manager/initController.ts:229", + "src/netshift/tabs/diagnostic/initController.ts:229", + "src/netshift/tabs/diagnostic/initController.ts:233", + "src/netshift/tabs/diagnostic/initController.ts:263", + "src/netshift/tabs/diagnostic/initController.ts:267", + "src/netshift/tabs/diagnostic/initController.ts:304", + "src/netshift/tabs/diagnostic/initController.ts:308" ] }, { @@ -832,7 +846,7 @@ "call": "Fully Routed IPs", "key": "Fully Routed IPs", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:855" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:874" ] }, { @@ -853,19 +867,12 @@ "call": "Global Proxy", "key": "Global Proxy", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:381" - ] - }, - { - "call": "Group by countries", - "key": "Group by countries", - "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400" ] }, { - "call": "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag", - "key": "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag", + "call": "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters.", + "key": "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters.", "places": [ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:195" ] @@ -888,7 +895,7 @@ "call": "Include servers by keyword", "key": "Include servers by keyword", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:207" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:226" ] }, { @@ -1314,7 +1321,7 @@ "call": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "key": "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:208" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:227" ] }, { @@ -1357,14 +1364,14 @@ "call": "Local Domain Lists", "key": "Local Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:759" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:778" ] }, { "call": "Local Subnet Lists", "key": "Local Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:802" ] }, { @@ -1406,7 +1413,7 @@ "call": "Mixed Proxy Port", "key": "Mixed Proxy Port", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:897" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:916" ] }, { @@ -1420,7 +1427,7 @@ "call": "Must be a number in the range of 50 - 1000", "key": "Must be a number in the range of 50 - 1000", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:343" ] }, { @@ -1462,7 +1469,7 @@ "call": "Network Interface", "key": "Network Interface", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418" ] }, { @@ -1509,11 +1516,25 @@ "src/netshift/tabs/diagnostic/diagnostic.store.ts:91" ] }, + { + "call": "Number of leading characters of each proxy name to group by.", + "key": "Number of leading characters of each proxy name to group by.", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:211" + ] + }, + { + "call": "Off", + "key": "Off", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:199" + ] + }, { "call": "Only one section can be global at a time.", "key": "Only one section can be global at a time.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:409" ] }, { @@ -1591,6 +1612,13 @@ "src/netshift/tabs/diagnostic/diagnostic.store.ts:139" ] }, + { + "call": "Prefix length", + "key": "Prefix length", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:210" + ] + }, { "call": "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT.", "key": "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT.", @@ -1623,28 +1651,28 @@ "call": "Regional options cannot be used together", "key": "Regional options cannot be used together", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547" ] }, { "call": "Remote Domain Lists", "key": "Remote Domain Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:807" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:826" ] }, { "call": "Remote Subnet Lists", "key": "Remote Subnet Lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:850" ] }, { "call": "Resolve real IP for routing", "key": "Resolve real IP for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:932" ] }, { @@ -1658,7 +1686,7 @@ "call": "Route all unmatched traffic through this section's outbound.", "key": "Route all unmatched traffic through this section's outbound.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401" ] }, { @@ -1742,7 +1770,7 @@ "call": "Russia inside restrictions", "key": "Russia inside restrictions", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:566" ] }, { @@ -1763,7 +1791,7 @@ "call": "Select a predefined list for routing", "key": "Select a predefined list for routing", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:495" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514" ] }, { @@ -1798,14 +1826,14 @@ "call": "Select network interface for VPN connection", "key": "Select network interface for VPN connection", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:419" ] }, { "call": "Select or enter DNS server address", "key": "Select or enter DNS server address", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:491", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59" ] }, @@ -1827,7 +1855,7 @@ "call": "Select the DNS protocol type for the domain resolver", "key": "Select the DNS protocol type for the domain resolver", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:458" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:477" ] }, { @@ -1869,7 +1897,7 @@ "call": "Selector Proxy Links", "key": "Selector Proxy Links", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:231" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:250" ] }, { @@ -1990,29 +2018,29 @@ "call": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "key": "Specify local IP addresses or subnets whose traffic will always be routed through the configured route", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:856" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:875" ] }, { "call": "Specify remote URLs to download and use domain lists", "key": "Specify remote URLs to download and use domain lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:827" ] }, { "call": "Specify remote URLs to download and use subnet lists", "key": "Specify remote URLs to download and use subnet lists", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:832" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:851" ] }, { "call": "Specify the path to the list file located on the router filesystem", "key": "Specify the path to the list file located on the router filesystem", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:760", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:779", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:803" ] }, { @@ -2058,6 +2086,13 @@ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:148" ] }, + { + "call": "Subscription grouping", + "key": "Subscription grouping", + "places": [ + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194" + ] + }, { "call": "Subscription Update Interval", "key": "Subscription Update Interval", @@ -2132,8 +2167,8 @@ "call": "Text List", "key": "Text List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:682" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:613", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701" ] }, { @@ -2147,21 +2182,21 @@ "call": "The interval between connectivity tests", "key": "The interval between connectivity tests", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:303" ] }, { "call": "The maximum difference in response times (ms) allowed when comparing servers", "key": "The maximum difference in response times (ms) allowed when comparing servers", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:318" ] }, { "call": "The URL used to test server connectivity", "key": "The URL used to test server connectivity", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:351" ] }, { @@ -2217,7 +2252,7 @@ "call": "UDP (Unprotected DNS)", "key": "UDP (Unprotected DNS)", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:462", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481", "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50" ] }, @@ -2225,25 +2260,25 @@ "call": "UDP over TCP", "key": "UDP over TCP", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:389" ] }, { "call": "unknown", "key": "unknown", "places": [ - "src/netshift/tabs/diagnostic/initController.ts:39", - "src/netshift/tabs/diagnostic/initController.ts:40", - "src/netshift/tabs/diagnostic/initController.ts:41", - "src/netshift/tabs/diagnostic/initController.ts:42", - "src/netshift/tabs/diagnostic/initController.ts:43", - "src/netshift/tabs/diagnostic/initController.ts:44", "src/netshift/tabs/manager/initController.ts:37", "src/netshift/tabs/manager/initController.ts:38", "src/netshift/tabs/manager/initController.ts:39", "src/netshift/tabs/manager/initController.ts:40", "src/netshift/tabs/manager/initController.ts:41", "src/netshift/tabs/manager/initController.ts:42", + "src/netshift/tabs/diagnostic/initController.ts:39", + "src/netshift/tabs/diagnostic/initController.ts:40", + "src/netshift/tabs/diagnostic/initController.ts:41", + "src/netshift/tabs/diagnostic/initController.ts:42", + "src/netshift/tabs/diagnostic/initController.ts:43", + "src/netshift/tabs/diagnostic/initController.ts:44", "src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7" ] }, @@ -2323,28 +2358,28 @@ "call": "URLTest Check Interval", "key": "URLTest Check Interval", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:302" ] }, { "call": "URLTest Proxy Links", "key": "URLTest Proxy Links", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:257" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:276" ] }, { "call": "URLTest Testing URL", "key": "URLTest Testing URL", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:331" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:350" ] }, { "call": "URLTest Tolerance", "key": "URLTest Tolerance", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:298" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:317" ] }, { @@ -2365,35 +2400,35 @@ "call": "Use with Exclusion sections to route specific domains directly.", "key": "Use with Exclusion sections to route specific domains directly.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:388" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:407" ] }, { "call": "User Domains", "key": "User Domains", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:621" ] }, { "call": "User Domains List", "key": "User Domains List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648" ] }, { "call": "User Subnets", "key": "User Subnets", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:690" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:709" ] }, { "call": "User Subnets List", "key": "User Subnets List", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:717" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:736" ] }, { @@ -2425,8 +2460,8 @@ "call": "Validation errors:", "key": "Validation errors:", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:749" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768" ] }, { @@ -2456,29 +2491,29 @@ "key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links", "places": [ "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232", - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:258" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:251", + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:277" ] }, { "call": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "key": "Warning: %s cannot be used together with %s. Previous selections have been removed.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:530" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549" ] }, { "call": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "key": "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:568" ] }, { "call": "When enabled, traffic not matching any other section's lists will go through this proxy.", "key": "When enabled, traffic not matching any other section's lists will go through this proxy.", "places": [ - "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384" + "../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:403" ] }, { diff --git a/fe-app-netshift/locales/netshift.pot b/fe-app-netshift/locales/netshift.pot index f9a0a843..df59ccb2 100644 --- a/fe-app-netshift/locales/netshift.pot +++ b/fe-app-netshift/locales/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-11 17:31+0300\n" -"PO-Revision-Date: 2026-06-11 17:31+0300\n" +"POT-Creation-Date: 2026-06-12 05:52+0300\n" +"PO-Revision-Date: 2026-06-12 05:52+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -40,11 +40,11 @@ msgstr "" msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:607 msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:695 msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" msgstr "" @@ -65,15 +65,15 @@ msgstr "" msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:371 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:667 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:737 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:756 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -109,6 +109,14 @@ msgstr "" msgid "Browser is using FakeIP correctly" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:200 +msgid "By country flag" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:201 +msgid "By name prefix" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:517 msgid "Cache File Path" msgstr "" @@ -166,7 +174,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:494 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513 msgid "Community Lists" msgstr "" @@ -219,11 +227,11 @@ msgstr "" msgid "Currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:606 msgid "Custom domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:694 msgid "Custom subnets" msgstr "" @@ -268,8 +276,8 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:592 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:680 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:699 msgid "Disabled" msgstr "" @@ -289,17 +297,17 @@ msgstr "" msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:460 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:461 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:480 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:476 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 msgid "DNS Protocol Type" msgstr "" @@ -308,7 +316,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:471 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:490 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58 msgid "DNS Server" msgstr "" @@ -325,7 +333,7 @@ msgstr "" msgid "Domain and subnet lists that decide which traffic uses this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465 msgid "Domain Resolver" msgstr "" @@ -355,12 +363,12 @@ msgstr "" msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:239 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:593 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:700 msgid "Dynamic List" msgstr "" @@ -368,11 +376,11 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:466 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:933 msgid "Enable DNS resolve to get real IP when routing" msgstr "" @@ -384,7 +392,7 @@ msgstr "" msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:883 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:902 msgid "Enable Mixed Proxy" msgstr "" @@ -392,7 +400,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:884 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:903 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -408,19 +416,19 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:630 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:649 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:622 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:691 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:710 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:287 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:306 msgid "Every 1 minute" msgstr "" @@ -432,7 +440,7 @@ msgstr "" msgid "Every 3 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:288 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:307 msgid "Every 3 minutes" msgstr "" @@ -440,11 +448,11 @@ msgstr "" msgid "Every 30 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:286 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:305 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:289 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:308 msgid "Every 5 minutes" msgstr "" @@ -468,7 +476,7 @@ msgstr "" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:238 msgid "Exclude servers by keyword" msgstr "" @@ -482,12 +490,6 @@ msgstr "" msgid "Failed to copy!" msgstr "" -#: src/netshift/tabs/diagnostic/initController.ts:229 -#: src/netshift/tabs/diagnostic/initController.ts:233 -#: src/netshift/tabs/diagnostic/initController.ts:263 -#: src/netshift/tabs/diagnostic/initController.ts:267 -#: src/netshift/tabs/diagnostic/initController.ts:304 -#: src/netshift/tabs/diagnostic/initController.ts:308 #: src/netshift/tabs/manager/initController.ts:122 #: src/netshift/tabs/manager/initController.ts:132 #: src/netshift/tabs/manager/initController.ts:150 @@ -496,6 +498,12 @@ msgstr "" #: src/netshift/tabs/manager/initController.ts:192 #: src/netshift/tabs/manager/initController.ts:225 #: src/netshift/tabs/manager/initController.ts:229 +#: src/netshift/tabs/diagnostic/initController.ts:229 +#: src/netshift/tabs/diagnostic/initController.ts:233 +#: src/netshift/tabs/diagnostic/initController.ts:263 +#: src/netshift/tabs/diagnostic/initController.ts:267 +#: src/netshift/tabs/diagnostic/initController.ts:304 +#: src/netshift/tabs/diagnostic/initController.ts:308 msgid "Failed to execute!" msgstr "" @@ -506,7 +514,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:855 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:874 msgid "Fully Routed IPs" msgstr "" @@ -518,16 +526,12 @@ msgstr "" msgid "Global check" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:381 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 msgid "Global Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194 -msgid "Group by countries" -msgstr "" - #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:195 -msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" +msgid "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters." msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 @@ -538,7 +542,7 @@ msgstr "" msgid "HTTP error" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:207 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:226 msgid "Include servers by keyword" msgstr "" @@ -784,7 +788,7 @@ msgstr "" msgid "Issues detected" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:208 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:227 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" @@ -809,11 +813,11 @@ msgstr "" msgid "Lists & Updates" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:759 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:778 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:802 msgid "Local Subnet Lists" msgstr "" @@ -837,7 +841,7 @@ msgstr "" msgid "Mixed proxy and DNS resolution tuning" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:897 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:916 msgid "Mixed Proxy Port" msgstr "" @@ -845,7 +849,7 @@ msgstr "" msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:343 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -869,7 +873,7 @@ msgstr "" msgid "Network" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418 msgid "Network Interface" msgstr "" @@ -902,7 +906,15 @@ msgstr "" msgid "Not running" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:211 +msgid "Number of leading characters of each proxy name to group by." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:199 +msgid "Off" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:409 msgid "Only one section can be global at a time." msgstr "" @@ -951,6 +963,10 @@ msgstr "" msgid "Pending" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:210 +msgid "Prefix length" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:37 msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT." msgstr "" @@ -967,19 +983,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:807 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:826 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:850 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:932 msgid "Resolve real IP for routing" msgstr "" @@ -987,7 +1003,7 @@ msgstr "" msgid "Restart NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401 msgid "Route all unmatched traffic through this section's outbound." msgstr "" @@ -1035,7 +1051,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:566 msgid "Russia inside restrictions" msgstr "" @@ -1047,7 +1063,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:495 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514 msgid "Select a predefined list for routing" msgstr "" @@ -1067,11 +1083,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:419 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:491 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59 msgid "Select or enter DNS server address" msgstr "" @@ -1084,7 +1100,7 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:458 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:477 msgid "Select the DNS protocol type for the domain resolver" msgstr "" @@ -1108,7 +1124,7 @@ msgstr "" msgid "Selector" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:231 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:250 msgid "Selector Proxy Links" msgstr "" @@ -1178,20 +1194,20 @@ msgstr "" msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:856 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:875 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:827 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:832 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:851 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:760 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:779 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:803 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1220,6 +1236,10 @@ msgstr "" msgid "Subscription format" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194 +msgid "Subscription grouping" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:178 msgid "Subscription Update Interval" msgstr "" @@ -1260,8 +1280,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:682 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:613 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701 msgid "Text List" msgstr "" @@ -1269,15 +1289,15 @@ msgstr "" msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:303 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:318 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:351 msgid "The URL used to test server connectivity" msgstr "" @@ -1309,27 +1329,27 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:462 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:389 msgid "UDP over TCP" msgstr "" -#: src/netshift/tabs/diagnostic/initController.ts:39 -#: src/netshift/tabs/diagnostic/initController.ts:40 -#: src/netshift/tabs/diagnostic/initController.ts:41 -#: src/netshift/tabs/diagnostic/initController.ts:42 -#: src/netshift/tabs/diagnostic/initController.ts:43 -#: src/netshift/tabs/diagnostic/initController.ts:44 #: src/netshift/tabs/manager/initController.ts:37 #: src/netshift/tabs/manager/initController.ts:38 #: src/netshift/tabs/manager/initController.ts:39 #: src/netshift/tabs/manager/initController.ts:40 #: src/netshift/tabs/manager/initController.ts:41 #: src/netshift/tabs/manager/initController.ts:42 +#: src/netshift/tabs/diagnostic/initController.ts:39 +#: src/netshift/tabs/diagnostic/initController.ts:40 +#: src/netshift/tabs/diagnostic/initController.ts:41 +#: src/netshift/tabs/diagnostic/initController.ts:42 +#: src/netshift/tabs/diagnostic/initController.ts:43 +#: src/netshift/tabs/diagnostic/initController.ts:44 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7 msgid "unknown" msgstr "" @@ -1376,19 +1396,19 @@ msgstr "" msgid "URLTest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:302 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:257 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:276 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:331 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:350 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:298 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:317 msgid "URLTest Tolerance" msgstr "" @@ -1400,23 +1420,23 @@ msgstr "" msgid "Use this only when the router has working IPv6 connectivity." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:388 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:407 msgid "Use with Exclusion sections to route specific domains directly." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:621 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:690 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:709 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:717 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:736 msgid "User Subnets List" msgstr "" @@ -1442,8 +1462,8 @@ msgstr "" msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:749 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768 msgid "Validation errors:" msgstr "" @@ -1461,20 +1481,20 @@ msgid "Visit Wiki" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:258 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:251 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:277 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:530 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:568 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:403 msgid "When enabled, traffic not matching any other section's lists will go through this proxy." msgstr "" diff --git a/fe-app-netshift/locales/netshift.ru.po b/fe-app-netshift/locales/netshift.ru.po index b7b22a20..6c21c32c 100644 --- a/fe-app-netshift/locales/netshift.ru.po +++ b/fe-app-netshift/locales/netshift.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-11 20:31+0300\n" -"PO-Revision-Date: 2026-06-11 20:31+0300\n" +"POT-Creation-Date: 2026-06-12 08:52+0300\n" +"PO-Revision-Date: 2026-06-12 08:52+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -86,6 +86,12 @@ msgstr "Браузер не использует FakeIP" msgid "Browser is using FakeIP correctly" msgstr "Браузер использует FakeIP" +msgid "By country flag" +msgstr "По флагу страны" + +msgid "By name prefix" +msgstr "По префиксу имени" + msgid "Cache File Path" msgstr "Путь к файлу кэша" @@ -368,11 +374,8 @@ msgstr "Глобальная проверка" msgid "Global Proxy" msgstr "Глобальный прокси" -msgid "Group by countries" -msgstr "Группировать по странам" - -msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" -msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgid "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters." +msgstr "Группировать прокси из подписки в группы URLTest. «По флагу страны» использует эмодзи флага в начале каждого имени; «По префиксу имени» группирует по первым N символам." msgid "How often to automatically update the subscription" msgstr "Как часто автоматически обновлять подписку" @@ -641,6 +644,12 @@ msgstr "Не отвечает" msgid "Not running" msgstr "Не запущено" +msgid "Number of leading characters of each proxy name to group by." +msgstr "Количество начальных символов имени каждого прокси для группировки." + +msgid "Off" +msgstr "Выключено" + msgid "Only one section can be global at a time." msgstr "Только одна секция может быть глобальной одновременно." @@ -674,6 +683,9 @@ msgstr "Путь должен заканчиваться на cache.db" msgid "Pending" msgstr "Ожидает запуска" +msgid "Prefix length" +msgstr "Длина префикса" + msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT." msgstr "Переключатели протоколов, пути к файлам и журналирование. Включайте блокировку DoH только после переключения вышестоящего DNS на UDP или DoT." @@ -872,6 +884,9 @@ msgstr "Источники подписок, фильтры серверов и msgid "Subscription format" msgstr "Формат подписки" +msgid "Subscription grouping" +msgstr "Группировка подписки" + msgid "Subscription Update Interval" msgstr "Интервал обновления подписки" diff --git a/fe-app-netshift/src/netshift/types.ts b/fe-app-netshift/src/netshift/types.ts index 0575ca91..bae558b1 100644 --- a/fe-app-netshift/src/netshift/types.ts +++ b/fe-app-netshift/src/netshift/types.ts @@ -120,7 +120,8 @@ export namespace NetShift { subscription_url: string[]; subscription_format_preference?: 'auto' | 'xray' | 'singbox'; subscription_update_interval?: string; - subscription_group_by_countries?: '0' | '1'; + subscription_group_mode?: 'off' | 'country' | 'prefix'; + subscription_group_prefix_len?: string; subscription_filter_include_keywords?: string[]; subscription_filter_exclude_keywords?: string[]; } diff --git a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js index 56b2d0e0..a33fbffd 100644 --- a/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js +++ b/luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js @@ -189,17 +189,36 @@ function createSectionContent(section) { o = section.taboption( "subscription", - form.Flag, - "subscription_group_by_countries", - _("Group by countries"), + form.ListValue, + "subscription_group_mode", + _("Subscription grouping"), _( - "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag", + "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters.", ), ); - o.default = "0"; + o.value("off", _("Off")); + o.value("country", _("By country flag")); + o.value("prefix", _("By name prefix")); + o.default = "off"; o.rmempty = false; o.depends({ connection_type: "proxy", proxy_config_type: "subscription" }); + o = section.taboption( + "subscription", + form.Value, + "subscription_group_prefix_len", + _("Prefix length"), + _("Number of leading characters of each proxy name to group by."), + ); + o.default = "2"; + o.datatype = "and(uinteger,min(1))"; + o.rmempty = false; + o.depends({ + connection_type: "proxy", + proxy_config_type: "subscription", + subscription_group_mode: "prefix", + }); + o = section.taboption( "subscription", form.DynamicList, diff --git a/luci-app-netshift/po/ru/netshift.po b/luci-app-netshift/po/ru/netshift.po index b7b22a20..6c21c32c 100644 --- a/luci-app-netshift/po/ru/netshift.po +++ b/luci-app-netshift/po/ru/netshift.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-11 20:31+0300\n" -"PO-Revision-Date: 2026-06-11 20:31+0300\n" +"POT-Creation-Date: 2026-06-12 08:52+0300\n" +"PO-Revision-Date: 2026-06-12 08:52+0300\n" "Last-Translator: yandexru45\n" "Language-Team: none\n" "Language: ru\n" @@ -86,6 +86,12 @@ msgstr "Браузер не использует FakeIP" msgid "Browser is using FakeIP correctly" msgstr "Браузер использует FakeIP" +msgid "By country flag" +msgstr "По флагу страны" + +msgid "By name prefix" +msgstr "По префиксу имени" + msgid "Cache File Path" msgstr "Путь к файлу кэша" @@ -368,11 +374,8 @@ msgstr "Глобальная проверка" msgid "Global Proxy" msgstr "Глобальный прокси" -msgid "Group by countries" -msgstr "Группировать по странам" - -msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" -msgstr "Группирует прокси подписки по флагу страны в начале тега в отдельные URLTest-группы" +msgid "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters." +msgstr "Группировать прокси из подписки в группы URLTest. «По флагу страны» использует эмодзи флага в начале каждого имени; «По префиксу имени» группирует по первым N символам." msgid "How often to automatically update the subscription" msgstr "Как часто автоматически обновлять подписку" @@ -641,6 +644,12 @@ msgstr "Не отвечает" msgid "Not running" msgstr "Не запущено" +msgid "Number of leading characters of each proxy name to group by." +msgstr "Количество начальных символов имени каждого прокси для группировки." + +msgid "Off" +msgstr "Выключено" + msgid "Only one section can be global at a time." msgstr "Только одна секция может быть глобальной одновременно." @@ -674,6 +683,9 @@ msgstr "Путь должен заканчиваться на cache.db" msgid "Pending" msgstr "Ожидает запуска" +msgid "Prefix length" +msgstr "Длина префикса" + msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT." msgstr "Переключатели протоколов, пути к файлам и журналирование. Включайте блокировку DoH только после переключения вышестоящего DNS на UDP или DoT." @@ -872,6 +884,9 @@ msgstr "Источники подписок, фильтры серверов и msgid "Subscription format" msgstr "Формат подписки" +msgid "Subscription grouping" +msgstr "Группировка подписки" + msgid "Subscription Update Interval" msgstr "Интервал обновления подписки" diff --git a/luci-app-netshift/po/templates/netshift.pot b/luci-app-netshift/po/templates/netshift.pot index f9a0a843..df59ccb2 100644 --- a/luci-app-netshift/po/templates/netshift.pot +++ b/luci-app-netshift/po/templates/netshift.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: NETSHIFT\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-06-11 17:31+0300\n" -"PO-Revision-Date: 2026-06-11 17:31+0300\n" +"POT-Creation-Date: 2026-06-12 05:52+0300\n" +"PO-Revision-Date: 2026-06-12 05:52+0300\n" "Last-Translator: yandexru45 <>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -40,11 +40,11 @@ msgstr "" msgid "Add one or more subscription URLs to fetch proxy configurations from. All feeds are downloaded and merged." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:588 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:607 msgid "Add your own domains: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:676 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:695 msgid "Add your own subnets or IPs: choose Dynamic List (one per row) or Text List (free-form), or Disabled to skip" msgstr "" @@ -65,15 +65,15 @@ msgstr "" msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:371 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390 msgid "Applicable for SOCKS and Shadowsocks proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:667 msgid "At least one valid domain must be specified. Comments-only content is not allowed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:737 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:756 msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "" @@ -109,6 +109,14 @@ msgstr "" msgid "Browser is using FakeIP correctly" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:200 +msgid "By country flag" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:201 +msgid "By name prefix" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:517 msgid "Cache File Path" msgstr "" @@ -166,7 +174,7 @@ msgstr "" msgid "Close" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:494 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:513 msgid "Community Lists" msgstr "" @@ -219,11 +227,11 @@ msgstr "" msgid "Currently unavailable" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:587 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:606 msgid "Custom domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:675 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:694 msgid "Custom subnets" msgstr "" @@ -268,8 +276,8 @@ msgstr "" msgid "Disable the QUIC protocol to improve compatibility or fix issues with video streaming" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:592 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:680 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:611 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:699 msgid "Disabled" msgstr "" @@ -289,17 +297,17 @@ msgstr "" msgid "DNS outbound section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:460 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:479 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:48 msgid "DNS over HTTPS (DoH)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:461 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:480 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:49 msgid "DNS over TLS (DoT)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:457 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:476 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:45 msgid "DNS Protocol Type" msgstr "" @@ -308,7 +316,7 @@ msgstr "" msgid "DNS Rewrite TTL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:471 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:490 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:58 msgid "DNS Server" msgstr "" @@ -325,7 +333,7 @@ msgstr "" msgid "Domain and subnet lists that decide which traffic uses this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:446 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:465 msgid "Domain Resolver" msgstr "" @@ -355,12 +363,12 @@ msgstr "" msgid "Downloading all lists via specific Proxy/VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:220 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:239 msgid "Drop subscription servers whose name contains any of these keywords (case-insensitive)." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:593 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:612 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:700 msgid "Dynamic List" msgstr "" @@ -368,11 +376,11 @@ msgstr "" msgid "Enable autostart" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:447 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:466 msgid "Enable built-in DNS resolver for domains handled by this section" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:914 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:933 msgid "Enable DNS resolve to get real IP when routing" msgstr "" @@ -384,7 +392,7 @@ msgstr "" msgid "Enable IPv6 TProxy routing, IPv6 DNS inbound, and IPv6 FakeIP support." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:883 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:902 msgid "Enable Mixed Proxy" msgstr "" @@ -392,7 +400,7 @@ msgstr "" msgid "Enable Output Network Interface" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:884 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:903 msgid "Enable the mixed proxy, allowing this section to route traffic through both HTTP and SOCKS proxies" msgstr "" @@ -408,19 +416,19 @@ msgstr "" msgid "Enter complete outbound configuration in JSON format" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:630 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:649 msgid "Enter domain names separated by commas, spaces, or newlines. You can add comments using //" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:603 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:622 msgid "Enter domain names without protocols, e.g. example.com or sub.example.com" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:691 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:710 msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:287 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:306 msgid "Every 1 minute" msgstr "" @@ -432,7 +440,7 @@ msgstr "" msgid "Every 3 hours" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:288 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:307 msgid "Every 3 minutes" msgstr "" @@ -440,11 +448,11 @@ msgstr "" msgid "Every 30 minutes" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:286 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:305 msgid "Every 30 seconds" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:289 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:308 msgid "Every 5 minutes" msgstr "" @@ -468,7 +476,7 @@ msgstr "" msgid "Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:219 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:238 msgid "Exclude servers by keyword" msgstr "" @@ -482,12 +490,6 @@ msgstr "" msgid "Failed to copy!" msgstr "" -#: src/netshift/tabs/diagnostic/initController.ts:229 -#: src/netshift/tabs/diagnostic/initController.ts:233 -#: src/netshift/tabs/diagnostic/initController.ts:263 -#: src/netshift/tabs/diagnostic/initController.ts:267 -#: src/netshift/tabs/diagnostic/initController.ts:304 -#: src/netshift/tabs/diagnostic/initController.ts:308 #: src/netshift/tabs/manager/initController.ts:122 #: src/netshift/tabs/manager/initController.ts:132 #: src/netshift/tabs/manager/initController.ts:150 @@ -496,6 +498,12 @@ msgstr "" #: src/netshift/tabs/manager/initController.ts:192 #: src/netshift/tabs/manager/initController.ts:225 #: src/netshift/tabs/manager/initController.ts:229 +#: src/netshift/tabs/diagnostic/initController.ts:229 +#: src/netshift/tabs/diagnostic/initController.ts:233 +#: src/netshift/tabs/diagnostic/initController.ts:263 +#: src/netshift/tabs/diagnostic/initController.ts:267 +#: src/netshift/tabs/diagnostic/initController.ts:304 +#: src/netshift/tabs/diagnostic/initController.ts:308 msgid "Failed to execute!" msgstr "" @@ -506,7 +514,7 @@ msgstr "" msgid "Fastest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:855 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:874 msgid "Fully Routed IPs" msgstr "" @@ -518,16 +526,12 @@ msgstr "" msgid "Global check" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:381 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 msgid "Global Proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194 -msgid "Group by countries" -msgstr "" - #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:195 -msgid "Group subscription proxies into separate URLTest groups by the country flag at the start of each tag" +msgid "Group subscription proxies into URLTest groups. 'By country flag' uses the flag emoji at the start of each name; 'By name prefix' groups by the first N characters." msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:179 @@ -538,7 +542,7 @@ msgstr "" msgid "HTTP error" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:207 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:226 msgid "Include servers by keyword" msgstr "" @@ -784,7 +788,7 @@ msgstr "" msgid "Issues detected" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:208 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:227 msgid "Keep only subscription servers whose name contains at least one of these keywords (case-insensitive). Leave empty to keep all." msgstr "" @@ -809,11 +813,11 @@ msgstr "" msgid "Lists & Updates" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:759 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:778 msgid "Local Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:783 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:802 msgid "Local Subnet Lists" msgstr "" @@ -837,7 +841,7 @@ msgstr "" msgid "Mixed proxy and DNS resolution tuning" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:897 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:916 msgid "Mixed Proxy Port" msgstr "" @@ -845,7 +849,7 @@ msgstr "" msgid "Monitored Interfaces" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:324 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:343 msgid "Must be a number in the range of 50 - 1000" msgstr "" @@ -869,7 +873,7 @@ msgstr "" msgid "Network" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:399 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:418 msgid "Network Interface" msgstr "" @@ -902,7 +906,15 @@ msgstr "" msgid "Not running" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:390 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:211 +msgid "Number of leading characters of each proxy name to group by." +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:199 +msgid "Off" +msgstr "" + +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:409 msgid "Only one section can be global at a time." msgstr "" @@ -951,6 +963,10 @@ msgstr "" msgid "Pending" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:210 +msgid "Prefix length" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:37 msgid "Protocol toggles, file paths and logging. Block DoH only after switching upstream DNS to UDP or DoT." msgstr "" @@ -967,19 +983,19 @@ msgstr "" msgid "Proxy traffic is routed via FakeIP" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:528 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547 msgid "Regional options cannot be used together" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:807 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:826 msgid "Remote Domain Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:831 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:850 msgid "Remote Subnet Lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:913 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:932 msgid "Resolve real IP for routing" msgstr "" @@ -987,7 +1003,7 @@ msgstr "" msgid "Restart NetShift" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:382 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:401 msgid "Route all unmatched traffic through this section's outbound." msgstr "" @@ -1035,7 +1051,7 @@ msgstr "" msgid "Run Diagnostic" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:547 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:566 msgid "Russia inside restrictions" msgstr "" @@ -1047,7 +1063,7 @@ msgstr "" msgid "Sections" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:495 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:514 msgid "Select a predefined list for routing" msgstr "" @@ -1067,11 +1083,11 @@ msgstr "" msgid "Select how to configure the proxy" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:400 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:419 msgid "Select network interface for VPN connection" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:472 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:491 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:59 msgid "Select or enter DNS server address" msgstr "" @@ -1084,7 +1100,7 @@ msgstr "" msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:458 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:477 msgid "Select the DNS protocol type for the domain resolver" msgstr "" @@ -1108,7 +1124,7 @@ msgstr "" msgid "Selector" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:231 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:250 msgid "Selector Proxy Links" msgstr "" @@ -1178,20 +1194,20 @@ msgstr "" msgid "Specify a local IP address to be excluded from routing" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:856 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:875 msgid "Specify local IP addresses or subnets whose traffic will always be routed through the configured route" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:808 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:827 msgid "Specify remote URLs to download and use domain lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:832 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:851 msgid "Specify remote URLs to download and use subnet lists" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:760 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:784 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:779 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:803 msgid "Specify the path to the list file located on the router filesystem" msgstr "" @@ -1220,6 +1236,10 @@ msgstr "" msgid "Subscription format" msgstr "" +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:194 +msgid "Subscription grouping" +msgstr "" + #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:178 msgid "Subscription Update Interval" msgstr "" @@ -1260,8 +1280,8 @@ msgstr "" msgid "Test latency" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:594 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:682 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:613 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:701 msgid "Text List" msgstr "" @@ -1269,15 +1289,15 @@ msgstr "" msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:284 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:303 msgid "The interval between connectivity tests" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:299 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:318 msgid "The maximum difference in response times (ms) allowed when comparing servers" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:332 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:351 msgid "The URL used to test server connectivity" msgstr "" @@ -1309,27 +1329,27 @@ msgstr "" msgid "TTL value cannot be empty" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:462 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:481 #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/settings.js:50 msgid "UDP (Unprotected DNS)" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:370 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:389 msgid "UDP over TCP" msgstr "" -#: src/netshift/tabs/diagnostic/initController.ts:39 -#: src/netshift/tabs/diagnostic/initController.ts:40 -#: src/netshift/tabs/diagnostic/initController.ts:41 -#: src/netshift/tabs/diagnostic/initController.ts:42 -#: src/netshift/tabs/diagnostic/initController.ts:43 -#: src/netshift/tabs/diagnostic/initController.ts:44 #: src/netshift/tabs/manager/initController.ts:37 #: src/netshift/tabs/manager/initController.ts:38 #: src/netshift/tabs/manager/initController.ts:39 #: src/netshift/tabs/manager/initController.ts:40 #: src/netshift/tabs/manager/initController.ts:41 #: src/netshift/tabs/manager/initController.ts:42 +#: src/netshift/tabs/diagnostic/initController.ts:39 +#: src/netshift/tabs/diagnostic/initController.ts:40 +#: src/netshift/tabs/diagnostic/initController.ts:41 +#: src/netshift/tabs/diagnostic/initController.ts:42 +#: src/netshift/tabs/diagnostic/initController.ts:43 +#: src/netshift/tabs/diagnostic/initController.ts:44 #: src/netshift/tabs/diagnostic/helpers/getNetshiftVersionRow.ts:7 msgid "unknown" msgstr "" @@ -1376,19 +1396,19 @@ msgstr "" msgid "URLTest" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:283 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:302 msgid "URLTest Check Interval" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:257 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:276 msgid "URLTest Proxy Links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:331 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:350 msgid "URLTest Testing URL" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:298 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:317 msgid "URLTest Tolerance" msgstr "" @@ -1400,23 +1420,23 @@ msgstr "" msgid "Use this only when the router has working IPv6 connectivity." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:388 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:407 msgid "Use with Exclusion sections to route specific domains directly." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:602 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:621 msgid "User Domains" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:629 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:648 msgid "User Domains List" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:690 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:709 msgid "User Subnets" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:717 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:736 msgid "User Subnets List" msgstr "" @@ -1442,8 +1462,8 @@ msgstr "" msgid "Valid" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:662 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:749 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:681 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:768 msgid "Validation errors:" msgstr "" @@ -1461,20 +1481,20 @@ msgid "Visit Wiki" msgstr "" #: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:67 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:232 -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:258 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:251 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:277 msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links" msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:530 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549 msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:549 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:568 msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." msgstr "" -#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:384 +#: ../luci-app-netshift/htdocs/luci-static/resources/view/netshift/section.js:403 msgid "When enabled, traffic not matching any other section's lists will go through this proxy." msgstr "" diff --git a/netshift/files/etc/config/netshift b/netshift/files/etc/config/netshift index bcda891d..adeb9c98 100644 --- a/netshift/files/etc/config/netshift +++ b/netshift/files/etc/config/netshift @@ -70,6 +70,12 @@ config section 'main' # # is set (an explicit UA always wins). # #option subscription_format_preference 'auto' # option subscription_update_interval '1h' +# # Node grouping: off | country | prefix. 'country' clusters by leading +# # flag emoji; 'prefix' clusters by the first N codepoints of each node +# # name (N = subscription_group_prefix_len). subscription_group_mode +# # outranks the legacy subscription_group_by_countries boolean below. +# #option subscription_group_mode 'off' +# #option subscription_group_prefix_len '2' # #option subscription_group_by_countries '0' # #option urltest_check_interval '3m' # #option urltest_tolerance '50' diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index 0a488a1d..c5d9395c 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -2141,10 +2141,25 @@ sing_box_get_unique_outbound_tag() { echo "$candidate" } -sing_box_build_subscription_country_groups() { +# Mode-aware subscription group-key builder (task-044). Generalizes the former +# country-only grouper into off/country/prefix modes. Returns the shape +# {group_order: [...], groups: {key: [tags]}, ungrouped: [...]}. +# - mode=country: byte-identical to the legacy flag extractor (regional- +# indicator gate; non-flag tags -> ungrouped). +# - mode=prefix: group key = first N codepoints of the tag (N = $3). A tag +# shorter than N codepoints keys by its WHOLE tag (still groups with +# identical short tags); an empty tag -> ungrouped. $3 is coerced via +# tonumber, floored to 1; bad/0/empty -> default 2. +# No Oniguruma jq (explode/implode/slice/index/reduce only). +sing_box_build_subscription_groups() { local subscription_outbound_tags_json="$1" + local mode="$2" + local prefix_len="$3" - printf '%s' "$subscription_outbound_tags_json" | jq -c ' + printf '%s' "$subscription_outbound_tags_json" | jq -c \ + --arg mode "$mode" \ + --arg prefix_len "$prefix_len" \ + --argjson default_len "$SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN" ' def is_regional_indicator: . >= 127462 and . <= 127487; def extract_country_flag: (. | explode) as $codepoints @@ -2154,17 +2169,33 @@ sing_box_build_subscription_country_groups() { then ($codepoints[0:2] | implode) else "" end; + # extract_prefix($n): group key = first $n codepoints, or "" (ungrouped) + # for an empty tag. A short tag keys by its whole self. + def extract_prefix($n): + (. | explode) as $codepoints + | if ($codepoints | length) == 0 + then "" + else ($codepoints[0:$n] | implode) + end; - (if type == "array" then . else [] end) as $tags + # Effective prefix length: coerce, floor to 1, fall back to default. + (try ($prefix_len | tonumber) catch $default_len) as $raw_len + | (if ($raw_len | type) != "number" or $raw_len < 1 + then $default_len + else ($raw_len | floor) + end) as $n + + | (if type == "array" then . else [] end) as $tags | reduce $tags[] as $tag ( - {country_order: [], country_groups: {}, ungrouped: []}; - ($tag | extract_country_flag) as $country_flag - | if $country_flag == "" then + {group_order: [], groups: {}, ungrouped: []}; + (if $mode == "prefix" then ($tag | extract_prefix($n)) + else ($tag | extract_country_flag) end) as $key + | if $key == "" then .ungrouped += [$tag] else - .country_groups[$country_flag] = ((.country_groups[$country_flag] // []) + [$tag]) - | if (.country_order | index($country_flag)) == null then - .country_order += [$country_flag] + .groups[$key] = ((.groups[$key] // []) + [$tag]) + | if (.group_order | index($key)) == null then + .group_order += [$key] else . end @@ -2330,7 +2361,7 @@ configure_outbound_handler() { log "Detected proxy configuration type: subscription" "debug" local subscription_urls_tmp subscription_url subscription_url_count urltest_tag selector_tag \ urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance \ - urltest_testing_url subscription_group_by_countries subscription_group_by_countries_raw \ + urltest_testing_url group_mode group_mode_raw prefix_len prefix_len_raw legacy_group_raw \ subscription_outbound_tags_json service_proxy_address subscription_ready \ subscription_filter_include_keywords_json subscription_filter_exclude_keywords_json \ subscription_keyword_filter_active urlhash subscription_json_path subscription_url_cache_path \ @@ -2340,20 +2371,55 @@ configure_outbound_handler() { config_get urltest_check_interval "$section" "urltest_check_interval" "3m" config_get urltest_tolerance "$section" "urltest_tolerance" 50 config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204" - config_get subscription_group_by_countries_raw "$section" "subscription_group_by_countries" "" - if [ -z "$subscription_group_by_countries_raw" ]; then - # Backward-compatible alias in case custom builds used another key - config_get subscription_group_by_countries_raw "$section" "group_by_countries" "" + # Grouping mode (task-044): off | country | prefix. The new + # subscription_group_mode option OUTRANKS the legacy boolean. When + # the new option is absent we fall back to the legacy boolean + # subscription_group_by_countries (and its older alias + # group_by_countries): truthy => country, else off. + config_get group_mode_raw "$section" "subscription_group_mode" "" + config_get prefix_len_raw "$section" "subscription_group_prefix_len" "$SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN" + + if [ -z "$group_mode_raw" ]; then + config_get legacy_group_raw "$section" "subscription_group_by_countries" "" + if [ -z "$legacy_group_raw" ]; then + config_get legacy_group_raw "$section" "group_by_countries" "" + fi + if is_truthy_option "$legacy_group_raw"; then + group_mode="country" + else + group_mode="off" + fi + else + case "$group_mode_raw" in + off | country | prefix) + group_mode="$group_mode_raw" + ;; + *) + group_mode="off" + ;; + esac fi - if is_truthy_option "$subscription_group_by_countries_raw"; then - subscription_group_by_countries=1 + # Sanitize the prefix length: positive integer only, else default. + case "$prefix_len_raw" in + '' | *[!0-9]*) + prefix_len="$SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN" + ;; + *) + if [ "$prefix_len_raw" -ge 1 ] 2>/dev/null; then + prefix_len="$prefix_len_raw" + else + prefix_len="$SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN" + fi + ;; + esac + + if [ "$group_mode" = "prefix" ]; then + log "Subscription grouping for section '$section': mode=$group_mode, prefix_len=$prefix_len" "debug" else - subscription_group_by_countries=0 + log "Subscription grouping for section '$section': mode=$group_mode" "debug" fi - log "Subscription country grouping for section '$section': raw='${subscription_group_by_countries_raw:-<empty>}', enabled=$subscription_group_by_countries" "debug" - # Keyword whitelist/blacklist filtering of subscription nodes by # display name. Both are optional UCI lists of opaque match strings. subscription_filter_include_keywords_json="$(build_subscription_filter_keywords_json "$section" "subscription_filter_include_keywords")" @@ -2519,36 +2585,50 @@ configure_outbound_handler() { subscription_outbound_tags_json="$(comma_string_to_json_array "$SUBSCRIPTION_OUTBOUND_TAGS")" fi - if [ "$subscription_group_by_countries" -eq 1 ]; then - local grouping_json country_flag country_group_outbounds country_group_tag \ + if [ "$group_mode" != "off" ]; then + local grouping_json group_key group_outbounds group_tag group_keys_tmp \ selector_outbounds_json selector_default ungrouped_outbounds_json grouped_count ungrouped_count - grouping_json="$(sing_box_build_subscription_country_groups "$subscription_outbound_tags_json")" + grouping_json="$(sing_box_build_subscription_groups "$subscription_outbound_tags_json" "$group_mode" "$prefix_len")" if [ -z "$grouping_json" ]; then log "Failed to build grouped subscription outbounds for section '$section'. Aborted." "fatal" exit 1 fi - grouped_count="$(echo "$grouping_json" | jq -r '.country_order | length' 2>/dev/null)" + grouped_count="$(echo "$grouping_json" | jq -r '.group_order | length' 2>/dev/null)" ungrouped_count="$(echo "$grouping_json" | jq -r '.ungrouped | length' 2>/dev/null)" - log "Country grouping prepared for section '$section': groups=$grouped_count, ungrouped=$ungrouped_count" "debug" + log "Subscription grouping prepared for section '$section' (mode=$group_mode): groups=$grouped_count, ungrouped=$ungrouped_count" "debug" selector_outbounds_json="[]" - for country_flag in $(echo "$grouping_json" | jq -r '.country_order[]' 2>/dev/null); do - country_group_outbounds="$(echo "$grouping_json" | jq -c --arg country_flag "$country_flag" '.country_groups[$country_flag] // []' 2>/dev/null)" - if [ -z "$country_group_outbounds" ] || [ "$country_group_outbounds" = "[]" ]; then + # Iterate group keys WITHOUT word-splitting: prefix-mode keys + # can legitimately contain spaces (e.g. a "letter+space" + # prefix), which a `for k in $(...)` loop would shatter. Use + # the same mktemp + `while read < file` pattern as the URL + # loop above so the body runs in the CURRENT shell and its + # mutations to $config / $selector_outbounds_json survive + # (a `... | while read` body would run in a subshell and lose + # them). One key per line; group keys never contain newlines. + group_keys_tmp="$(mktemp "${TMPDIR:-/tmp}/netshift-cfg-groupkeys.XXXXXX")" || { + log "Failed to enumerate subscription group keys for section '$section'. Aborted." "fatal" + exit 1 + } + echo "$grouping_json" | jq -r '.group_order[]' 2>/dev/null > "$group_keys_tmp" + while IFS= read -r group_key || [ -n "$group_key" ]; do + group_outbounds="$(echo "$grouping_json" | jq -c --arg group_key "$group_key" '.groups[$group_key] // []' 2>/dev/null)" + if [ -z "$group_outbounds" ] || [ "$group_outbounds" = "[]" ]; then continue fi - country_group_tag="$(sing_box_get_unique_outbound_tag "$config" "$country_flag Fastest")" - config="$(sing_box_cm_add_urltest_outbound "$config" "$country_group_tag" "$country_group_outbounds" \ + group_tag="$(sing_box_get_unique_outbound_tag "$config" "$group_key Fastest")" + config="$(sing_box_cm_add_urltest_outbound "$config" "$group_tag" "$group_outbounds" \ "$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")" selector_outbounds_json=$( - printf '%s' "$selector_outbounds_json" | jq -ac --arg tag "$country_group_tag" '. + [$tag]' 2>/dev/null + printf '%s' "$selector_outbounds_json" | jq -ac --arg tag "$group_tag" '. + [$tag]' 2>/dev/null ) - done + done < "$group_keys_tmp" + rm -f "$group_keys_tmp" if [ -z "$selector_outbounds_json" ]; then selector_outbounds_json="[]" diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 5c833901..3d9063a2 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -127,6 +127,9 @@ SB_SERVICE_MIXED_INBOUND_ADDRESS="127.0.0.1" SB_SERVICE_MIXED_INBOUND_PORT=4534 # Outbounds SB_DIRECT_OUTBOUND_TAG="direct-out" +# Subscription grouping (task-044). Default codepoint count for prefix-mode +# grouping when subscription_group_prefix_len is unset/invalid. +SUBSCRIPTION_GROUP_DEFAULT_PREFIX_LEN=2 # Route SB_REJECT_RULE_TAG="reject-rule-tag" SB_EXCLUSION_RULE_TAG="exclusion-rule-tag" diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index f4506e1e..76a58aa5 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -2046,6 +2046,124 @@ JQEOF fail "Country flag grouping wrong: got $grouped grouped, $ungrouped ungrouped" fi + # ── Universal grouper (task-044): prefix mode over synthetic tags ────── + # Mirror the shipped sing_box_build_subscription_groups extractor in an + # inline .jq so we exercise the exact mode-aware key logic without real + # node names. Synthetic tags only. + local grouper_filter_file="/tmp/netshift-grouper-filter-$$.jq" + cat > "$grouper_filter_file" << 'JQEOF' +def is_regional_indicator: . >= 127462 and . <= 127487; +def extract_country_flag: + (. | explode) as $codepoints + | if ($codepoints | length) >= 2 + and ($codepoints[0] | is_regional_indicator) + and ($codepoints[1] | is_regional_indicator) + then ($codepoints[0:2] | implode) + else "" end; +def extract_prefix($n): + (. | explode) as $codepoints + | if ($codepoints | length) == 0 then "" + else ($codepoints[0:$n] | implode) end; +(try ($prefix_len | tonumber) catch $default_len) as $raw_len +| (if ($raw_len | type) != "number" or $raw_len < 1 + then $default_len else ($raw_len | floor) end) as $n +| (if type == "array" then . else [] end) as $tags +| reduce $tags[] as $tag ( + {group_order: [], groups: {}, ungrouped: []}; + (if $mode == "prefix" then ($tag | extract_prefix($n)) + else ($tag | extract_country_flag) end) as $key + | if $key == "" then .ungrouped += [$tag] + else + .groups[$key] = ((.groups[$key] // []) + [$tag]) + | if (.group_order | index($key)) == null + then .group_order += [$key] else . end + end + ) +JQEOF + + # prefix-len 2 over synthetic tags: US(2), DE(1), short tag X keyed as X(1) + local prefix_synth prefix_result prefix_groups prefix_us prefix_de prefix_x prefix_ungrouped + prefix_synth='["US-01","US-02","DE-01","X"]' + prefix_result=$(echo "$prefix_synth" | jq -c \ + --arg mode "prefix" --arg prefix_len "2" --argjson default_len 2 \ + -f "$grouper_filter_file") + prefix_groups=$(echo "$prefix_result" | jq -r '.group_order | length') + prefix_us=$(echo "$prefix_result" | jq -r '.groups["US"] | length') + prefix_de=$(echo "$prefix_result" | jq -r '.groups["DE"] | length') + prefix_x=$(echo "$prefix_result" | jq -r '.groups["X"] | length') + prefix_ungrouped=$(echo "$prefix_result" | jq -r '.ungrouped | length') + if [ "$prefix_groups" -eq 3 ] && [ "$prefix_us" -eq 2 ] && \ + [ "$prefix_de" -eq 1 ] && [ "$prefix_x" -eq 1 ] && [ "$prefix_ungrouped" -eq 0 ]; then + pass "Prefix grouping (len 2): US=$prefix_us DE=$prefix_de X=$prefix_x, groups=$prefix_groups, ungrouped=$prefix_ungrouped" + else + fail "Prefix grouping (len 2) wrong: US=$prefix_us DE=$prefix_de X=$prefix_x groups=$prefix_groups ungrouped=$prefix_ungrouped" + fi + + # Consistency: prefix-len 2 over flag-only tags == country grouping. A + # flag is exactly 2 codepoints, so prefix-len-2 keys each flag tag by its + # leading flag — identical group keys to country mode (the non-flag + # "no-flag" element of country_test is excluded here, since under prefix + # mode it would group by its first 2 chars instead of going ungrouped). + local flag_only_test prefix_flag_result country_flag_result + local flag_only_file="/tmp/netshift-flagonly-$$.jq" + cat > "$flag_only_file" << 'JQEOF' +def flag($l1; $l2): ([127462 + $l1, 127462 + $l2] | implode); +[(flag(3; 4) + " Frankfurt"), (flag(20; 18) + " New York"), (flag(13; 11) + " Amsterdam"), (flag(9; 15) + " Tokyo")] +JQEOF + flag_only_test=$(jq -cn -f "$flag_only_file") + rm -f "$flag_only_file" + prefix_flag_result=$(echo "$flag_only_test" | jq -c \ + --arg mode "prefix" --arg prefix_len "2" --argjson default_len 2 \ + -f "$grouper_filter_file") + country_flag_result=$(echo "$flag_only_test" | jq -c \ + --arg mode "country" --arg prefix_len "2" --argjson default_len 2 \ + -f "$grouper_filter_file") + if [ "$prefix_flag_result" = "$country_flag_result" ]; then + pass "Prefix grouping consistency vs country: identical grouping over flag tags" + else + fail "Prefix grouping consistency vs country wrong: prefix=$prefix_flag_result country=$country_flag_result" + fi + + # Bad/empty len must NOT crash; falls back to default 2. + local prefix_badlen_result prefix_badlen_us prefix_emptylen_result prefix_emptylen_us + prefix_badlen_result=$(echo "$prefix_synth" | jq -c \ + --arg mode "prefix" --arg prefix_len "abc" --argjson default_len 2 \ + -f "$grouper_filter_file" 2>/dev/null) + prefix_badlen_us=$(echo "$prefix_badlen_result" | jq -r '.groups["US"] | length' 2>/dev/null) + prefix_emptylen_result=$(echo "$prefix_synth" | jq -c \ + --arg mode "prefix" --arg prefix_len "" --argjson default_len 2 \ + -f "$grouper_filter_file" 2>/dev/null) + prefix_emptylen_us=$(echo "$prefix_emptylen_result" | jq -r '.groups["US"] | length' 2>/dev/null) + if [ "$prefix_badlen_us" = "2" ] && [ "$prefix_emptylen_us" = "2" ]; then + pass "Prefix grouping bad/empty len falls back to 2 (no crash)" + else + fail "Prefix grouping len fallback wrong: bad-len US=$prefix_badlen_us empty-len US=$prefix_emptylen_us" + fi + + # Space-containing prefix keys must group correctly (regression for the + # word-splitting bug: a `for k in $(...)` loop over group keys shatters a + # key like "A " (letter+space) — the current-shell `while read < file` + # loop in the subscription branch preserves it). Synthetic tags only. + local space_synth space_result space_groups space_a space_b space_ungrouped space_keys + space_synth='["A 1","A 2","B 9"]' + space_result=$(echo "$space_synth" | jq -c \ + --arg mode "prefix" --arg prefix_len "2" --argjson default_len 2 \ + -f "$grouper_filter_file") + rm -f "$grouper_filter_file" + space_groups=$(echo "$space_result" | jq -r '.group_order | length') + space_a=$(echo "$space_result" | jq -r '.groups["A "] | length') + space_b=$(echo "$space_result" | jq -r '.groups["B "] | length') + space_ungrouped=$(echo "$space_result" | jq -r '.ungrouped | length') + # The group_order keys must be exactly "A " and "B " (each ends with a + # space) — confirms the space is preserved, not split away. + space_keys=$(echo "$space_result" | jq -c '.group_order') + if [ "$space_groups" -eq 2 ] && [ "$space_a" = "2" ] && [ "$space_b" = "1" ] && \ + [ "$space_ungrouped" -eq 0 ] && [ "$space_keys" = '["A ","B "]' ]; then + pass "Prefix grouping space-key: 'A '=$space_a 'B '=$space_b, groups=$space_groups, ungrouped=$space_ungrouped" + else + fail "Prefix grouping space-key wrong: 'A '=$space_a 'B '=$space_b groups=$space_groups ungrouped=$space_ungrouped keys=$space_keys" + fi + # ── Fallback Subscription Normalizer (helpers.sh) ─────────────── # Exercise normalize_subscription_to_singbox end-to-end against the # real libs. The facade hardcodes NETSHIFT_LIB=/usr/lib/netshift, so we From 996eb7ab2950145e3972455d73f5252b171f1b63 Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Fri, 12 Jun 2026 09:37:35 +0300 Subject: [PATCH 72/75] =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B0=20gzip=20=D0=B2=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 38 ++++++++++ .../memory/shell-backend-developer.md | 37 ++++++++++ netshift/files/usr/bin/netshift | 10 +++ netshift/files/usr/lib/helpers.sh | 52 ++++++++++++++ tests/entrypoint.sh | 71 +++++++++++++++++++ 5 files changed, 208 insertions(+) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 1575e203..3e1f5691 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -1191,3 +1191,41 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> session (frontend stopped before section.js; reviewer stopped mid-analysis) — ALWAYS verify on-disk state + run/inspect gates yourself rather than trusting a truncated "done". + +## task-046 gzip subscription bodies — issue #13 (2026-06-12) + +- ISSUE #13: a subscription panel sometimes returns a gzip-compressed body; + NetShift parsed the binary as text -> all nodes skipped -> "No subscription + User-Agent candidate produced valid outbounds". Reporter's workaround + gzip-decompressed only inside normalize_subscription_to_singbox — INSUFFICIENT + because validate_subscription_file runs BEFORE normalize and also chokes on a + gzipped sing-box JSON. Correct seam = DOWNLOAD path (decompress once, both + consumers see text). +- DEVICE FACTS (verified, OWRT 24.10 + 25.12): gzip/gunzip/zcat present + (busybox); zstd/unzstd ABSENT (would need a new DEPENDS pkg). wget sends NO + Accept-Encoding and does NOT transparently decompress, so gzip = server + unconditionally compressing. gzip magic 1f 8b; `gzip -dc` on non-gzip rc=1. + NO od/hexdump/xxd on device. +- OPERATOR DECISIONS: gzip ONLY (not zstd/deflate — separate future task if + panels actually send them); detect at download time; + NUL-byte guard (reject + still-binary body to next UA). Full backend+smoke. +- FIX (task-046, APPROVED W/ CONDITIONS, smoke 174/0): two helpers.sh helpers + (modeled on convert_crlf_to_lf, mktemp+mv, all local, best-effort return 0): + * maybe_gunzip_subscription_file <f>: ATTEMPT-DECOMPRESS detector (NO od) — + `gzip -dc f > tmp` accept ONLY if rc=0 AND non-empty AND NUL-free, else leave + original byte-identical (plain text makes gzip -dc rc!=0 -> untouched, can + never corrupt text). This doubles as the gzip detector, avoids byte-fiddling. + * subscription_body_is_binary <f>: NUL detector via `wc -c < f` vs + `tr -d '\000' < f | wc -c` (counts differ -> NUL). Portable, no special grep + flag, no od. + Wired into download_subscription_into_cache per-UA while-read loop AFTER + download_subscription, BEFORE validate (netshift:585-594): gunzip then if + binary -> warn + continue to next UA (same control flow as a validation fail). + file_size/debug log MOVED to after decompress so size reflects decompressed + body. NO wget/Accept-Encoding change, NO Makefile DEPENDS change. +- REUSABLE PATTERNS: file-transform helper = mktemp+transform+mv-on-success/ + rm-on-fail (convert_crlf_to_lf is the template). NUL/binary detect without od = + wc-vs-tr-d-NUL byte-count. attempt-decompress is a clean od-free magic detector. +- CONDITION (M1) is commit-hygiene only: keep the pre-existing unrelated + .opencode/agent/*.md churn (model rename + `bash "*": ask`->`allow`) OUT of the + commit; the permission loosening is a separate human decision. diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 9b3e1ec9..71361b6e 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -1423,3 +1423,40 @@ findings; keep under ~200 lines. harness counting quirk, not a regression; all four `fb-caseI-xraypref-*` lines print :OK in both runs). No ports/marks/paths/schema touched; runtime contract intact. + +## task-046 — gzip subscription body decompress + NUL guard (issue #13) + +- Some panels return a gzip-compressed HTTP body unconditionally; busybox wget + does NOT transparently decompress and NetShift sends no Accept-Encoding, so the + raw bytes are binary and validate/normalize choke ("No subscription User-Agent + candidate produced valid outbounds"). +- Added two best-effort helpers to `helpers.sh` (next to `convert_crlf_to_lf`): + - `maybe_gunzip_subscription_file <f>`: attempt-based detection (no + od/hexdump/xxd — none on device). `gzip -dc` (busybox built-in) into a + mktemp; accept ONLY if rc=0 AND result non-empty AND NUL-free, then `mv` into + place (else `rm`). `gzip -dc` on plain text returns rc!=0 cleanly, so plain + text is left byte-for-byte untouched — never corrupts text. Always returns 0. + - `subscription_body_is_binary <f>`: returns 0 (true) if file has a NUL byte. + Busybox-safe, no od: compare `wc -c < f` to `tr -d '\000' < f | wc -c` (differ + ⇒ had NUL). All vars local. +- Wired in `bin/netshift` `download_subscription_into_cache` right after a + successful `download_subscription` (now ~:590-591), BEFORE + `validate_subscription_file`: call `maybe_gunzip_subscription_file`, then if + `subscription_body_is_binary` log a warn and `continue` (inside the per-UA + `while read` loop → falls to next UA, same flow as a validation failure). The + file_size/debug log stays AFTER so the logged size reflects the decompressed + body. mv/cache-persist below unchanged. +- NO Accept-Encoding/wget change, NO Makefile DEPENDS change (gzip/gunzip/zcat + are busybox built-ins), NO schema/ports/marks/frontend. zstd/unzstd are NOT on + device — gzip-only by design; deflate/zstd are a future task if panels send + them. +- Smoke: extended the `test_subscription` fb harness (sources helpers via the + facade) with synthetic-only fixtures: caseP gzip→text (cmp byte-equal), + text-passthrough (cmp unchanged), gzip→validate (validate_subscription_file + passes); caseQ `printf 'abc\000def'`→binary true, plain text→false. The smoke + container has gzip (tests/Dockerfile apk add gzip). No new test_* fn — folded + into existing test_subscription, so no main()/case/usage registration needed. +- Gates: shellcheck -S error clean on bin + all libs + install.sh; `smoke-tests + all` 174 passed / 0 failed (aggregate counter folds the 5 new tokens into the + test_subscription header group — count unchanged, the 5 fb-caseP/Q :OK lines + print explicitly in the `subscription` run). diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index c5d9395c..ca2de0eb 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -583,6 +583,16 @@ download_subscription_into_cache() { continue fi + # Some panels return a gzip-compressed body unconditionally (we send no + # Accept-Encoding and busybox wget does not transparently decompress). + # Decompress in place before validate/normalize so every consumer sees + # text. Best-effort: a plain-text body is left untouched. + maybe_gunzip_subscription_file "$tmpfile" + if subscription_body_is_binary "$tmpfile"; then + log "Subscription body for section '$section' is binary/undecodable after gzip handling (not text, not gzip) with User-Agent '$effective_user_agent'; trying next candidate" "warn" + continue + fi + file_size="$(wc -c < "$tmpfile" 2>/dev/null | tr -d ' ')" log "Downloaded subscription body for section '$section': bytes=${file_size:-unknown}, User-Agent='$effective_user_agent'" "debug" diff --git a/netshift/files/usr/lib/helpers.sh b/netshift/files/usr/lib/helpers.sh index 93e5dbe7..36ec2b64 100644 --- a/netshift/files/usr/lib/helpers.sh +++ b/netshift/files/usr/lib/helpers.sh @@ -572,6 +572,58 @@ convert_crlf_to_lf() { fi } +# Best-effort, in-place gzip decompression of a downloaded subscription body. +# +# Some panels unconditionally return a gzip-compressed HTTP body (busybox wget +# does NOT transparently decompress and we send no Accept-Encoding), so the raw +# bytes are binary and every downstream consumer (validate/normalize) chokes. +# This decompresses once at download time so all consumers see text. +# +# Detection is attempt-based (no od/hexdump/xxd, none of which exist on device): +# we try `gzip -dc` (busybox built-in) into a temp file and accept the result +# ONLY if (a) gzip returned 0, (b) the result is non-empty, and (c) the result +# is NUL-free. gzip -dc on plain-text input returns rc!=0 cleanly, so a +# plain-text body is left byte-for-byte untouched; this can never corrupt text. +# Modeled on convert_crlf_to_lf: mktemp -> transform -> mv on success / rm on +# failure. Best-effort: always returns 0 (never aborts the caller). +maybe_gunzip_subscription_file() { + local filepath="$1" + local tmpfile + + [ -s "$filepath" ] || return 0 + + tmpfile=$(mktemp) + if gzip -dc "$filepath" > "$tmpfile" 2>/dev/null && + [ -s "$tmpfile" ] && + ! subscription_body_is_binary "$tmpfile"; then + log "Decompressed gzip subscription body for '$filepath'" "debug" + mv "$tmpfile" "$filepath" + else + rm -f "$tmpfile" + fi + + return 0 +} + +# Returns 0 (true) if the file contains at least one NUL byte (i.e. it is +# binary / undecodable, not text). Busybox-safe, no od/hexdump/xxd: `tr -d` +# strips NUL bytes and we compare the resulting byte count to the original; a +# difference means a NUL was present. All vars local. +subscription_body_is_binary() { + local filepath="$1" + local raw_count stripped_count + + [ -s "$filepath" ] || return 1 + + raw_count="$(wc -c < "$filepath" 2>/dev/null | tr -d ' ')" + stripped_count="$(tr -d '\000' < "$filepath" 2>/dev/null | wc -c 2>/dev/null | tr -d ' ')" + + [ -n "$raw_count" ] || raw_count=0 + [ -n "$stripped_count" ] || stripped_count=0 + + [ "$raw_count" != "$stripped_count" ] +} + ####################################### # Parses a whitespace-separated string, validates items as either domains # or IPv4 addresses/subnets, and returns a comma-separated string of valid items. diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 76a58aa5..c220f26a 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -3123,6 +3123,77 @@ else fi rm -f "$caseO_in" "$caseO_out" +# ── CASE P: gzip subscription body handling (task-046, issue #13) ─── +# All synthetic fixtures (no real node/panel data). The smoke container +# installs gzip (tests/Dockerfile), so we can build a real gzip body in-test. +if command -v gzip > /dev/null 2>&1; then + # (P1) gzip -> text: gzip a tiny known-good plain body, run the helper, + # assert the result is the original plain text (byte-equal). + caseP_plain="/tmp/netshift-fb-caseP-plain-$$.txt" + caseP_gz="/tmp/netshift-fb-caseP-gz-$$.bin" + printf 'vless://33333333-3333-3333-3333-333333333333@example.com:443#P\n' > "$caseP_plain" + gzip -c "$caseP_plain" > "$caseP_gz" + maybe_gunzip_subscription_file "$caseP_gz" + if cmp -s "$caseP_gz" "$caseP_plain"; then + echo 'fb-caseP-gzip-to-text:OK' + else + echo 'fb-caseP-gzip-to-text:FAIL' + fi + rm -f "$caseP_gz" + + # (P2) non-gzip passthrough: a plain-text body is UNCHANGED (no spurious + # gunzip, no corruption). + caseP_pt="/tmp/netshift-fb-caseP-pt-$$.txt" + caseP_pt_ref="/tmp/netshift-fb-caseP-pt-ref-$$.txt" + printf 'just plain text, definitely not gzip\nsecond line\n' > "$caseP_pt" + cp "$caseP_pt" "$caseP_pt_ref" + maybe_gunzip_subscription_file "$caseP_pt" + if cmp -s "$caseP_pt" "$caseP_pt_ref"; then + echo 'fb-caseP-text-passthrough:OK' + else + echo 'fb-caseP-text-passthrough:FAIL' + fi + rm -f "$caseP_pt" "$caseP_pt_ref" "$caseP_plain" + + # (P3) whole-chain: gzip a small synthetic VALID sing-box JSON, run the + # helper, then validate_subscription_file -> must now VALIDATE. + caseP_json="/tmp/netshift-fb-caseP-json-$$.json" + caseP_jgz="/tmp/netshift-fb-caseP-jgz-$$.bin" + cat > "$caseP_json" << 'PJSON' +{"outbounds":[{"type":"shadowsocks","tag":"P-node","server":"example.com","server_port":443,"method":"aes-256-gcm","password":"p"}]} +PJSON + gzip -c "$caseP_json" > "$caseP_jgz" + maybe_gunzip_subscription_file "$caseP_jgz" + if validate_subscription_file "$caseP_jgz"; then + echo 'fb-caseP-gzip-then-validate:OK' + else + echo 'fb-caseP-gzip-then-validate:FAIL' + fi + rm -f "$caseP_json" "$caseP_jgz" +else + echo 'fb-caseP-gzip-to-text:SKIP' + echo 'fb-caseP-text-passthrough:SKIP' + echo 'fb-caseP-gzip-then-validate:SKIP' +fi + +# ── CASE Q: NUL-byte binary detector (task-046) ───────────────────── +# A body with an embedded NUL is binary (true); plain text is not (false). +caseQ_nul="/tmp/netshift-fb-caseQ-nul-$$.bin" +caseQ_txt="/tmp/netshift-fb-caseQ-txt-$$.txt" +printf 'abc\000def' > "$caseQ_nul" +printf 'abcdef\nplain text\n' > "$caseQ_txt" +if subscription_body_is_binary "$caseQ_nul"; then + echo 'fb-caseQ-nul-is-binary:OK' +else + echo 'fb-caseQ-nul-is-binary:FAIL' +fi +if subscription_body_is_binary "$caseQ_txt"; then + echo 'fb-caseQ-text-not-binary:FAIL' +else + echo 'fb-caseQ-text-not-binary:OK' +fi +rm -f "$caseQ_nul" "$caseQ_txt" + echo 'DONE' FBEOF From 0ac0a3659804cb1a6a8a89c182454a327b9dfd1c Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Fri, 12 Jun 2026 10:00:52 +0300 Subject: [PATCH 73/75] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D0=BD=D0=B3=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D0=B8=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= =?UTF-8?q?=20netshift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 47 +++++++ .../memory/shell-backend-developer.md | 27 ++++ netshift/files/usr/lib/updater.sh | 16 ++- tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 121 +++++++++++++++++- 5 files changed, 206 insertions(+), 7 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 3e1f5691..2548070a 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -1229,3 +1229,50 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> - CONDITION (M1) is commit-hygiene only: keep the pre-existing unrelated .opencode/agent/*.md churn (model rename + `bash "*": ask`->`allow`) OUT of the commit; the permission loosening is a separate human decision. + +## task-047 false-"Outdated" — minified-JSON tag parse (2026-06-12) + +- ISSUE (user, AX3000T, OWRT 25.12.2 apk, NetShift 0.8.8): "check for updates" + always says Outdated; "update" -> error; reinstall via Putty doesn't help. OCR + of the UI showed the "latest version" as a URL + `https://api.github.com/repos/yandexru45/netshift/releases/<id>` instead of a + version number. +- ROOT CAUSE (PROVEN by reproduction): updates_netshift_latest_tag + (updater.sh) parsed the tag with `grep '"tag_name":' | head -n1 | + cut -d'"' -f4`. That ONLY works on PRETTY-PRINTED JSON. GitHub API/CDN/proxies + often return MINIFIED JSON (whole object on one line) -> grep matches the + entire line, `cut -d'"' -f4` returns the FIRST key's value = the release + "url". So latest="<url>", `0.8.8 != <url>` -> false "outdated"; and the + self-update worker (same fn for $latest) logged "downloading NetShift <url> + release packages" then failed. Reinstall is useless: bug is in PARSING. + Repro: `printf '{"url":".../releases/338202209","id":...,"tag_name":"0.8.8",...}' + | grep '"tag_name":'|head -1|cut -d'"' -f4` => the url. +- WHY ONLY THIS FN: sing-box-extended path already uses jq (.tag_name); asset + download uses `grep -o "https://...\.(ipk|apk)"` (newline-agnostic, robust); + install.sh only rate-limit-greps + asset grep -o. All unaffected. jq is a hard + dep used 23x in updater.sh. +- FIX (task-047, APPROVED W/ CONDITIONS): replace the body with + `tag="$(printf '%s' "$response" | jq -r '.tag_name // empty' 2>/dev/null)"; + [ -n "$tag" ] || return 1; printf '%s' "$tag"`. Format-independent; `.tag_name + // empty` -> empty on rate-limit/error object (callers already handle + "could not determine latest"). Fixes BOTH check_update and self-update (shared + fn). Callers untouched. No Oniguruma. +- TEST (smoke `latesttag`, registered main all)+case+usage): stub the network + boundary `updates_http_get_once` (NOT the fn under test), run the REAL + updates_netshift_latest_tag. Cases: MINIFIED (exact bug shape, url before + tag_name) -> 0.8.8 not url (regression guard); pretty -> 0.8.8; rate-limit -> + empty+nonzero; e2e minified through updates_check_netshift -> status latest. + set -e rc capture via `&& printf 0 || printf %s "$?"`. Guard self-proven. +- LESSON (reusable): NEVER field-position-parse JSON (`grep KEY|head|cut -d'"' + -fN`) — it silently breaks on minified responses by grabbing the first key's + value. Use jq (already a dep) for any tag/scalar extraction; reserve `grep -o + <url-regex>` for URL-list extraction (that IS newline-agnostic and fine). +- WHOLE-CHAIN verified on the user's class of box earlier: get_system_info keeps + latest="unknown" (no net); check_update is the on-demand fetch; the diagnostic + row + manager card flag outdated when installed != latest, so a bad latest + poisons BOTH UI spots — fixing the parse fixes all consumers. +- CONDITION [S1] is commit-hygiene: keep the pre-existing unrelated + .opencode/agent/*.md churn (model rename + `bash "*": ask`->`allow`) OUT of the + commit; the bash-permission widening is security-relevant and needs a separate + explicit human decision. (Recurring across tasks 043/046/047 — these 5 files + were already dirty at session start.) diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 71361b6e..41e7b40b 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -1460,3 +1460,30 @@ findings; keep under ~200 lines. all` 174 passed / 0 failed (aggregate counter folds the 5 new tokens into the test_subscription header group — count unchanged, the 5 fb-caseP/Q :OK lines print explicitly in the `subscription` run). + +## task-047 — latest-tag jq parse (false "Outdated" fix) + +- `updates_netshift_latest_tag` (`updater.sh:~1635`) used + `grep '"tag_name":' | head -n1 | cut -d'"' -f4`. That ONLY works on + pretty-printed JSON. On MINIFIED GitHub JSON (whole object on one line, `"url"` + before `"tag_name"`), grep matches the whole line and `cut -f4` returns the + FIRST key's value = the release `"url"` → false "outdated" + self-update + downloads a garbage "version". Fix: `jq -r '.tag_name // empty'` + (format-independent; `// empty` → empty on rate-limit/error objects; no + Oniguruma). jq is a hard dep (used 23× in updater.sh) and the sibling + sing-box-extended path already used `.tag_name`. +- LESSON (whitespace-fragile grep|cut on JSON): never field-position-`cut` JSON + that may be minified. Prefer jq when it's already a dep. +- New smoke test `test_netshift_latest_tag` (token `latesttag`): driver sources + updater.sh, stubs ONLY the network boundary `updates_http_get_once` (NOT the + parse fn), runs the REAL `updates_netshift_latest_tag`. 4 cases: minified→tag + (regression guard), pretty→tag, rate-limit→empty+nonzero, e2e via + `updates_check_netshift`→status "latest". Registered in main() all)+case+usage + +docker-compose.yml comment. +- GOTCHA: under harness `set -e`, a stub-driver `ash "$drv"` that returns + non-zero (the rate-limit case) aborts the whole suite — guard rc capture with + `ash ... && printf 0 >rc || printf $? >rc`. +- Guard self-proven: restoring the old grep|cut line made + `latesttag-minified-returns-tag-not-url` FAIL (returned the .../releases/<id> + url), then restored jq. Gates: shellcheck -S error clean (bin + libs + + install.sh); `smoke-tests all` 174→178 passed / 0 failed (+4 latesttag). diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 6243f089..1ede17dc 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -1626,17 +1626,23 @@ updates_self_update_netshift() { return "$rc" } -# Echoes the GitHub latest-release tag for NetShift (e.g. "v0.8.1"), or nothing. -# Reuses the same API endpoint as get_system_info / install.sh; parsed with -# grep/cut (the tag is needed only as a display/compare string, no jq array). +# Echoes the GitHub latest-release tag for NetShift (e.g. "0.8.8"), or nothing. +# Reuses the same API endpoint as get_system_info / install.sh. Parsed with jq +# (NOT grep/cut): GitHub may return the release object pretty-printed OR minified +# (single line); a field-positional grep|cut grabs the first key's value (the +# release "url") on minified JSON, which caused a false "outdated" + a self-update +# that downloaded a garbage "version". jq is format-independent. updates_netshift_latest_tag() { - local response + local response tag response="$(updates_http_get_once "$NETSHIFT_RELEASE_API_URL" "")" if [ -z "$response" ]; then return 1 fi - printf '%s' "$response" | grep '"tag_name":' | head -n1 | cut -d'"' -f4 + + tag="$(printf '%s' "$response" | jq -r '.tag_name // empty' 2>/dev/null)" + [ -n "$tag" ] || return 1 + printf '%s' "$tag" } # Downloads the NetShift release assets matching the package-name prefixes for diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index e4668f69..68ae2e02 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -11,7 +11,7 @@ # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, # nftv6, selmark, isolation, monfd, unsupported, diagnostics, subscription, insecure, rejected, # jobstate, selfheal, dnsdetour, globalproxy, stablecheck, -# extcheck, netshiftcheck, selfupdate, backupguard +# extcheck, netshiftcheck, latesttag, selfupdate, backupguard # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index c220f26a..b0842ca7 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -5305,6 +5305,123 @@ DRVEOF rm -rf "$work" } +# ───────────────────────────────────────────────────────────────── +# Test: NetShift latest-tag parse — minified vs pretty JSON (task-047) +# ───────────────────────────────────────────────────────────────── +# Guards the false-"outdated" bug: updates_netshift_latest_tag used a +# field-positional grep|cut that, on MINIFIED GitHub JSON (whole object on one +# line, "url" before "tag_name"), returned the release "url" instead of the tag +# — causing a false "outdated" + a self-update that downloaded a garbage +# "version". The fix parses with jq '.tag_name // empty' (format-independent). +# +# We exercise the REAL parse: the driver sources updater.sh and stubs ONLY the +# network boundary (updates_http_get_once) with markered JSON, then calls the +# real updates_netshift_latest_tag / updates_check_netshift. No network. +test_netshift_latest_tag() { + header "NetShift latest-tag jq parse (task-047)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local updater="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$updater" ]; then + skip "updater.sh not found in ${NETSHIFT_LIB_DIR}" + return + fi + + local work="/tmp/netshift-latesttag-$$" + rm -rf "$work" + mkdir -p "$work" + + # Driver: source helpers.sh + updater.sh, silence logging, pin constants, + # stub the network boundary (updates_http_get_once) to emit $STUBLT_BODY, + # then run the REAL parse function named in $STUBLT_FN. + local drv="$work/driver.sh" + cat > "$drv" << 'DRVEOF' +log() { :; } +echolog() { :; } +nolog() { :; } +updates_log() { :; } +. "DRV_HELPERS" +. "DRV_UPDATER" +NETSHIFT_RELEASE_API_URL="https://api.test/latest" +NETSHIFT_VERSION="$STUBLT_INSTALLED" +updates_http_get_once() { printf '%s' "$STUBLT_BODY"; } +"$STUBLT_FN" +DRVEOF + sed -i "s|DRV_UPDATER|$updater|g;s|DRV_HELPERS|${NETSHIFT_LIB_DIR}/helpers.sh|g" "$drv" + + local out="$work/out.txt" + local rc_file="$work/rc.txt" + run_lt() { + # The parse function returns non-zero on the rate-limit/error case; under + # the harness `set -e` that would abort the suite, so capture rc via the + # `|| ...` guard (assertions read $out + $rc_file, not the live rc). + ash "$drv" > "$out" 2>/dev/null && printf '0' > "$rc_file" || printf '%s' "$?" > "$rc_file" + } + + # The exact minified release object from the bug report: "url" BEFORE + # "tag_name", whole object on a single line, with .../releases/338202209. + local minified='{"url":"https://api.github.com/repos/yandexru45/netshift/releases/338202209","id":338202209,"tag_name":"0.8.8","name":"0.8.8"}' + # Pretty-printed equivalent (one key per line). + local pretty='{ + "url": "https://api.github.com/repos/yandexru45/netshift/releases/338202209", + "id": 338202209, + "tag_name": "0.8.8", + "name": "0.8.8" +}' + # Rate-limit/error object — no tag_name. + local ratelimit='{"message":"API rate limit exceeded for 1.2.3.4","documentation_url":"https://docs.github.com/rest"}' + + export STUBLT_FN="updates_netshift_latest_tag" + export STUBLT_INSTALLED="0.8.8" + + # ── Case 1 (REGRESSION GUARD): minified → exactly 0.8.8, NOT the url ────── + export STUBLT_BODY="$minified" + run_lt + if [ "$(cat "$out" 2>/dev/null)" = "0.8.8" ] && [ "$(cat "$rc_file" 2>/dev/null)" = "0" ]; then + pass "latesttag-minified-returns-tag-not-url:OK" + else + fail "latesttag-minified-returns-tag-not-url:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)" + fi + + # ── Case 2: pretty-printed → 0.8.8 ─────────────────────────────────────── + export STUBLT_BODY="$pretty" + run_lt + if [ "$(cat "$out" 2>/dev/null)" = "0.8.8" ] && [ "$(cat "$rc_file" 2>/dev/null)" = "0" ]; then + pass "latesttag-pretty-returns-tag:OK" + else + fail "latesttag-pretty-returns-tag:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)" + fi + + # ── Case 3: rate-limit/error object (no tag_name) → empty + non-zero ───── + export STUBLT_BODY="$ratelimit" + run_lt + if [ -z "$(cat "$out" 2>/dev/null)" ] && [ "$(cat "$rc_file" 2>/dev/null)" != "0" ]; then + pass "latesttag-ratelimit-empty-nonzero:OK" + else + fail "latesttag-ratelimit-empty-nonzero:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)" + fi + + # ── Case 4 (end-to-end): minified through updates_check_netshift with + # installed == tag → status "latest" (the false-outdated is gone). ──────── + export STUBLT_FN="updates_check_netshift" + export STUBLT_BODY="$minified" + export STUBLT_INSTALLED="0.8.8" + run_lt + if jq -e '.success == true and .status == "latest" + and .latest_version == "0.8.8"' "$out" > /dev/null 2>&1; then + pass "latesttag-e2e-check-minified-latest:OK" + else + fail "latesttag-e2e-check-minified-latest:FAIL" "$(cat "$out" 2>/dev/null)" + fi + + unset STUBLT_FN STUBLT_BODY STUBLT_INSTALLED + rm -rf "$work" +} + # ───────────────────────────────────────────────────────────────── # Test: NetShift self-update (task-017) # ───────────────────────────────────────────────────────────────── @@ -5851,6 +5968,7 @@ main() { test_check_update_stable test_check_update_extended test_check_update_netshift + test_netshift_latest_tag test_self_update_netshift test_backup_integrity ;; @@ -5875,6 +5993,7 @@ main() { stablecheck) test_check_update_stable ;; extcheck) test_check_update_extended ;; netshiftcheck) test_check_update_netshift ;; + latesttag) test_netshift_latest_tag ;; selfupdate) test_self_update_netshift ;; backupguard) test_backup_integrity ;; jq) test_jq_helpers ;; @@ -5882,7 +6001,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd unsupported diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck selfupdate backupguard" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd unsupported diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck latesttag selfupdate backupguard" exit 1 ;; esac From f63e0b9fd919af33e8f7832a2706705a753ccc0c Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Fri, 12 Jun 2026 23:16:47 +0300 Subject: [PATCH 74/75] =?UTF-8?q?=D1=84=D0=BE=D0=BB=D0=BB=D0=B1=D0=B5?= =?UTF-8?q?=D0=BA=20=D0=B8=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BB=D0=B8=D1=81=D1=82=D0=BE=D0=B2=20=D1=81=D0=BE=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=80=D1=8B=D1=85=20=D0=BA=D0=B3=D1=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 49 +++++ docs/agent-rules/memory/code-reviewer.md | 4 + .../memory/shell-backend-developer.md | 80 ++++++++ netshift/files/usr/bin/netshift | 99 +++++++++- tests/docker-compose.yml | 2 +- tests/entrypoint.sh | 185 +++++++++++++++++- 6 files changed, 411 insertions(+), 8 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index 2548070a..a348c04f 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -1276,3 +1276,52 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> commit; the bash-permission widening is security-relevant and needs a separate explicit human decision. (Recurring across tasks 043/046/047 — these 5 files were already dirty at session start.) + +## task-048 scalar option subscription_url — sing-box won't start (2026-06-12) + +- ISSUE (Nick, Cudy WR3000E, OWRT 25.12.4, 0.8.9): urltest section works; switch + to subscription -> whole chain dead (sing-box not running, no nft table, + FakeIP 127.0.0.42:53 refused). Startup log: "Outbound section not found. + Aborted." despite config having a subscription_url. +- DIAGNOSIS METHOD: user sent 2x3 diagnostic txt (global_check/show_sing_box_config/ + view_logs) for working vs broken. The view_logs filenames were SWAPPED (the + tiny 435B file was the broken run, the big one was the working urltest run) — + read by CONTENT not filename. Broken view_logs: single "Outbound section not + found. Aborted." line. global_check(broken): proxy_config_type 'subscription' + with `option subscription_url '...'` (scalar, NOT list). +- ROOT CAUSE (PROVEN on hardware): get_subscription_urls_for_section reads + subscription_url ONLY via config_list_foreach, which iterates ONLY UCI `list` + values and returns EMPTY for a scalar `option`. Proven: config_list_foreach + over option => []; config_get => the value; over list => works. The UI writes + `list` (form.DynamicList) so new configs are fine; legacy/CLI/podkop-migrated + configs use `option` and broke. Every subscription consumer funnels through + this ONE helper -> fixing it fixes the whole chain. +- FIX (task-048, APPROVED round 2): (1) load-bearing read-fallback in + get_subscription_urls_for_section: if the list read is empty, config_get the + scalar and feed it through _collect_subscription_url_handler. (2) one-time + idempotent option->list migration at top of start_main (after config_load, + before check_requirements), only on the broken shape, never exits. +- REVIEW LOOP (2 rounds, important): round 1 REQUIRES CHANGES — BLOCKER [B1] + data loss: the migration used `uci add_list "key=value"` which SPLITS ON THE + FIRST `=` and loses query-string URLs (?token=abc&x=1) — reproduced on hardware + (rc=1, list empty, scalar already deleted => URL gone on disk). Fix: use the + `uci_add_list <cfg> <sec> <opt> "<val>"` SHELL HELPER (separate-arg, preserves + =/&), delete-then-add with scalar RESTORE on add failure, flag gates the + commit. Plus [S1] the new test ran assertions on the RHS of a pipe (subshell + counter loss) so it didn't gate CI -> fixed to `while read < tmpfile`. Round 2 + APPROVED. +- LESSONS (reusable): (a) NEVER trust user-supplied filenames for which-is-which — + read by content. (b) `config_list_foreach` does NOT read scalar options; any + list-option reader needs a scalar config_get fallback for back-compat with + legacy/CLI/migrated configs. (c) `uci add_list "k=v"` CLI form is unsafe for + values containing `=` — use the uci_add_list shell helper. (d) a smoke test + that pipes into `while read; pass/fail` does NOT gate CI (subshell) — the count + jump (178->190) when fixed is the tell. (e) RE-check the dev's memory note on a + fix round — it's often pre-fix and re-seeds the anti-pattern. +- PRIVACY: Nick's dump contained a real subscription URL; moved all 6 txt to + /tmp/opencode/nick-diag (out of git); code/tests/specs/memory use ONLY synthetic + https://example.com/sub. Final whole-tree sweep for the real host => clean. +- GATES: shellcheck -S error clean; smoke `all` 178->190/0 (the +12 are the now- + gating suburlopt tokens incl. 4 =-URL guards); whole-chain verified (option + config -> has_outbound_section TRUE -> gen -> sing-box check). Runtime contract + intact (UCI schema = back-compat repr normalization only). diff --git a/docs/agent-rules/memory/code-reviewer.md b/docs/agent-rules/memory/code-reviewer.md index b3474865..a127dad5 100644 --- a/docs/agent-rules/memory/code-reviewer.md +++ b/docs/agent-rules/memory/code-reviewer.md @@ -67,3 +67,7 @@ append recurring findings; keep under ~200 lines. - Package-manager rc is NOT a reliable success signal on opkg: rc=0 for "Not downgrading"/"already installed"/"up to date". A self-update/install that trusts only rc silently no-ops (the v→no-v rename trap: legacy `v0.8.6` sorts ABOVE `0.8.7` in opkg's compare, so `opkg install` refuses the "downgrade" and returns 0). When reviewing a package-install path, require: (a) `--force-downgrade --force-reinstall` on the opkg branch (apk overwrites by default); AND (b) verify-after-install — RE-READ the installed version (opkg `list-installed | grep "^pkg "`, apk `list --installed`; grep/awk only, NO Oniguruma jq) and compare v-stripped semver (`${x#v}`, `${x%%-*}`) with `==` OR `is_min_package_version installed target`; empty-installed must fail-safe to success:false. Keep install.sh `pkg_install` and updater.sh `updates_pkg_install_file` opkg branches ALIGNED. (task-041/042) - Async self-update worker landmine: the `_*_core` worker MUST `return 1` (NEVER `exit`) on failure so the public wrapper's always-run `updates_restore_after_swap` epilogue + finished-job-state write still execute. Verify the wrapper captures core rc/JSON to a temp file then unconditionally restores. Smoke assertions for these must be in the MAIN shell body (direct `if…pass/fail`), never inside `cmd | while read` (subshell swallows PASS/FAIL — harness-wide landmine). (task-041) + +- UCI option→list rewrites: the `uci add_list "key=value"` CLI form splits on the FIRST `=` and SILENTLY LOSES query-string URLs (`?token=abc&x=1`) — reproduced on hardware (rc=1, list empty). Require the `uci_add_list <cfg> <sec> <opt> "<val>"` SHELL HELPER (separate-arg, preserves `=`/`&`). For delete-then-add rewrites, verify a failed add RESTORES the scalar AND that the change-flag gates the `uci commit` (an uncommitted in-memory delete must never persist). (task-048 [B1]) +- When RE-reviewing a fix round, also diff the developer's MEMORY note: it is frequently written against the PRE-fix code and re-seeds the very anti-pattern that was just fixed (task-048 [M2]: note still showed the `key=value` form + "non-gating piped-while" after both were fixed). Flag a stale memory note as a (minor) condition. +- Test-gating landmine: a smoke test whose assertions run on the RHS of a pipe (`cmd | while read; pass/fail`) does NOT gate CI (subshell counter loss) — a FAIL token prints red but the suite exits 0. Require current-shell parsing (`while read < tmpfile`). The 178→190 count jump when task-048 fixed this is the tell. (task-048 [S1]) diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index 41e7b40b..af507add 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -1487,3 +1487,83 @@ findings; keep under ~200 lines. `latesttag-minified-returns-tag-not-url` FAIL (returned the .../releases/<id> url), then restored jq. Gates: shellcheck -S error clean (bin + libs + install.sh); `smoke-tests all` 174→178 passed / 0 failed (+4 latesttag). + +## task-048: scalar `option subscription_url` read-fallback + option->list migration + +- Root cause (PROVEN on hardware, Cudy WR3000E / OWRt 25.12.4 / NetShift 0.8.9): + a section storing `option subscription_url '<url>'` (legacy / CLI / + podkop-migrated configs) made `get_subscription_urls_for_section` + (`bin/netshift`) return EMPTY → `has_outbound_section` false → "Outbound + section not found. Aborted." → sing-box never starts → whole chain down (no nft + table, FakeIP 127.0.0.42:53 refused). `config_list_foreach` iterates ONLY UCI + `list` values; over a scalar `option` it iterates NOTHING. `config_get` reads + the scalar. Regression from task-022 (multi-URL feature made subscription_url a + list / form.DynamicList); the task-022 memory note "a lone legacy option reads + as a 1-element list — NO migration code" was the FALSE assumption that shipped + the bug. EVERY subscription-URL reader funnels through this one helper. +- Fix 1 (load-bearing, single source): in `get_subscription_urls_for_section`, + AFTER the `config_list_foreach`, if `SUBSCRIPTION_URLS_COLLECTED` is still + empty, `config_get scalar_url "$section" "subscription_url"` and (if non-empty) + `_collect_subscription_url_handler "$scalar_url"` (reuse the handler so + dedup/format stays identical). All new vars `local`. Must stand alone on + read-only fs / when migration is skipped. Corrected the false comment at + `section_has_configured_outbound` (subscription branch) and the collector + header. +- Fix 2 (hygiene, idempotent): `migrate_legacy_subscription_url_option` + + `_migrate_legacy_subscription_url_option_handler` (config_foreach callback). + Detects the broken shape robustly: LIST read empty AND scalar config_get + non-empty (an already-correct list is never touched). Rewrites via + `uci -q delete netshift.<sec>.subscription_url` then the `uci_add_list netshift + "$sec" subscription_url "$url"` SHELL HELPER (from /lib/functions.sh) — NOT the + `uci add_list "key=value"` CLI form, which splits on the first `=` and SILENTLY + LOSES query-string URLs (`?token=abc&x=1`) [code-review BLOCKER B1, reproduced + on hardware: CLI add_list rc=1, list empty, scalar already deleted => URL lost + on disk]. On add_list FAILURE the else branch `uci_set`s the scalar back so a + failed migration never leaves the section with NO url (and the flag stays 0 => + no commit => uncommitted in-memory delete never persists; on-disk URL survives). + sets a module-level flag `SUBSCRIPTION_URL_OPTION_MIGRATED`; a SINGLE `uci commit netshift` + + `config_load "$NETSHIFT_CONFIG"` only if anything changed (mirrors the + :956/:1099 commit+reload). NEVER exits — uci failures log `warn` and continue + (the read-fallback covers correctness). Invoked ONCE at the TOP of `start_main` + BEFORE `check_requirements` (which reads URLs via has_outbound_section), AFTER + the file-scope `config_load`. +- Smoke landmine confirmed (again): the multi-url `test_subscription` harness + STUBS `config_list_foreach` (feeds MU_URLS) and does NOT touch real UCI / does + NOT stub `config_get` — so it can NEVER catch this bug (it bypasses the broken + primitive). The regression guard MUST be a REAL-UCI test that `config_load`s a + fixture and runs the SHIPPED (awk-extracted) functions. +- New top-level smoke test `test_sub_url_option` (alias `suburlopt`). 12 tokens: + `suburlopt:scalar-read` (regression guard — empty before fix), + `:scalar-hasoutbound`, `:list-read` (no-regression), `:migrate-flag`, + `:migrate-value`, `:migrate-islist`, `:migrate-idempotent`, + `:migrate-idempotent-value`, PLUS the `=`-URL [B1] guards + `:migrate-equrl-preserved`, `:migrate-equrl-islist`, `:migrate-equrl-single` + (asserts exactly 1 list element), `:migrate-idempotent-equrl` — fixture URL + `https://example.com/sub?token=abc&x=1`. The driver output is parsed in the + CURRENT shell (temp file + `while read < "$out"`, NO pipe) so the tokens + ACTUALLY GATE CI (fixed the harness-wide piped-while counter-quirk for this + test). Migration is tested against a throwaway + `/etc/config/netshift` (the function hardcodes the `netshift` config name) with + NETSHIFT_CONFIG=netshift; the caller backs up + restores any real one. Skips + cleanly if /lib/functions.sh or uci unavailable. Registered in all)+case alias+ + "Available:" usage line + docker-compose.yml comment. Synthetic + `https://example.com/sub` ONLY (operator privacy rule: a user dump leaked a + real URL — never write a real subscription URL/host/id anywhere). +- Self-prove DONE: removing the read-fallback block made `suburlopt:scalar-read` + go empty (its `:OK` disappeared) and `suburlopt:scalar-hasoutbound:FAIL` + appeared; restored and all tokens green again. Also self-proved [B1]: the old + `key=value` CLI add_list made `:migrate-equrl-preserved` FAIL (URL lost); + `uci_add_list` helper fixed it. +- Whole-chain verified in-container: `has_outbound_section` returns TRUE for an + option-shaped config → requirements gate passes → config gen + sing-box check + proceed. +- PRE-EXISTING (NOT mine): `test_rejected_hash` rh-case1/2/6 fail on the BASELINE + bin/netshift too (verified via git stash) — an existing container/env issue, + unrelated to task-048. +- Gates: shellcheck -S error clean (bin + libs + install.sh); `smoke-tests all` + 178→190 passed / 0 failed (the +12 is the 12 `suburlopt` tokens, which now + count because the test parses driver output in the CURRENT shell, NOT a pipe). + NO sacred constant/port/mark/path changed; UCI schema only normalizes an + existing key's representation (option→list, back-compat). +- code-review round 2: APPROVED WITH CONDITIONS — [B1]/[S1]/[M1] all resolved; + the only condition was fixing THIS stale memory note (done). diff --git a/netshift/files/usr/bin/netshift b/netshift/files/usr/bin/netshift index ca2de0eb..544b9695 100755 --- a/netshift/files/usr/bin/netshift +++ b/netshift/files/usr/bin/netshift @@ -117,9 +117,10 @@ section_has_configured_outbound() { [ -n "$outbound_json" ] && return 0 ;; subscription) - # subscription_url is now a UCI list (a lone legacy option reads as a - # 1-element list). The section has a configured outbound if at least - # one URL is present. + # get_subscription_urls_for_section handles both the UCI `list` shape + # (new UI configs) and a scalar `option` shape (legacy / CLI / + # migrated configs), so the section has a configured outbound if it + # returns at least one URL. [ -n "$(get_subscription_urls_for_section "$section")" ] && return 0 ;; esac @@ -197,9 +198,10 @@ get_subscription_user_agent_cache_path() { echo "$SUBSCRIPTION_CACHE_FOLDER/${section}${urlhash:+.$urlhash}.user_agent" } -# Collect a section's subscription_url entries (a UCI list, but a lone legacy -# `option subscription_url` reads as a 1-element list exactly like -# community_lists) into the newline-delimited global SUBSCRIPTION_URLS_COLLECTED. +# Collect a section's subscription_url entries (a UCI list; a legacy / CLI / +# migrated `option subscription_url` is a scalar that config_list_foreach does +# NOT iterate — get_subscription_urls_for_section handles that shape with a +# scalar fallback) into the newline-delimited global SUBSCRIPTION_URLS_COLLECTED. # URLs are opaque user text and may contain shell-special chars, so they are # accumulated newline-delimited (URLs cannot contain a newline) and consumers # read them with `while IFS= read -r`, never via word-splitting. @@ -217,9 +219,23 @@ $url" get_subscription_urls_for_section() { local section="$1" + local scalar_url SUBSCRIPTION_URLS_COLLECTED="" config_list_foreach "$section" "subscription_url" _collect_subscription_url_handler + + # Backward compat: legacy / CLI / podkop-migrated configs store + # subscription_url as a scalar `option` (not a `list`). config_list_foreach + # iterates ONLY list values, so it returns nothing for a scalar option. Fall + # back to a scalar read and treat it as a 1-element list. (PROVEN on hardware: + # config_list_foreach over an option => empty; config_get => the value.) This + # is the load-bearing fix and must stand alone even when the option->list + # migration is skipped (read-only fs / uci failure). + if [ -z "$SUBSCRIPTION_URLS_COLLECTED" ]; then + config_get scalar_url "$section" "subscription_url" + [ -n "$scalar_url" ] && _collect_subscription_url_handler "$scalar_url" + fi + printf '%s' "$SUBSCRIPTION_URLS_COLLECTED" } @@ -839,6 +855,12 @@ start_subscription_startup_retry_worker() { start_main() { log "Starting netshift" + # Normalize legacy scalar `option subscription_url` to the canonical `list` + # shape BEFORE check_requirements / config generation read the URLs. The + # read-fallback in get_subscription_urls_for_section already covers + # correctness; this is hygiene that converges stored configs to a list. + migrate_legacy_subscription_url_option + check_requirements migration @@ -1170,6 +1192,71 @@ migration() { : } +# config_foreach callback for migrate_legacy_subscription_url_option. For a +# subscription section whose subscription_url is stored as a scalar `option` +# (legacy / CLI / podkop-migrated configs) rather than a UCI `list`, rewrite it +# in place as a `list` via uci. Detects the broken shape robustly: the LIST read +# (config_list_foreach) yields nothing AND a scalar config_get is non-empty — +# exactly the option-only shape, so an already-correct list is never touched. +# Sets the module-level SUBSCRIPTION_URL_OPTION_MIGRATED flag when it changes +# anything so the caller commits + reloads exactly once. Never exits: any uci +# failure is logged at warn and skipped (the read-fallback in +# get_subscription_urls_for_section covers correctness regardless). +_migrate_legacy_subscription_url_option_handler() { + local section="$1" + local connection_type proxy_config_type scalar_url + + config_get connection_type "$section" "connection_type" + [ "$connection_type" = "proxy" ] || return 0 + + config_get proxy_config_type "$section" "proxy_config_type" "url" + [ "$proxy_config_type" = "subscription" ] || return 0 + + # Only the broken shape: empty via the list path but present as a scalar. + SUBSCRIPTION_URLS_COLLECTED="" + config_list_foreach "$section" "subscription_url" _collect_subscription_url_handler + [ -z "$SUBSCRIPTION_URLS_COLLECTED" ] || return 0 + + config_get scalar_url "$section" "subscription_url" + [ -n "$scalar_url" ] || return 0 + + # Rewrite the scalar option as a list. Use the uci_add_list SHELL HELPER + # (from /lib/functions.sh), NOT the `uci add_list "key=value"` CLI form: the + # CLI form splits on the FIRST `=`, so a URL with a query string (very common, + # e.g. "...?token=abc&x=1") makes the CLI add_list fail and lose the value. + # The helper passes the value as a separate argument, preserving `=`/`&` + # byte-for-byte. Delete the scalar first so the result is a CLEAN single- + # element list (adding while the scalar option still exists would duplicate + # it into a 2-element list); if the add then fails, RESTORE the scalar option + # so a failed migration can never leave the section with NO url. Never exits. + uci -q delete "netshift.${section}.subscription_url" 2>/dev/null + if uci_add_list netshift "$section" subscription_url "$scalar_url" 2>/dev/null; then + SUBSCRIPTION_URL_OPTION_MIGRATED=1 + log "Migrated legacy scalar subscription_url to list for section '$section'" "info" + else + uci_set netshift "$section" subscription_url "$scalar_url" + log "Failed to migrate scalar subscription_url to list for section '$section'; restored the original option and continuing (read fallback covers correctness)" "warn" + fi +} + +# One-time, idempotent normalization of a legacy scalar `option subscription_url` +# into the canonical `list subscription_url`. Runs once at startup AFTER +# config_load and BEFORE config generation reads the URLs. Idempotent: a config +# already using `list` is left untouched (no commit, no churn). Never exits. +migrate_legacy_subscription_url_option() { + SUBSCRIPTION_URL_OPTION_MIGRATED=0 + + config_foreach _migrate_legacy_subscription_url_option_handler "section" + + if [ "$SUBSCRIPTION_URL_OPTION_MIGRATED" -eq 1 ]; then + if uci commit "netshift" 2>/dev/null; then + config_load "$NETSHIFT_CONFIG" + else + log "Failed to commit subscription_url option->list migration; continuing (read fallback covers correctness)" "warn" + fi + fi +} + validate_service() { local service="$1" diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 68ae2e02..0b705dec 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -10,7 +10,7 @@ # # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, # nftv6, selmark, isolation, monfd, unsupported, diagnostics, subscription, insecure, rejected, -# jobstate, selfheal, dnsdetour, globalproxy, stablecheck, +# jobstate, selfheal, dnsdetour, suburlopt, globalproxy, stablecheck, # extcheck, netshiftcheck, latesttag, selfupdate, backupguard # ────────────────────────────────────────────────────────────────── diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index b0842ca7..051f2e9a 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -4836,6 +4836,187 @@ DDEOF rm -f "$drv" } +# ───────────────────────────────────────────────────────────────── +# Test: scalar `option subscription_url` read-fallback + option->list migration +# (task-048) +# ───────────────────────────────────────────────────────────────── +# REAL-UCI regression guard for the hardware bug: a section storing +# subscription_url as a scalar UCI `option` (legacy / CLI / podkop-migrated +# configs) made get_subscription_urls_for_section return EMPTY (config_list_foreach +# iterates ONLY list values), so has_outbound_section failed and sing-box never +# started. This must use the SHIPPED functions against an actual config_load — NOT +# the stubbed config_list_foreach in test_subscription (which honors MU_URLS +# directly and therefore cannot catch the broken primitive). Synthetic URL only. +test_sub_url_option() { + header "Scalar option subscription_url read-fallback + migration (task-048)" + + local bin="${NETSHIFT_SRC}/usr/bin/netshift" + if [ ! -r "$bin" ]; then + skip "suburlopt — bin/netshift not found" + return + fi + if [ ! -r /lib/functions.sh ] || [ ! -r /lib/config/uci.sh ] || ! command -v uci > /dev/null 2>&1; then + skip "suburlopt — LuCI config_load / uci not available" + return + fi + + local lib="${NETSHIFT_LIB_DIR}" + local drv="/tmp/netshift-suburlopt-$$.sh" + cat > "$drv" << 'SUBOPTEOF' +BIN="BIN_PATH_PLACEHOLDER" +LIB="LIB_DIR_PLACEHOLDER" +. /lib/functions.sh +. /lib/config/uci.sh 2>/dev/null || true +# shellcheck disable=SC1090 +. "$LIB/constants.sh" +# shellcheck disable=SC1090 +. "$LIB/helpers.sh" +log() { :; } +echolog() { :; } +nolog() { :; } +# Exercise the SHIPPED functions verbatim (awk-extracted) against a real +# config_load — this is the whole point: the real LuCI config_list_foreach / +# config_get primitives, not a stub. +for fn in get_subscription_urls_for_section _collect_subscription_url_handler \ + section_has_configured_outbound \ + migrate_legacy_subscription_url_option \ + _migrate_legacy_subscription_url_option_handler; do + eval "$(awk -v f="$fn" '$0 ~ "^"f"\\(\\) \\{"{p=1} p{print} p&&/^\}/{exit}' "$BIN")" +done + +mkdir -p /etc/config + +# ── Fixture A: SCALAR option subscription_url (the exact broken shape) ── +cat > /etc/config/netshift_suboptscalar <<'CFGEOF' +config section 'main' + option connection_type 'proxy' + option proxy_config_type 'subscription' + option subscription_url 'https://example.com/sub' +CFGEOF +config_load netshift_suboptscalar +urls="$(get_subscription_urls_for_section main)" +[ "$urls" = "https://example.com/sub" ] && echo 'suburlopt:scalar-read:OK' || echo "suburlopt:scalar-read:FAIL [$urls]" +if section_has_configured_outbound main; then + echo 'suburlopt:scalar-hasoutbound:OK' +else + echo 'suburlopt:scalar-hasoutbound:FAIL' +fi +rm -f /etc/config/netshift_suboptscalar + +# ── Fixture B: LIST subscription_url (must still work — no regression) ── +cat > /etc/config/netshift_suboptlist <<'CFGEOF' +config section 'main' + option connection_type 'proxy' + option proxy_config_type 'subscription' + list subscription_url 'https://example.com/sub' +CFGEOF +config_load netshift_suboptlist +urls="$(get_subscription_urls_for_section main)" +[ "$urls" = "https://example.com/sub" ] && echo 'suburlopt:list-read:OK' || echo "suburlopt:list-read:FAIL [$urls]" +rm -f /etc/config/netshift_suboptlist + +# ── Migration: option -> list, idempotent. The migration function hardcodes +# the `netshift` config name, so write a throwaway /etc/config/netshift (the +# caller backs up + restores any real one). Two sections: a plain URL AND a URL +# with a query string containing `=`/`&`/`?` — the latter is the [B1] regression +# guard: the old `uci add_list "key=value"` CLI form splits on the first `=` and +# LOSES the value, while uci_add_list preserves it byte-for-byte. ── +NETSHIFT_CONFIG="netshift" +EQ_URL='https://example.com/sub?token=abc&x=1' +cat > /etc/config/netshift <<CFGEOF +config section 'main' + option connection_type 'proxy' + option proxy_config_type 'subscription' + option subscription_url 'https://example.com/sub' + +config section 'query' + option connection_type 'proxy' + option proxy_config_type 'subscription' + option subscription_url '$EQ_URL' +CFGEOF +config_load netshift + +# First run: must migrate both scalar options -> lists and flip the flag. +migrate_legacy_subscription_url_option +if [ "$SUBSCRIPTION_URL_OPTION_MIGRATED" = "1" ]; then + echo 'suburlopt:migrate-flag:OK' +else + echo "suburlopt:migrate-flag:FAIL [$SUBSCRIPTION_URL_OPTION_MIGRATED]" +fi +# The stored values must be preserved. +migrated_val="$(uci -q get netshift.main.subscription_url)" +[ "$migrated_val" = "https://example.com/sub" ] && echo 'suburlopt:migrate-value:OK' || echo "suburlopt:migrate-value:FAIL [$migrated_val]" +# [B1] regression guard: the `=`/`&` URL survives byte-for-byte. +migrated_eq="$(uci -q get netshift.query.subscription_url)" +[ "$migrated_eq" = "$EQ_URL" ] && echo 'suburlopt:migrate-equrl-preserved:OK' || echo "suburlopt:migrate-equrl-preserved:FAIL [$migrated_eq]" + +# After a fresh config_load the LIST path (config_list_foreach) returns each URL, +# and the committed state must be a CLEAN single-element list (no leftover scalar +# option and no duplicate element). +config_load netshift +SUBSCRIPTION_URLS_COLLECTED="" +config_list_foreach main subscription_url _collect_subscription_url_handler +[ "$SUBSCRIPTION_URLS_COLLECTED" = "https://example.com/sub" ] && echo 'suburlopt:migrate-islist:OK' || echo "suburlopt:migrate-islist:FAIL [$SUBSCRIPTION_URLS_COLLECTED]" +SUBSCRIPTION_URLS_COLLECTED="" +config_list_foreach query subscription_url _collect_subscription_url_handler +[ "$SUBSCRIPTION_URLS_COLLECTED" = "$EQ_URL" ] && echo 'suburlopt:migrate-equrl-islist:OK' || echo "suburlopt:migrate-equrl-islist:FAIL [$SUBSCRIPTION_URLS_COLLECTED]" +# Clean single element: `uci show` must render exactly one list value per section +# (no leftover scalar option, no duplicate). uci renders a list element with the +# index-bearing `[0]` syntax; assert exactly one line each. +eq_lines="$(uci -q show netshift.query.subscription_url | grep -c "subscription_url")" +[ "$eq_lines" = "1" ] && echo 'suburlopt:migrate-equrl-single:OK' || echo "suburlopt:migrate-equrl-single:FAIL [$eq_lines]" + +# Second run: idempotent no-op (already a list -> flag stays 0, no churn). +migrate_legacy_subscription_url_option +if [ "$SUBSCRIPTION_URL_OPTION_MIGRATED" = "0" ]; then + echo 'suburlopt:migrate-idempotent:OK' +else + echo "suburlopt:migrate-idempotent:FAIL [$SUBSCRIPTION_URL_OPTION_MIGRATED]" +fi +idem_val="$(uci -q get netshift.main.subscription_url)" +[ "$idem_val" = "https://example.com/sub" ] && echo 'suburlopt:migrate-idempotent-value:OK' || echo "suburlopt:migrate-idempotent-value:FAIL [$idem_val]" +idem_eq="$(uci -q get netshift.query.subscription_url)" +[ "$idem_eq" = "$EQ_URL" ] && echo 'suburlopt:migrate-idempotent-equrl:OK' || echo "suburlopt:migrate-idempotent-equrl:FAIL [$idem_eq]" + +rm -f /etc/config/netshift +echo 'DONE' +SUBOPTEOF + sed -i "s|LIB_DIR_PLACEHOLDER|$lib|g; s|BIN_PATH_PLACEHOLDER|$bin|g" "$drv" + + # Protect any real /etc/config/netshift the container may carry: the + # migration path writes a throwaway one under that exact name. + local netshift_cfg_backup="" + if [ -f /etc/config/netshift ]; then + netshift_cfg_backup="/tmp/netshift-cfg-backup-$$" + cp /etc/config/netshift "$netshift_cfg_backup" + fi + + # Parse in the CURRENT shell (temp file + `while read < "$out"`, NO pipe) so + # pass/fail update the global counters and a suburlopt:*:FAIL actually gates + # the suite (a pipe would run the while-body in a subshell — non-gating). + local sub_out="/tmp/netshift-suburlopt-out-$$" + sh "$drv" > "$sub_out" 2>/dev/null + # FAIL/SKIP tokens carry a trailing " [diagnostic]" suffix, so match with a + # trailing glob (*:FAIL*) — a bare "*:FAIL)" would miss them and silently + # drop the failure, defeating S1's gating. + while IFS= read -r line; do + case "$line" in + *:FAIL*) fail "$line" ;; + *:SKIP*) skip "$line" ;; + *:OK) pass "$line" ;; + DONE) ;; + *) ;; + esac + done < "$sub_out" + rm -f "$drv" "$sub_out" + + if [ -n "$netshift_cfg_backup" ]; then + mv "$netshift_cfg_backup" /etc/config/netshift + else + rm -f /etc/config/netshift + fi +} + # ───────────────────────────────────────────────────────────────── # Test: global_proxy route rule semantics # ───────────────────────────────────────────────────────────────── @@ -5964,6 +6145,7 @@ main() { test_jobstate test_selfheal test_dns_via_outbound + test_sub_url_option test_global_proxy test_check_update_stable test_check_update_extended @@ -5989,6 +6171,7 @@ main() { jobstate) test_jobstate ;; selfheal) test_selfheal ;; dnsdetour) test_dns_via_outbound ;; + suburlopt) test_sub_url_option ;; globalproxy) test_global_proxy ;; stablecheck) test_check_update_stable ;; extcheck) test_check_update_extended ;; @@ -6001,7 +6184,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd unsupported diagnostics subscription insecure rejected jobstate selfheal dnsdetour globalproxy stablecheck extcheck netshiftcheck latesttag selfupdate backupguard" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd unsupported diagnostics subscription insecure rejected jobstate selfheal dnsdetour suburlopt globalproxy stablecheck extcheck netshiftcheck latesttag selfupdate backupguard" exit 1 ;; esac From ba759305104c8b82e04da578d6f7484ddfdcbc2f Mon Sep 17 00:00:00 2001 From: yandexru45 <> Date: Fri, 12 Jun 2026 23:42:45 +0300 Subject: [PATCH 75/75] =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=8C=D1=88=D0=B5=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BA?= =?UTF-8?q?=20=D0=B3=D0=B8=D1=82=D1=85=D0=B0=D0=B1=20=D0=B0=D0=BF=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/architect-orchestrator.md | 54 +++++++ docs/agent-rules/memory/code-reviewer.md | 2 + .../memory/shell-backend-developer.md | 52 +++++++ install.sh | 107 ++++++++++---- netshift/files/usr/lib/constants.sh | 9 ++ netshift/files/usr/lib/updater.sh | 112 ++++++++++++-- tests/docker-compose.yml | 3 +- tests/entrypoint.sh | 139 +++++++++++++++++- 8 files changed, 431 insertions(+), 47 deletions(-) diff --git a/docs/agent-rules/memory/architect-orchestrator.md b/docs/agent-rules/memory/architect-orchestrator.md index a348c04f..282fd0b1 100644 --- a/docs/agent-rules/memory/architect-orchestrator.md +++ b/docs/agent-rules/memory/architect-orchestrator.md @@ -1325,3 +1325,57 @@ save+`sing-box check` -> cron jobs -> start sing-box -> dnsmasq_configure -> gating suburlopt tokens incl. 4 =-URL guards); whole-chain verified (option config -> has_outbound_section TRUE -> gen -> sing-box check). Runtime contract intact (UCI schema = back-compat repr normalization only). + +## GitHub API rate-limit — research + task-049 (2026-06-12) + +- HOW IT WORKS (official docs, verified): anonymous api.github.com = 60 req/HOUR + per IP (authed=5000). On 403/429 the body is {"message":"API rate limit + exceeded for <IP>..."} and headers x-ratelimit-remaining:0 + x-ratelimit-reset + (UTC epoch). GET /rate_limit shows budget and does NOT cost primary quota. +- WHY users hit it a lot: limit is PER IP. Routers behind CGNAT / shared ISP + IPs / shared-VPN egress share ONE IP's 60/hour with many strangers -> the + budget is often already drained by others. Not the user's fault; we're + anonymous so we can't raise it without a token. +- KEY LEVER (PROVEN on hardware, curl): github.com/<repo>/releases/latest is + served by the github.com FRONTEND, NOT the rate-limited API. It 302-redirects + to /releases/tag/<tag>. `curl -sI -o /dev/null -w '%{redirect_url}'` returns + `.../releases/tag/0.8.9` -> tag extracted WITHOUT touching api.github.com. + And github.com/<repo>/releases/download/<tag>/<asset> 302s to the CDN + (release-assets.githubusercontent.com) -> direct asset download, no API. + (We already use this redirect path for SRS_MAIN_URL = releases/latest/download.) +- BUSYBOX wget on-device is STRIPPED: no -S, no --max-redirect, can't read + Location/headers. So tag extraction MUST use curl (a hard DEPENDS: +curl), + via %{redirect_url} (or -w %{url_effective} with -L). updates_http_get_once + already prefers curl. +- ASSET NAMING is deterministic from the tag: ipk = netshift-<ver>-r1-all.ipk, + luci-app-netshift-<ver>-r1-all.ipk, luci-i18n-netshift-ru-<ver>.ipk; apk = + netshift-<ver>-r1.apk, luci-app-netshift-<ver>-r1.apk, + luci-i18n-netshift-ru-<ver>.apk. (<ver> = tag.) All 302 on the direct path. +- SCOPE DECISION (task-049): migrate the 3 NETSHIFT-repo touchpoints off + api.github.com -> redirect path: version check (updater.sh:1638 + updates_netshift_latest_tag), self-update asset download + (updater.sh:1657 _updates_self_update_download_assets), and install.sh:259. + KEEP the sing-box-EXTENDED path (updater.sh:553 releases?per_page=30) on the + API + proxy-fallback: it genuinely needs the releases LIST (draft/prerelease + flags + per-arch asset selection) which a redirect can't give; it's also a + rarer, on-demand action. Proxy-fallback stays as the safety net there. +- ALSO: honor x-ratelimit-reset / show honest "GitHub limit, retry after HH:MM" + instead of generic error; optionally TTL-cache the latest tag. (Secondary.) +- itdoginfo's podkop historically had the SAME complaint class; this redirect + approach is the standard fix. + +## task-049 CLOSED — APPROVED (2026-06-12) +- Implemented: redirect-first tag fetch (updates_github_resolve_redirect + + updates_netshift_latest_tag), deterministic asset-URL download + (updates_netshift_asset_filename + _updates_self_update_download_assets), and + install.sh redirect path (+ shared download_release_asset helper). API path + kept as graceful fallback everywhere; sing-box-extended path untouched. +- Gates: shellcheck -S error clean; smoke 190->196/0 (+6 ghredirect, gating); + self-proved. Tag is rejected if empty/`/`-containing (injection/traversal + guard) and only used quoted in URL strings (no eval) — reviewed safe. +- Review: 1 round, APPROVED. Only finding [M1] = a comment typo + (updates_http_get_once -> updates_download_to_file); fixed during review, + shellcheck re-clean, ghredirect re-run 6/0. +- Net result for the user complaint: the normal version-check/self-update/install + path no longer touches the 60/hr-per-IP api.github.com, so CGNAT/shared-IP + rate-limit errors should largely disappear; API remains the fallback. diff --git a/docs/agent-rules/memory/code-reviewer.md b/docs/agent-rules/memory/code-reviewer.md index a127dad5..7549d228 100644 --- a/docs/agent-rules/memory/code-reviewer.md +++ b/docs/agent-rules/memory/code-reviewer.md @@ -71,3 +71,5 @@ append recurring findings; keep under ~200 lines. - UCI option→list rewrites: the `uci add_list "key=value"` CLI form splits on the FIRST `=` and SILENTLY LOSES query-string URLs (`?token=abc&x=1`) — reproduced on hardware (rc=1, list empty). Require the `uci_add_list <cfg> <sec> <opt> "<val>"` SHELL HELPER (separate-arg, preserves `=`/`&`). For delete-then-add rewrites, verify a failed add RESTORES the scalar AND that the change-flag gates the `uci commit` (an uncommitted in-memory delete must never persist). (task-048 [B1]) - When RE-reviewing a fix round, also diff the developer's MEMORY note: it is frequently written against the PRE-fix code and re-seeds the very anti-pattern that was just fixed (task-048 [M2]: note still showed the `key=value` form + "non-gating piped-while" after both were fixed). Flag a stale memory note as a (minor) condition. - Test-gating landmine: a smoke test whose assertions run on the RHS of a pipe (`cmd | while read; pass/fail`) does NOT gate CI (subshell counter loss) — a FAIL token prints red but the suite exits 0. Require current-shell parsing (`while read < tmpfile`). The 178→190 count jump when task-048 fixed this is the tell. (task-048 [S1]) + +- Rate-limit avoidance via redirect path (task-049): version-check/self-update/install can read the latest tag from `github.com/<repo>/releases/latest` (302 -> /releases/tag/<tag>, served by the github.com FRONTEND, NOT the 60/hr-per-IP api.github.com) instead of the API. Tag extracted with `curl -sI -o /dev/null -w '%{redirect_url}'` then `case`/param-expansion `${r##*/releases/tag/}` — when reviewing such code REQUIRE: (a) the tag is rejected if empty OR `/`-containing (path-traversal/injection guard) via `case "$tag" in ''|*/*) tag="" ;;`; (b) the tag is only used quoted inside a URL string / passed quoted to helpers, never `eval`'d or used as a bare filesystem path; (c) curl-absent / non-match degrades to the API fallback (no hard-fail/exit); (d) the file-download helper uses `curl -fsSL`/`-L` so the CDN 302 on `releases/download/<tag>/<asset>` is followed. busybox wget on-device is STRIPPED (no -S/--max-redirect/header read) so redirect reading MUST use curl (hard +curl dep). Keep the sing-box-EXTENDED releases-LIST path on the API (a redirect can't give draft/prerelease/per-arch). diff --git a/docs/agent-rules/memory/shell-backend-developer.md b/docs/agent-rules/memory/shell-backend-developer.md index af507add..8b02bc62 100644 --- a/docs/agent-rules/memory/shell-backend-developer.md +++ b/docs/agent-rules/memory/shell-backend-developer.md @@ -1567,3 +1567,55 @@ findings; keep under ~200 lines. existing key's representation (option→list, back-compat). - code-review round 2: APPROVED WITH CONDITIONS — [B1]/[S1]/[M1] all resolved; the only condition was fixing THIS stale memory note (done). + +## task-049: avoid api.github.com rate-limit via github.com redirect (curl) + +- Anonymous api.github.com = 60 req/HOUR/IP; CGNAT/shared-IP/shared-VPN routers + share that budget → frequent "API rate limit exceeded". LEVER (proven on HW): + github.com/<repo>/releases/latest is the github.com FRONTEND (NOT the API) and + 302-redirects to /releases/tag/<tag>; releases/download/<tag>/<asset> 302s to + the CDN. Neither hits the rate-limited API. +- New constants (constants.sh, repo slug ONLY here): NETSHIFT_REPO_RELEASES_LATEST_URL + (.../releases/latest), NETSHIFT_REPO_RELEASES_DOWNLOAD_BASE (.../releases/download). + Kept NETSHIFT_RELEASE_API_URL as the fallback. +- New STUBBABLE resolver `updates_github_resolve_redirect <url>` (updater.sh): + `command -v curl || return 1; curl -sI -o /dev/null -w '%{redirect_url}' + --connect-timeout 5 -m 15 -A 'netshift-updater' "$url"`. busybox wget is + STRIPPED (no -S/header read) so tag extraction MUST be curl; curl is a hard dep. +- `updates_netshift_latest_tag` rewrite: PRIMARY resolve redirect, parse with + `case "$redirect" in */releases/tag/*) tag="${redirect##*/releases/tag/}"; + case "$tag" in ''|*/*) tag="" ;; esac ;; *) tag="" ;; esac` (NO Oniguruma) — + a trailing-slash redirect leaves a `/` in tag → rejected → empty → fallback. + FALLBACK = the task-047 api.github.com + `jq -r '.tag_name // empty'` path, + kept intact. Bare-tag/non-zero contract preserved (feeds updates_check_netshift + + self-update worker). +- `updates_netshift_asset_filename <pkg> <tag> <ext>` single-source naming helper: + i18n = `<pkg>-<tag>.<ext>` (no -r1); core/luci ipk = `<pkg>-<tag>-r1-all.ipk`, + apk = `<pkg>-<tag>-r1.apk`. `_updates_self_update_download_assets` now resolves + the tag and builds deterministic `$DOWNLOAD_BASE/<tag>/<filename>` URLs (core+luci + always, i18n only if updates_pkg_is_installed), downloads via updates_http_get_once + (follows the 302 to CDN), got_core=1 when core `-s "$dest"`. OLD api-JSON grep -o + loop KEPT verbatim as the else branch when tag unresolved. +- install.sh: added RELEASES_LATEST_REDIRECT + RELEASES_DOWNLOAD_BASE literals + (install.sh has its own REPO, not constants.sh). PRIMARY: curl -sI redirect → + case/param-expansion tag → deterministic releases/download/<tag>/<asset> URLs + (core+luci, RU i18n if pkg_is_installed). FALLBACK: existing API scrape + the + "API rate limit" message kept intact. Factored the retry-download into a new + `download_release_asset url filename` helper reused by both paths. name-prefix + install loop semantics unchanged. +- GOTCHA: the EXISTING test_netshift_latest_tag driver had to ALSO stub + `updates_github_resolve_redirect() { printf ''; }` — else the new primary would + shell out to real curl in CI (network) and bypass the API path that test targets. +- EXTENDED PATH UNTOUCHED: updates_fetch_sing_box_extended_releases + (releases?per_page=30) + updates_extended_release_* — they need the releases LIST + (draft/prerelease/per-arch) a redirect can't give. Left on API + proxy-fallback. +- New smoke test `test_github_redirect_tag` (alias `ghredirect`, 6 tokens): stubs + the resolver + updates_http_get_once, parses driver output in the CURRENT shell + (gates). tag-from-redirect, tag-trailing-slash-rejected (→fallback empty), + nonmatch-falls-back (login URL→API stub→tag), ratelimit-empty (curl-absent + + rate-limit object→empty+nonzero), asset-ipk, asset-apk. Registered all 5 points. +- SELF-PROVEN: `if false && [ -n "$tag" ]` on the primary return made + ghredirect:tag-from-redirect FAIL (5/1), restored→6/0. +- Gates: shellcheck -S error clean (bin+libs+install.sh); `smoke-tests all` + 190→196 passed / 0 failed (+6 ghredirect). NO sacred constant/port/mark/path/ + schema/frontend change. diff --git a/install.sh b/install.sh index 903c2d06..baaf5895 100755 --- a/install.sh +++ b/install.sh @@ -2,6 +2,12 @@ # shellcheck shell=dash REPO="https://api.github.com/repos/yandexru45/netshift/releases/latest" +# github.com FRONTEND redirect path (NOT the rate-limited api.github.com). +# /releases/latest 302s to /releases/tag/<tag>; /releases/download/<tag>/<asset> +# 302s to the CDN. Primary install path so CGNAT / shared-IP routers avoid the +# 60/hour/IP API limit; REPO stays as the fallback. +RELEASES_LATEST_REDIRECT="https://github.com/yandexru45/netshift/releases/latest" +RELEASES_DOWNLOAD_BASE="https://github.com/yandexru45/netshift/releases/download" DOWNLOAD_DIR="/tmp/netshift" COUNT=3 @@ -241,6 +247,30 @@ migrate_from_podkop() { msg "Your old config is preserved at /etc/config/podkop.bak.pre-netshift" } +# Download one release asset URL into $DOWNLOAD_DIR with retry. POSIX sh. +download_release_asset() { + url="$1" + filename="$2" + filepath="$DOWNLOAD_DIR/$filename" + + attempt=0 + while [ $attempt -lt $COUNT ]; do + msg "Download $filename (count $((attempt + 1)))..." + if wget -q -O "$filepath" "$url"; then + if [ -s "$filepath" ]; then + msg "$filename successfully downloaded" + return 0 + fi + fi + msg "Download error for $filename. Retrying..." + rm -f "$filepath" + attempt=$((attempt + 1)) + done + + msg "Failed to download $filename after $COUNT attempts" + return 1 +} + main() { check_system sing_box @@ -255,44 +285,63 @@ main() { msg "Installing NetShift..." fi - if command -v curl >/dev/null 2>&1; then - check_response=$(curl -s "https://api.github.com/repos/yandexru45/netshift/releases/latest") - - if echo "$check_response" | grep -q 'API rate limit '; then - msg "You've reached the GitHub rate limit. Repeat in five minutes." - exit 1 - fi - fi - - local grep_url_pattern + local ext release_tag redirect_url if [ "$PKG_IS_APK" -eq 1 ]; then - grep_url_pattern='https://[^"[:space:]]*\.apk' + ext="apk" else - grep_url_pattern='https://[^"[:space:]]*\.ipk' + ext="ipk" fi - wget -qO- "$REPO" | grep -o "$grep_url_pattern" | while read -r url; do - filename=$(basename "$url") - filepath="$DOWNLOAD_DIR/$filename" + # PRIMARY: resolve the latest tag via the github.com frontend redirect (no + # api.github.com hit → not subject to the 60/hour/IP rate limit), then build + # the deterministic releases/download/<tag>/<asset> URLs and download them. + release_tag="" + if command -v curl >/dev/null 2>&1; then + redirect_url=$(curl -sI -o /dev/null -w '%{redirect_url}' \ + --connect-timeout 5 -m 15 -A 'netshift-installer' \ + "$RELEASES_LATEST_REDIRECT" 2>/dev/null) + case "$redirect_url" in + */releases/tag/*) + release_tag="${redirect_url##*/releases/tag/}" + case "$release_tag" in '' | */*) release_tag="" ;; esac + ;; + esac + fi - attempt=0 - while [ $attempt -lt $COUNT ]; do - msg "Download $filename (count $((attempt+1)))..." - if wget -q -O "$filepath" "$url"; then - if [ -s "$filepath" ]; then - msg "$filename successfully downloaded" - break - fi + if [ -n "$release_tag" ]; then + msg "Latest NetShift release: $release_tag (direct download, no GitHub API)" + for pkg in netshift luci-app-netshift; do + if [ "$ext" = "ipk" ]; then + filename="${pkg}-${release_tag}-r1-all.${ext}" + else + filename="${pkg}-${release_tag}-r1.${ext}" fi - msg "Download error for $filename. Retrying..." - rm -f "$filepath" - attempt=$((attempt+1)) + download_release_asset "$RELEASES_DOWNLOAD_BASE/$release_tag/$filename" "$filename" done + # RU i18n only if already installed (mirrors the install flow below). + if pkg_is_installed luci-i18n-netshift-ru; then + filename="luci-i18n-netshift-ru-${release_tag}.${ext}" + download_release_asset "$RELEASES_DOWNLOAD_BASE/$release_tag/$filename" "$filename" + fi + else + # FALLBACK: scrape the api.github.com release JSON for .ipk/.apk URLs. + if command -v curl >/dev/null 2>&1; then + check_response=$(curl -s "$REPO") - if [ $attempt -eq $COUNT ]; then - msg "Failed to download $filename after $COUNT attempts" + if echo "$check_response" | grep -q 'API rate limit '; then + msg "You've reached the GitHub rate limit. Repeat in five minutes." + exit 1 + fi fi - done + + local grep_url_pattern + grep_url_pattern="https://[^\"[:space:]]*\.${ext}" + + wget -qO- "$REPO" | grep -o "$grep_url_pattern" | while read -r url; do + filename=$(basename "$url") + download_release_asset "$url" "$filename" + done + fi # Check if any files were downloaded if ! ls "$DOWNLOAD_DIR"/*netshift* >/dev/null 2>&1; then diff --git a/netshift/files/usr/lib/constants.sh b/netshift/files/usr/lib/constants.sh index 3d9063a2..9cd5fb0f 100644 --- a/netshift/files/usr/lib/constants.sh +++ b/netshift/files/usr/lib/constants.sh @@ -92,6 +92,15 @@ UPDATES_LIBCRONET_LIB="/usr/lib/libcronet.so" # API for NetShift itself (same endpoint install.sh and get_system_info use); # the self-update worker downloads the release .ipk/.apk assets from it. NETSHIFT_RELEASE_API_URL="https://api.github.com/repos/yandexru45/netshift/releases/latest" +# GitHub FRONTEND (github.com, NOT the rate-limited api.github.com) redirect path +# for the NetShift repo. /releases/latest 302-redirects to /releases/tag/<tag> +# (resolve with curl -w '%{redirect_url}' — no API hit, not subject to the +# 60/hour/IP anonymous API limit); /releases/download/<tag>/<asset> 302s to the +# CDN for direct asset download. Primary path for version-check + self-update; +# NETSHIFT_RELEASE_API_URL stays as the graceful fallback. Repo slug lives here +# only — do not hardcode it elsewhere. +NETSHIFT_REPO_RELEASES_LATEST_URL="https://github.com/yandexru45/netshift/releases/latest" +NETSHIFT_REPO_RELEASES_DOWNLOAD_BASE="https://github.com/yandexru45/netshift/releases/download" # tmpfs scratch dir for the self-update download (release packages) — RAM, never # the tiny overlay; reaped on success and on reboot. UPDATES_NETSHIFT_DOWNLOAD_DIR="/tmp/netshift/selfupdate" diff --git a/netshift/files/usr/lib/updater.sh b/netshift/files/usr/lib/updater.sh index 1ede17dc..0c904a42 100644 --- a/netshift/files/usr/lib/updater.sh +++ b/netshift/files/usr/lib/updater.sh @@ -1626,15 +1626,45 @@ updates_self_update_netshift() { return "$rc" } +# Resolve a URL's HTTP redirect target via curl WITHOUT hitting the rate-limited +# API or downloading the body. Echoes the redirect URL (empty if curl absent or +# no redirect). Stubbable in tests. +updates_github_resolve_redirect() { + local url="$1" + command -v curl >/dev/null 2>&1 || return 1 + curl -sI -o /dev/null -w '%{redirect_url}' --connect-timeout 5 -m 15 -A 'netshift-updater' "$url" 2>/dev/null +} + # Echoes the GitHub latest-release tag for NetShift (e.g. "0.8.8"), or nothing. -# Reuses the same API endpoint as get_system_info / install.sh. Parsed with jq -# (NOT grep/cut): GitHub may return the release object pretty-printed OR minified -# (single line); a field-positional grep|cut grabs the first key's value (the -# release "url") on minified JSON, which caused a false "outdated" + a self-update -# that downloaded a garbage "version". jq is format-independent. +# PRIMARY: resolve the github.com frontend redirect of /releases/latest — it +# 302s to /releases/tag/<tag>. That frontend is NOT the 60/hour-per-IP +# api.github.com, so it sidesteps the anonymous rate limit entirely (the common +# failure on CGNAT / shared-IP / shared-VPN-egress routers). FALLBACK: the +# api.github.com release object parsed with jq (task-047) so a curl-less box or a +# changed-redirect github still degrades gracefully instead of hard-failing. +# jq is format-independent (minified or pretty); a field-positional grep|cut +# grabbed the wrong key on minified JSON, causing a false "outdated". +# Bare tag on success / non-zero otherwise (contract consumed by +# updates_check_netshift and the self-update worker). updates_netshift_latest_tag() { - local response tag + local response tag redirect + + # PRIMARY: github.com/<repo>/releases/latest 302-redirects to + # /releases/tag/<tag>. Parse with case/param-expansion (no Oniguruma). + redirect="$(updates_github_resolve_redirect "$NETSHIFT_REPO_RELEASES_LATEST_URL")" + case "$redirect" in + */releases/tag/*) + tag="${redirect##*/releases/tag/}" + case "$tag" in '' | */*) tag="" ;; esac + ;; + *) tag="" ;; + esac + if [ -n "$tag" ]; then + printf '%s' "$tag" + return 0 + fi + # FALLBACK: api.github.com (rate-limited) parsed with jq. response="$(updates_http_get_once "$NETSHIFT_RELEASE_API_URL" "")" if [ -z "$response" ]; then return 1 @@ -1645,25 +1675,75 @@ updates_netshift_latest_tag() { printf '%s' "$tag" } -# Downloads the NetShift release assets matching the package-name prefixes for -# the active package manager into $dir. Echoes nothing; returns 0 if at least -# the core "netshift" package was downloaded, non-zero otherwise. The asset URL -# list comes from the same latest-release JSON, filtered to .ipk or .apk by the -# package manager (busybox grep -o, no jq array walk required). +# Echo the deterministic release asset filename for a package + tag + ext. +# ipk core/luci carry "-r1-all"; apk core/luci carry "-r1"; the i18n package +# carries neither suffix (just "<pkg>-<tag>.<ext>"). Single source of the asset +# naming pattern so it lives in one place, not scattered. +updates_netshift_asset_filename() { + local pkg="$1" tag="$2" ext="$3" + case "$pkg" in + "$UPDATES_NETSHIFT_PKG_I18N_RU") printf '%s-%s.%s' "$pkg" "$tag" "$ext" ;; + *) + if [ "$ext" = "ipk" ]; then + printf '%s-%s-r1-all.%s' "$pkg" "$tag" "$ext" + else + printf '%s-%s-r1.%s' "$pkg" "$tag" "$ext" + fi + ;; + esac +} + +# Downloads the NetShift release assets for the active package manager into $dir. +# Echoes nothing; returns 0 if at least the core "netshift" package was +# downloaded, non-zero otherwise. +# PRIMARY: resolve the latest tag (redirect-based, rate-limit-free) and build the +# deterministic github.com/<repo>/releases/download/<tag>/<asset> URLs — the +# CDN 302 is followed by updates_download_to_file (curl -L / wget both follow it). +# FALLBACK: if the tag can't be resolved, scrape the api.github.com release JSON +# for .ipk/.apk URLs (busybox grep -o) as before, so a curl-less box still works. _updates_self_update_download_assets() { local dir="$1" local response ext pattern url filename dest attempt got_core=0 - - response="$(updates_http_get_once "$NETSHIFT_RELEASE_API_URL" "")" - if [ -z "$response" ]; then - return 1 - fi + local tag pkg if updates_pkg_is_apk; then ext="apk" else ext="ipk" fi + + tag="$(updates_netshift_latest_tag)" + if [ -n "$tag" ]; then + # Direct deterministic asset URLs (no API). Core + luci always; the RU + # i18n package only if already installed. + for pkg in "$UPDATES_NETSHIFT_PKG_CORE" "$UPDATES_NETSHIFT_PKG_LUCI" "$UPDATES_NETSHIFT_PKG_I18N_RU"; do + if [ "$pkg" = "$UPDATES_NETSHIFT_PKG_I18N_RU" ]; then + updates_pkg_is_installed "$UPDATES_NETSHIFT_PKG_I18N_RU" || continue + fi + filename="$(updates_netshift_asset_filename "$pkg" "$tag" "$ext")" + url="$NETSHIFT_REPO_RELEASES_DOWNLOAD_BASE/$tag/$filename" + dest="$dir/$filename" + attempt=0 + while [ "$attempt" -lt 3 ]; do + if updates_download_to_file "$url" "$dest"; then + break + fi + rm -f "$dest" 2>/dev/null + attempt=$((attempt + 1)) + done + if [ "$pkg" = "$UPDATES_NETSHIFT_PKG_CORE" ] && [ -s "$dest" ]; then + got_core=1 + fi + done + [ "$got_core" -eq 1 ] + return $? + fi + + # FALLBACK: scrape the API release JSON for direct asset URLs. + response="$(updates_http_get_once "$NETSHIFT_RELEASE_API_URL" "")" + if [ -z "$response" ]; then + return 1 + fi pattern="https://[^\"[:space:]]*\.${ext}" # Iterate the matching browser_download_url values. Only keep assets whose diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 0b705dec..6387be8d 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -11,7 +11,8 @@ # Test names: all, deps, syntax, config, helpers, jq, cm, sb, nft, # nftv6, selmark, isolation, monfd, unsupported, diagnostics, subscription, insecure, rejected, # jobstate, selfheal, dnsdetour, suburlopt, globalproxy, stablecheck, -# extcheck, netshiftcheck, latesttag, selfupdate, backupguard +# extcheck, netshiftcheck, latesttag, ghredirect, selfupdate, +# backupguard # ────────────────────────────────────────────────────────────────── services: diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh index 051f2e9a..f5aa3787 100755 --- a/tests/entrypoint.sh +++ b/tests/entrypoint.sh @@ -5529,6 +5529,9 @@ updates_log() { :; } . "DRV_UPDATER" NETSHIFT_RELEASE_API_URL="https://api.test/latest" NETSHIFT_VERSION="$STUBLT_INSTALLED" +# Force the API-fallback path this test targets: the redirect resolver returns +# empty so updates_netshift_latest_tag falls back to the stubbed API body. +updates_github_resolve_redirect() { printf ''; } updates_http_get_once() { printf '%s' "$STUBLT_BODY"; } "$STUBLT_FN" DRVEOF @@ -5603,6 +5606,138 @@ DRVEOF rm -rf "$work" } +# ───────────────────────────────────────────────────────────────── +# Test: GitHub redirect-based latest-tag + deterministic asset URLs (task-049) +# ───────────────────────────────────────────────────────────────── +# Sidestepping the api.github.com 60/hour/IP rate limit: the version-check + the +# self-update asset download now resolve github.com/<repo>/releases/latest via a +# redirect (curl -w '%{redirect_url}') → /releases/tag/<tag>, with the API + jq +# path kept only as a graceful fallback. The network boundary is STUBBED here +# (override updates_github_resolve_redirect / updates_http_get_once), so no curl +# shell-out and no real network in CI. Synthetic data only. +test_github_redirect_tag() { + header "GitHub redirect latest-tag + asset URLs (task-049)" + + if ! command -v jq > /dev/null 2>&1; then + skip "jq not available" + return + fi + + local updater="${NETSHIFT_LIB_DIR}/updater.sh" + if [ ! -r "$updater" ]; then + skip "updater.sh not found in ${NETSHIFT_LIB_DIR}" + return + fi + + local work="/tmp/netshift-ghredirect-$$" + rm -rf "$work" + mkdir -p "$work" + + # Driver: source helpers.sh + updater.sh, silence logging, pin the redirect + # + API constants, OVERRIDE the redirect resolver ($STUBGR_REDIRECT) and the + # API boundary ($STUBGR_BODY), then run the function/expr named in $STUBGR_FN. + local drv="$work/driver.sh" + cat > "$drv" << 'DRVEOF' +log() { :; } +echolog() { :; } +nolog() { :; } +updates_log() { :; } +. "DRV_HELPERS" +. "DRV_UPDATER" +NETSHIFT_REPO_RELEASES_LATEST_URL="https://github.com/yandexru45/netshift/releases/latest" +NETSHIFT_REPO_RELEASES_DOWNLOAD_BASE="https://github.com/yandexru45/netshift/releases/download" +NETSHIFT_RELEASE_API_URL="https://api.test/latest" +UPDATES_NETSHIFT_PKG_CORE="netshift" +UPDATES_NETSHIFT_PKG_LUCI="luci-app-netshift" +UPDATES_NETSHIFT_PKG_I18N_RU="luci-i18n-netshift-ru" +updates_github_resolve_redirect() { printf '%s' "$STUBGR_REDIRECT"; } +updates_http_get_once() { printf '%s' "$STUBGR_BODY"; } +eval "$STUBGR_FN" +DRVEOF + sed -i "s|DRV_UPDATER|$updater|g;s|DRV_HELPERS|${NETSHIFT_LIB_DIR}/helpers.sh|g" "$drv" + + local out="$work/out.txt" + local rc_file="$work/rc.txt" + run_gr() { + ash "$drv" > "$out" 2>/dev/null && printf '0' > "$rc_file" || printf '%s' "$?" > "$rc_file" + } + + export STUBGR_FN="updates_netshift_latest_tag" + export STUBGR_REDIRECT="" + export STUBGR_BODY="" + + # ── Case 1: clean redirect → tag 0.8.9 (primary path, no API) ──────────── + export STUBGR_REDIRECT="https://github.com/yandexru45/netshift/releases/tag/0.8.9" + export STUBGR_BODY="" + run_gr + if [ "$(cat "$out" 2>/dev/null)" = "0.8.9" ] && [ "$(cat "$rc_file" 2>/dev/null)" = "0" ]; then + pass "ghredirect:tag-from-redirect:OK" + else + fail "ghredirect:tag-from-redirect:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)" + fi + + # ── Case 2: trailing-slash redirect → parse rejects (slash) → falls back ── + # A trailing slash makes the stripped tag contain "/", which the guard + # rejects; with NO API body it then yields empty + non-zero. + export STUBGR_REDIRECT="https://github.com/yandexru45/netshift/releases/tag/0.8.9/" + export STUBGR_BODY="" + run_gr + if [ -z "$(cat "$out" 2>/dev/null)" ] && [ "$(cat "$rc_file" 2>/dev/null)" != "0" ]; then + pass "ghredirect:tag-trailing-slash-rejected:OK" + else + fail "ghredirect:tag-trailing-slash-rejected:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)" + fi + + # ── Case 3: non-matching redirect (login page) → primary empty → API + # FALLBACK returns the release object → still yields the tag. ───────────── + export STUBGR_REDIRECT="https://github.com/login?return_to=%2Fyandexru45%2Fnetshift" + export STUBGR_BODY='{"url":"https://api.github.com/repos/yandexru45/netshift/releases/1","tag_name":"0.8.9"}' + run_gr + if [ "$(cat "$out" 2>/dev/null)" = "0.8.9" ] && [ "$(cat "$rc_file" 2>/dev/null)" = "0" ]; then + pass "ghredirect:nonmatch-falls-back:OK" + else + fail "ghredirect:nonmatch-falls-back:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)" + fi + + # ── Case 4: curl-absent (resolver empty) + API rate-limit object → empty + + # non-zero (honest failure, no false tag). ─────────────────────────────── + export STUBGR_REDIRECT="" + export STUBGR_BODY='{"message":"API rate limit exceeded for 1.2.3.4"}' + run_gr + if [ -z "$(cat "$out" 2>/dev/null)" ] && [ "$(cat "$rc_file" 2>/dev/null)" != "0" ]; then + pass "ghredirect:ratelimit-empty:OK" + else + fail "ghredirect:ratelimit-empty:FAIL" "got=[$(cat "$out" 2>/dev/null)] rc=$(cat "$rc_file" 2>/dev/null)" + fi + + # ── Case 5: asset-URL builder, ipk → deterministic names ───────────────── + export STUBGR_REDIRECT="" + export STUBGR_BODY="" + export STUBGR_FN='c="$(updates_netshift_asset_filename netshift 0.8.9 ipk)"; l="$(updates_netshift_asset_filename luci-app-netshift 0.8.9 ipk)"; i="$(updates_netshift_asset_filename luci-i18n-netshift-ru 0.8.9 ipk)"; printf "%s\n%s\n%s\n" "$c" "$l" "$i"' + run_gr + if [ "$(sed -n 1p "$out" 2>/dev/null)" = "netshift-0.8.9-r1-all.ipk" ] && + [ "$(sed -n 2p "$out" 2>/dev/null)" = "luci-app-netshift-0.8.9-r1-all.ipk" ] && + [ "$(sed -n 3p "$out" 2>/dev/null)" = "luci-i18n-netshift-ru-0.8.9.ipk" ]; then + pass "ghredirect:asset-ipk:OK" + else + fail "ghredirect:asset-ipk:FAIL" "got=[$(cat "$out" 2>/dev/null)]" + fi + + # ── Case 6: asset-URL builder, apk → deterministic names ───────────────── + export STUBGR_FN='c="$(updates_netshift_asset_filename netshift 0.8.9 apk)"; l="$(updates_netshift_asset_filename luci-app-netshift 0.8.9 apk)"; i="$(updates_netshift_asset_filename luci-i18n-netshift-ru 0.8.9 apk)"; printf "%s\n%s\n%s\n" "$c" "$l" "$i"' + run_gr + if [ "$(sed -n 1p "$out" 2>/dev/null)" = "netshift-0.8.9-r1.apk" ] && + [ "$(sed -n 2p "$out" 2>/dev/null)" = "luci-app-netshift-0.8.9-r1.apk" ] && + [ "$(sed -n 3p "$out" 2>/dev/null)" = "luci-i18n-netshift-ru-0.8.9.apk" ]; then + pass "ghredirect:asset-apk:OK" + else + fail "ghredirect:asset-apk:FAIL" "got=[$(cat "$out" 2>/dev/null)]" + fi + + unset STUBGR_FN STUBGR_REDIRECT STUBGR_BODY + rm -rf "$work" +} + # ───────────────────────────────────────────────────────────────── # Test: NetShift self-update (task-017) # ───────────────────────────────────────────────────────────────── @@ -6151,6 +6286,7 @@ main() { test_check_update_extended test_check_update_netshift test_netshift_latest_tag + test_github_redirect_tag test_self_update_netshift test_backup_integrity ;; @@ -6177,6 +6313,7 @@ main() { extcheck) test_check_update_extended ;; netshiftcheck) test_check_update_netshift ;; latesttag) test_netshift_latest_tag ;; + ghredirect) test_github_redirect_tag ;; selfupdate) test_self_update_netshift ;; backupguard) test_backup_integrity ;; jq) test_jq_helpers ;; @@ -6184,7 +6321,7 @@ main() { sb) test_sing_box_config ;; *) echo "Unknown test: $target" - echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd unsupported diagnostics subscription insecure rejected jobstate selfheal dnsdetour suburlopt globalproxy stablecheck extcheck netshiftcheck latesttag selfupdate backupguard" + echo "Available: all deps syntax config helpers jq cm sb nft nftv6 selmark isolation monfd unsupported diagnostics subscription insecure rejected jobstate selfheal dnsdetour suburlopt globalproxy stablecheck extcheck netshiftcheck latesttag ghredirect selfupdate backupguard" exit 1 ;; esac