Краткое описание
✅ Subscription link — это уникальная ссылка, по которой клиент (например, мобильный VPN-/прокси-клиент или панель управления) запрашивает актуальный список прокси-конфигураций для пользователя.
Это не просто однократный экспорт — при каждом запросе клиент получает свежие данные (адреса, ключи, параметры).
Это стандарт, который используется в панелях типа 3x-UI, Remnawave и других, чтобы не рассылать вручную каждому пользователю обновлённые настройки
Проблема, которую решает
✅ Зачем это нужно:
Если изменился порт/ключ/сервер — клиент получит обновлённые параметры автоматически;
Пользователь может добавлять подписку в клиента один раз — и далее получать обновления;
Упрощение интеграции с клиентами и панелями.
📌 В 3x-UI при активированной опции подписки в настройках можно указать ссылку для каждого клиента и использовать её в приложениях (v2RayTun, v2rayNG, Clash-производные и пр.).
💡 Предлагаемое решение
Предлагаю добавить новый тип конфигурации Sub link.
При выборе данной конфигурации чтобы можно было указать несколько ссылок на подписки (от разных поставщиков)
По каждой ссылке подкоп собирает конфигурации (только поддерживаемые) и использует по ним механику URLTest или селектор.
Workaround
Сейчас я решаю эту проблему скриптом который по крону раз в 4 часа забирает прокси по ссылке и пишет их в конфиг podkop. Панель используется Remnawave, для нее при запросе подписки нужно указывать в заголовке HWID устройства.
Скрипт:
#!/bin/sh
# update-podkop-from-remnawave.sh
# Обновление UCI-конфига podkop (urltest_proxy_links) из подписки Remnawave.
# Требуется: curl. (jq — опционально, только если вдруг придёт JSON и надо попытаться вытащить ссылки)
#
# Пример cron:
# echo '0 */4 * * * /usr/bin/update-podkop-from-remnawave.sh >/tmp/podkop-sub.log 2>&1' >> /etc/crontabs/root
# /etc/init.d/cron restart
set -eu
SUB_URL="${SUB_URL:-https://sub.link.com/user123456}"
UA="${UA:-V2RayTun/2.0 (Android); owrt-updater/0.2}"
TIMEOUT="${TIMEOUT:-20}"
UCI_CFG="podkop"
UCI_SEC="main"
# Дефолты urltest (можно переопределять env-переменными)
URLTEST_CHECK_INTERVAL="${URLTEST_CHECK_INTERVAL:-3m}"
URLTEST_TOLERANCE="${URLTEST_TOLERANCE:-50}"
URLTEST_TESTING_URL="${URLTEST_TESTING_URL:-https://www.gstatic.com/generate_204}"
log() { echo "[$(date '+%F %T')] $*"; }
need() { command -v "$1" >/dev/null 2>&1 || { echo "Нужна утилита: $1"; exit 1; }; }
need curl
# === Универсальный base64-декодер (с фолбэками) ===
b64_try_decode() {
local in="$1" out="$2"
if command -v base64 >/dev/null 2>&1; then
base64 -d "$in" > "$out" 2>/dev/null && return 0
base64 --decode "$in" > "$out" 2>/dev/null && return 0
fi
if command -v openssl >/dev/null 2>&1; then
openssl base64 -d -in "$in" -out "$out" 2>/dev/null && return 0
fi
awk '
BEGIN { map="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for (i=1;i<=length(map);i++) M[substr(map,i,1)]=i-1 }
function ob(b){ printf "%c", b }
{ gsub(/[ \t\r\n]/,""); l=length($0)
for (i=1;i<=l;i+=4){
c1=substr($0,i,1); c2=substr($0,i+1,1);
c3=substr($0,i+2,1); c4=substr($0,i+3,1);
if(c1=="") break; v1=M[c1]; v2=M[c2];
if(c3=="="){ ob((v1<<2)|(v2>>4)); break }
v3=M[c3];
if(c4=="="){ ob((v1<<2)|(v2>>4)); ob(((v2&15)<<4)|(v3>>2)); break }
v4=M[c4];
ob((v1<<2)|(v2>>4)); ob(((v2&15)<<4)|(v3>>2)); ob(((v3&3)<<6)|v4)
}}' "$in" > "$out" 2>/dev/null && return 0
return 1
}
detect_hwid() {
if [ -s /etc/machine-id ]; then cat /etc/machine-id && return; fi
if [ -s /proc/cpuinfo ] && grep -qi 'serial' /proc/cpuinfo; then
awk -F': *' '/[Ss]erial/ {print $2; exit}' /proc/cpuinfo && return
fi
for IF in eth0 br-lan wlan0; do
[ -e "/sys/class/net/$IF/address" ] && { tr -d ':' < "/sys/class/net/$IF/address"; return; }
done
(uname -n; cat /proc/uptime) 2>/dev/null | sha1sum | awk '{print $1}'
}
ensure_uci_section() {
# Если секции podkop.main нет — создадим и переименуем в main
if ! uci -q show "${UCI_CFG}.${UCI_SEC}" >/dev/null 2>&1; then
log "Секция ${UCI_CFG}.${UCI_SEC} не найдена — создаю."
local anon
anon="$(uci -q add "$UCI_CFG" main)"
uci -q rename "${UCI_CFG}.${anon}=${UCI_SEC}"
fi
}
restart_target() {
# Под разные прошивки/сервисы: podkop, sing-box, или просто reload network (не трогаем лишнее)
if [ -x /etc/init.d/podkop ]; then
/etc/init.d/podkop restart && log "podkop: restart OK" && return 0
fi
if [ -x /etc/init.d/sing-box ]; then
if /etc/init.d/sing-box reload 2>/dev/null; then
log "sing-box: reload OK"
else
/etc/init.d/sing-box restart
log "sing-box: restart OK"
fi
return 0
fi
log "Не нашёл /etc/init.d/podkop и /etc/init.d/sing-box — рестарт пропущен."
}
# Вытаскиваем vless://... из любого текста
extract_vless_list() {
# печатает по 1 ссылке на строку
grep -Eo 'vless://[^[:space:]]+' || true
}
HWID="$(detect_hwid)"
log "HWID: $HWID"
# Лучше сразу просить “не sing-box json”, чтобы получить список ссылок.
# Если твой сервер понимает другой формат — можно поменять format=...
case "$SUB_URL" in
*\?*) SUB_URL_QS="${SUB_URL}&hwid=${HWID}" ;;
*) SUB_URL_QS="${SUB_URL}?hwid=${HWID}" ;;
esac
TMP_SUB="/tmp/remnawave-sub-$$.blob"
TMP_TXT="/tmp/remnawave-sub-$$.txt"
VLESS_LIST="/tmp/remnawave-vless-$$.list"
log "Запрашиваем подписку..."
if ! curl -fsSL --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" \
-H "User-Agent: ${UA}" \
-H "X-HWID: ${HWID}" \
-H "X-Device-ID: ${HWID}" \
-H "Accept: */*" \
"$SUB_URL_QS" -o "$TMP_SUB"; then
log "Не удалось скачать подписку"
rm -f "$TMP_SUB" "$TMP_TXT" "$VLESS_LIST" 2>/dev/null || true
exit 1
fi
# 1) Если там уже есть vless:// — отлично
if grep -q 'vless://' "$TMP_SUB"; then
cat "$TMP_SUB" > "$TMP_TXT"
else
# 2) Иначе пробуем base64 → текст
if tr -d '\n\r\t ' < "$TMP_SUB" | grep -Eq '^[A-Za-z0-9+/=]+$'; then
if b64_try_decode "$TMP_SUB" "$TMP_TXT"; then
:
else
# 3) Если это JSON и есть jq — можно попробовать вытащить оттуда что-то (часто бесполезно без конвертера)
if command -v jq >/dev/null 2>&1 && jq -e . "$TMP_SUB" >/dev/null 2>&1; then
log "Пришёл JSON. Прямые vless:// не найдены. (Без конвертации outbounds→URI список может быть пуст.)"
# Попытка “хоть что-то”: если внезапно внутри есть строки vless://
jq -r '.. | strings | select(startswith("vless://"))' "$TMP_SUB" > "$TMP_TXT" || true
else
cat "$TMP_SUB" > "$TMP_TXT"
fi
fi
else
# 4) Просто текст, но без vless:// — оставим как есть
cat "$TMP_SUB" > "$TMP_TXT"
fi
fi
# Достаём ссылки
extract_vless_list < "$TMP_TXT" > "$VLESS_LIST"
if [ ! -s "$VLESS_LIST" ]; then
log "Не нашёл ни одной ссылки vless:// в подписке."
log "Первые 400 байт ответа:"
head -c 400 "$TMP_SUB" | sed "s/[^[:print:]\t]/?/g"
rm -f "$TMP_SUB" "$TMP_TXT" "$VLESS_LIST" 2>/dev/null || true
exit 1
fi
COUNT="$(wc -l < "$VLESS_LIST" | tr -d ' ')"
log "Найдено vless-ссылок: $COUNT"
# === Применяем UCI ===
ensure_uci_section
# Чтобы не дёргать uci 100 раз — используем batch
# Важно: "del" на list-опции удаляет её целиком (и мы заново add_list делаем).
TMP_UCIB="/tmp/uci-batch-$$.txt"
{
echo "del ${UCI_CFG}.${UCI_SEC}.proxy_string"
echo "set ${UCI_CFG}.${UCI_SEC}.proxy_config_type='urltest'"
echo "del ${UCI_CFG}.${UCI_SEC}.urltest_proxy_links"
while IFS= read -r link; do
# uci batch ожидает корректные кавычки; безопасно экранируем одиночные
esc="${link//\'/\'\\\'\'}"
echo "add_list ${UCI_CFG}.${UCI_SEC}.urltest_proxy_links='${esc}'"
done < "$VLESS_LIST"
echo "set ${UCI_CFG}.${UCI_SEC}.urltest_check_interval='${URLTEST_CHECK_INTERVAL}'"
echo "set ${UCI_CFG}.${UCI_SEC}.urltest_tolerance='${URLTEST_TOLERANCE}'"
echo "set ${UCI_CFG}.${UCI_SEC}.urltest_testing_url='${URLTEST_TESTING_URL}'"
echo "commit ${UCI_CFG}"
} > "$TMP_UCIB"
log "Применяю UCI batch..."
uci -q batch < "$TMP_UCIB"
rm -f "$TMP_SUB" "$TMP_TXT" "$VLESS_LIST" "$TMP_UCIB" 2>/dev/null || true
log "UCI обновлён: ${UCI_CFG}.${UCI_SEC} (urltest)."
restart_target
log "Готово."
Идеи реализации (опционально)
No response
Краткое описание
✅ Subscription link — это уникальная ссылка, по которой клиент (например, мобильный VPN-/прокси-клиент или панель управления) запрашивает актуальный список прокси-конфигураций для пользователя.
Это не просто однократный экспорт — при каждом запросе клиент получает свежие данные (адреса, ключи, параметры).
Это стандарт, который используется в панелях типа 3x-UI, Remnawave и других, чтобы не рассылать вручную каждому пользователю обновлённые настройки
Проблема, которую решает
✅ Зачем это нужно:
Если изменился порт/ключ/сервер — клиент получит обновлённые параметры автоматически;
Пользователь может добавлять подписку в клиента один раз — и далее получать обновления;
Упрощение интеграции с клиентами и панелями.
📌 В 3x-UI при активированной опции подписки в настройках можно указать ссылку для каждого клиента и использовать её в приложениях (v2RayTun, v2rayNG, Clash-производные и пр.).
💡 Предлагаемое решение
Предлагаю добавить новый тип конфигурации Sub link.
При выборе данной конфигурации чтобы можно было указать несколько ссылок на подписки (от разных поставщиков)
По каждой ссылке подкоп собирает конфигурации (только поддерживаемые) и использует по ним механику URLTest или селектор.
Workaround
Сейчас я решаю эту проблему скриптом который по крону раз в 4 часа забирает прокси по ссылке и пишет их в конфиг podkop. Панель используется Remnawave, для нее при запросе подписки нужно указывать в заголовке HWID устройства.
Скрипт:
Идеи реализации (опционально)
No response