From 4202530585e4e26681184699c1a03a7fb439d65f Mon Sep 17 00:00:00 2001 From: yandexru45 Date: Sat, 7 Mar 2026 14:55:11 +0300 Subject: [PATCH] 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 a938bc1..2b8ac9c 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 6f840d1..19e0907 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 6696d66..554caa6 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 e9c2940..6b0337a 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 672ec0c..e93ab03 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 6376d7c..6fde517 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 c8fae38..6ff4711 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 dc619bc..16f71fd 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 27003cf..ff8d976 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 e26164a..7dd2c57 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 563ad0c..68bc3b5 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 c25edb8..24f656c 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 6887e20..7315696 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" +}