add HWID and subscription support
This commit is contained in:
55
README.md
55
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)
|
||||
|
||||
[](https://deepwiki.com/itdoginfo/podkop)
|
||||
- [ ] Интеграционные тесты бекенда (OpenWrt rootfs + BATS)
|
||||
@@ -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)',
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -84,4 +84,6 @@ export const PodkopShellMethods = {
|
||||
callBaseMethod<Podkop.GetSystemInfo>(
|
||||
Podkop.AvailableMethods.GET_SYSTEM_INFO,
|
||||
),
|
||||
subscriptionUpdate: async () =>
|
||||
callBaseMethod<unknown>(Podkop.AvailableMethods.SUBSCRIPTION_UPDATE),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
10
install.sh
10
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."
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
;;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user