add HWID and subscription support

This commit is contained in:
yandexru45
2026-03-07 14:55:11 +03:00
parent 9a015bbde4
commit 4202530585
13 changed files with 563 additions and 21 deletions

View File

@@ -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)
- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS)

View File

@@ -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)',

View File

@@ -154,6 +154,41 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
],
};
}
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') {

View File

@@ -84,4 +84,6 @@ export const PodkopShellMethods = {
callBaseMethod<Podkop.GetSystemInfo>(
Podkop.AvailableMethods.GET_SYSTEM_INFO,
),
subscriptionUpdate: async () =>
callBaseMethod<unknown>(Podkop.AvailableMethods.SUBSCRIPTION_UPDATE),
};

View File

@@ -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;

View File

@@ -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."

View File

@@ -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)",

View File

@@ -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") +
' <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]) => {
@@ -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;

View File

@@ -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'
#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'

View File

@@ -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
;;

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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"
}