Skip to content

[FEATURE] Тип конфигурации: ссылка подписки (Sub Link) #285

Description

@yazhog

Краткое описание

✅ 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions