Skip to content

Add support for Subscription's URL with HWID headers and etc#325

Open
yandexru45 wants to merge 76 commits into
itdoginfo:mainfrom
yandexru45:main
Open

Add support for Subscription's URL with HWID headers and etc#325
yandexru45 wants to merge 76 commits into
itdoginfo:mainfrom
yandexru45:main

Conversation

@yandexru45

Copy link
Copy Markdown

Описание изменений

Добавил в Podkop поддержку подписок(URL подписок от провайдеров), а также, для обеспечения совместимости со всеми панелями провайдеров добавлена отправка HWID и ещё пары заголовков.

Что изменено

Детальное описание изменений:

  • новый тип конфигурации: Subscription
  • Как работает? Запрашивает у провайдера по предоставленной ссылке подписку с прокси, затем использует их аналогично режиму URLTest. То есть, полученные конфиги парсятся, из них достаются аутбаунды и ставятся на использование в podkop, весь остальной конфиг провайдера игнорируется, да и в целом не требуется, ибо маршрутизацией управляет podkop
  • Меры предосторожности в виде sing-box check, чтобы в конфиг не попали невалидные аутбаунды, на случай, если вдруг провайдер возвращает что-то очень необычное или какой-то конкретный аутбаунд не поддерживается ядром singbox
  • Для обеспечения совместимости со всеми провайдерами передаются заголовки:
    User-Agent: singbox/ (сделано, чтобы по умолчанию сработало со всеми провайдерами, кто поддерживает клиенты на базе singbox)
    X-HWID: (первые несколько символов от md5 для строки с mac + моделью устройства)
    X-Device-OS: OpenWrt Linux (константа)
    X-Device-Model: (модель роутера)
    X-Ver-OS: (версия ядра Linux)
  • Имеется функциональность авто обновления подписки и настройка периодичности по времени, по умолчанию подписка в первый раз сохраняется, как "кеш", далее каждый промежуток времени подписка заново запрашивается у провайдера
  • Для каждого прокси отображается оригинальный тег из подписки, при этом работоспособность функциональности смены прокси через дашборд сохранена и проработана

Скриншот меню настройки:
image

Скриншот из дашборда:
image

@yandexru45 yandexru45 requested a review from itdoginfo as a code owner March 7, 2026 16:50
@a9fm

a9fm commented Mar 7, 2026

Copy link
Copy Markdown

добавь поддержку сырых (vless://) ключей, а не только жсон, и цены тебе не будет

@yandexru45

Copy link
Copy Markdown
Author

добавь поддержку сырых (vless://) ключей, а не только жсон, и цены тебе не будет

в podkop это уже есть и по умолчанию. мой PR нацелен на добавление поддержки ссылок подписок, но в виду грядущих сильных изменений в архитектуре podkop, скорее всего мой PR будет отложен в долгий ящик.

Если всё же вам нужна данная функция, то мой форк уже готов. Когда-нибудь это появится и в мейн репозитории Podkop.

@a9fm

a9fm commented Mar 8, 2026

Copy link
Copy Markdown

у тебя есть поддержка подписки, если она жсон, а мне надо поддержку подписки, если в ней кучу vless:// ключей

@moogeek

moogeek commented Mar 24, 2026

Copy link
Copy Markdown

@yandexru45 подскажите, пожалуйста, формат подписки, который забирается по url. пробовал добавлять url подписки из платного бота - все нормально, а когда меняю в json в секции outbounds список vless (tcp) на свои - не срабатывает все время ошибка No proxy outbounds found in subscription for section 'main'.

Также вопрос, в случае если установлен sing-box extended - поддерживаются ли xhttp подключения в этом режиме Subscription ?

@MarrShaLL22

Copy link
Copy Markdown

Привет @yandexru45! С 3x-ui не добавляются подписки? У меня в ошибку падает с двух разных панелей. С телефонов и с пк эти же подписки работают.

@golubevalexander310-tech

Copy link
Copy Markdown

как установить?

@w3struk

w3struk commented Apr 22, 2026

Copy link
Copy Markdown

Зачем вы продвигаете HWID в openwrt? Сбросить MAC WAN секундное дело.

@vlab97

vlab97 commented Apr 28, 2026

Copy link
Copy Markdown

@yandexru45 подскажите, пожалуйста, формат подписки, который забирается по url. пробовал добавлять url подписки из платного бота - все нормально, а когда меняю в json в секции outbounds список vless (tcp) на свои - не срабатывает все время ошибка No proxy outbounds found in subscription for section 'main'.

Также вопрос, в случае если установлен sing-box extended - поддерживаются ли xhttp подключения в этом режиме Subscription ?

К сожалению форк не поддерживает подписки, которые отдают gzip → base64 → список vless:// ссылок. И схему VLESS + Reality + XHTTP. С помощью LLM у себя локально наладил работу с такими подписками. Как раз была изначально такая ошибка.

@moogeek

moogeek commented Apr 28, 2026

Copy link
Copy Markdown

@vlab97

К сожалению форк не поддерживает подписки, которые отдают gzip → base64 → список vless:// ссылок. И схему VLESS + Reality + XHTTP. С помощью LLM у себя локально наладил работу с такими подписками. Как раз была изначально такая ошибка.

можешь подсказать, пожалуйста, как именно наладил?

@vlab97

vlab97 commented Apr 28, 2026

Copy link
Copy Markdown

@vlab97

К сожалению форк не поддерживает подписки, которые отдают gzip → base64 → список vless:// ссылок. И схему VLESS + Reality + XHTTP. С помощью LLM у себя локально наладил работу с такими подписками. Как раз была изначально такая ошибка.

можешь подсказать, пожалуйста, как именно наладил?

Выжимку скопировал с chatgpt. При наличии проблем обращаться к нейросетям :)

Решение: Podkop + подписка gzip → base64 → vless:// XHTTP

Проблема была в том, что подписка отдавалась не как готовый sing-box JSON, а в таком формате:

gzip → base64 → список vless:// ссылок

После распаковки начало выглядело так:

dmxlc3M6Ly9...

dmxlc3M6Ly9 — это base64-представление строки:

vless://

Для стандартного режима Podkop:

proxy_config_type='subscription'

такой формат не подходил, потому что Podkop ожидал JSON вида:

{
  "outbounds": []
}

Также обычный режим urltest_proxy_links не подходил, потому что Podkop пытался сам парсить vless://...type=xhttp... и падал с ошибками:

Unknown transport 'xhttp' detected.
Unsupported proxy ... type. Aborted.

Решение: поставить sing-box-extended с поддержкой XHTTP и добавить в Podkop конвертер подписки:

gzip/base64/vless:// → sing-box JSON outbounds

  1. Установка sing-box extended
wget -O /tmp/singbox-extended-install.sh \
  https://raw.githubusercontent.com/EikeiDev/OpenWRT-sing-box-extended/refs/heads/main/install.sh

sh /tmp/singbox-extended-install.sh

sing-box version

Ожидаемо:

sing-box version 1.13.11-extended-1.6.2
2. Настройка Podkop на режим Subscription

SUB_URL='https://YOUR_SUBSCRIPTION_URL'

uci set podkop.main.connection_type='proxy'
uci set podkop.main.proxy_config_type='subscription'
uci set podkop.main.subscription_url="$SUB_URL"

uci -q delete podkop.main.proxy_string
uci -q delete podkop.main.selector_proxy_links
uci -q delete podkop.main.urltest_proxy_links

uci commit podkop
  1. Патч /usr/lib/podkop/helpers.sh

Патч добавляет поддержку формата:

gzip → base64 → vless://

и конвертирует vless://...type=xhttp... в готовый sing-box outbound.

/etc/init.d/podkop stop
/etc/init.d/sing-box stop 2>/dev/null

HELPERS="/usr/lib/podkop/helpers.sh"

cp "$HELPERS" "$HELPERS.bak.xhttp_compat.$(date +%Y%m%d_%H%M%S)"

awk '
  /^# BEGIN PODKOP_COMPAT_SUBSCRIPTION/ {skip=1; next}
  /^# END PODKOP_COMPAT_SUBSCRIPTION/ {skip=0; next}
  /^# BEGIN PODKOP_COMPAT_XHTTP_SCHEMA_V2/ {skip=1; next}
  /^# END PODKOP_COMPAT_XHTTP_SCHEMA_V2/ {skip=0; next}
  skip==0 {print}
' "$HELPERS" > /tmp/helpers.clean && mv /tmp/helpers.clean "$HELPERS"

cat >> "$HELPERS" <<'EOF'

# BEGIN PODKOP_COMPAT_SUBSCRIPTION

podkop_compat_url_decode() {
  local s="$1"
  printf '%b' "$(printf '%s' "$s" | sed 's/+/ /g; s/%/\\x/g')"
}

podkop_compat_alpn_json() {
  local raw="$1"

  if [ -n "$raw" ]; then
    printf '%s' "$raw" | tr ',' '\n' | jq -R . | jq -s .
  else
    echo '["h2","http/1.1"]'
  fi
}

podkop_compat_vless_to_outbound() {
  local line="$1"
  local index="$2"

  local url tag host port uuid security sni pbk sid fp flow packet_encoding
  local transport path mode x_padding_bytes host_header alpn alpn_json

  tag="${line#*#}"
  if [ "$tag" = "$line" ] || [ -z "$tag" ]; then
    tag="server-$index"
  else
    tag="$(podkop_compat_url_decode "$tag")"
  fi

  url="${line%%#*}"

  host="$(url_get_host "$url")"
  port="$(url_get_port "$url")"
  uuid="$(url_get_userinfo "$url")"

  security="$(url_get_query_param "$url" "security")"
  sni="$(url_get_query_param "$url" "sni")"
  pbk="$(url_get_query_param "$url" "pbk")"
  sid="$(url_get_query_param "$url" "sid")"
  fp="$(url_get_query_param "$url" "fp")"
  flow="$(url_get_query_param "$url" "flow")"
  packet_encoding="$(url_get_query_param "$url" "packetEncoding")"

  transport="$(url_get_query_param "$url" "type")"
  path="$(podkop_compat_url_decode "$(url_get_query_param "$url" "path")")"
  mode="$(url_get_query_param "$url" "mode")"
  x_padding_bytes="$(url_get_query_param "$url" "x_padding_bytes")"
  host_header="$(url_get_query_param "$url" "host")"
  alpn="$(url_get_query_param "$url" "alpn")"

  [ -z "$port" ] && port="443"
  [ -z "$path" ] && path="/"
  [ -z "$mode" ] && mode="auto"
  [ -z "$x_padding_bytes" ] && x_padding_bytes="100-1000"
  [ -z "$host_header" ] && host_header="$sni"

  alpn_json="$(podkop_compat_alpn_json "$alpn")"

  jq -nc \
    --arg tag "$tag" \
    --arg host "$host" \
    --arg port "$port" \
    --arg uuid "$uuid" \
    --arg security "$security" \
    --arg sni "$sni" \
    --arg pbk "$pbk" \
    --arg sid "$sid" \
    --arg fp "$fp" \
    --arg flow "$flow" \
    --arg packet_encoding "$packet_encoding" \
    --arg transport "$transport" \
    --arg path "$path" \
    --arg mode "$mode" \
    --arg x_padding_bytes "$x_padding_bytes" \
    --arg host_header "$host_header" \
    --argjson alpn "$alpn_json" '
      {
        type: "vless",
        tag: $tag,
        server: $host,
        server_port: ($port | tonumber),
        uuid: $uuid
      }
      + (if $flow != "" then {flow: $flow} else {} end)
      + (if $packet_encoding != "" then {packet_encoding: $packet_encoding} else {} end)
      + (
          if ($security == "reality" or $security == "tls") then
            {
              tls:
                (
                  {
                    enabled: true,
                    server_name: $sni,
                    alpn: $alpn
                  }
                  + (if $fp != "" then {utls: {enabled: true, fingerprint: $fp}} else {} end)
                  + (
                      if $security == "reality" then
                        {
                          reality:
                            (
                              {enabled: true}
                              + (if $pbk != "" then {public_key: $pbk} else {} end)
                              + (if $sid != "" then {short_id: $sid} else {} end)
                            )
                        }
                      else
                        {}
                      end
                    )
                )
            }
          else
            {}
          end
        )
      + (
          if $transport == "xhttp" then
            {
              transport:
                (
                  {
                    type: "xhttp",
                    path: $path,
                    mode: $mode,
                    x_padding_bytes: $x_padding_bytes
                  }
                  + (if $host_header != "" then {host: $host_header} else {} end)
                )
            }
          elif $transport == "ws" then
            {
              transport:
                (
                  {
                    type: "ws",
                    path: $path
                  }
                  + (if $host_header != "" then {headers: {Host: $host_header}} else {} end)
                )
            }
          elif $transport == "grpc" then
            {
              transport: {
                type: "grpc",
                service_name: $path
              }
            }
          else
            {}
          end
        )
    '
}

podkop_compat_convert_subscription_file() {
  local filepath="$1"

  local tmpdir raw plain links outbounds line index outbound
  tmpdir="$(mktemp -d /tmp/podkop-compat.XXXXXX)" || return 1

  raw="$tmpdir/raw"
  plain="$tmpdir/plain"
  links="$tmpdir/links"
  outbounds="$tmpdir/outbounds"

  cp "$filepath" "$raw" || {
    rm -rf "$tmpdir"
    return 1
  }

  if gzip -t "$raw" 2>/dev/null; then
    gzip -dc "$raw" > "$plain" || {
      rm -rf "$tmpdir"
      return 1
    }
  else
    cp "$raw" "$plain"
  fi

  if jq -e 'type=="object" and (.outbounds|type=="array") and ((.outbounds|length)>0)' "$plain" >/dev/null 2>&1; then
    cp "$plain" "$filepath"
    rm -rf "$tmpdir"
    return 0
  fi

  tr -d '\r\n ' < "$plain" | base64 -d > "$links" 2>/dev/null

  if ! grep -qE 'vless://' "$links"; then
    cp "$plain" "$links"
  fi

  sed -i 's#vless://#\nvless://#g' "$links"

  echo "[]" > "$outbounds"
  index=1

  while IFS= read -r line; do
    case "$line" in
      vless://*)
        outbound="$(podkop_compat_vless_to_outbound "$line" "$index")"
        if [ -n "$outbound" ]; then
          jq --argjson ob "$outbound" '. + [$ob]' "$outbounds" > "$outbounds.tmp" && mv "$outbounds.tmp" "$outbounds"
          index=$((index + 1))
        fi
      ;;
    esac
  done < "$links"

  if [ "$index" -le 1 ]; then
    rm -rf "$tmpdir"
    return 1
  fi

  jq -n --slurpfile out "$outbounds" '{outbounds: $out[0]}' > "$filepath"

  rm -rf "$tmpdir"
  return 0
}

validate_subscription_file() {
  local filepath="$1"

  [ -s "$filepath" ] || return 1

  if jq -e 'type == "object" and (.outbounds | type == "array") and ((.outbounds | length) > 0)' "$filepath" >/dev/null 2>&1; then
    return 0
  fi

  podkop_compat_convert_subscription_file "$filepath" || return 1

  jq -e 'type == "object" and (.outbounds | type == "array") and ((.outbounds | length) > 0)' "$filepath" >/dev/null 2>&1
}

# END PODKOP_COMPAT_SUBSCRIPTION
EOF

chmod 644 "$HELPERS"

ash -n "$HELPERS" && echo "helpers.sh syntax OK" || echo "helpers.sh syntax ERROR"

Ожидаемый результат:

helpers.sh syntax OK
4. Очистка кэша и запуск Podkop

rm -rf /tmp/sing-box/subscriptions/*
rm -f /etc/sing-box/config.json

/etc/init.d/podkop restart

sleep 60
  1. Проверка результата
sing-box version
sing-box check -c /etc/sing-box/config.json
/etc/init.d/sing-box status
ps w | grep -E 'sing-box|podkop' | grep -v grep

Проверка, что XHTTP-ноды появились в итоговом конфиге:

jq '[.outbounds[] | select(.type=="vless" and .transport.type=="xhttp")] | length' /etc/sing-box/config.json

У меня получилось:

78

Проверка тегов:

jq '.outbounds[] | select(.type=="vless" and .transport.type=="xhttp") | .tag' /etc/sing-box/config.json | head

Пример результата:

"Turkey 4 🇹🇷 
"Bosnia and Herzegovina 6 🇧🇦
"UAE 6 🇦🇪"
  1. Проверка сгенерированной подписки
ls -lh /tmp/sing-box/subscriptions/
jq '.outbounds[0]' /tmp/sing-box/subscriptions/main.json

@itdoginfo

Copy link
Copy Markdown
Owner

Чтоб не искать. Рекомендации по исправлению: https://t.me/itdogchat/142500/1175801

Исправлять не нужно, не тратье сейчас своё время.

kjljxybr and others added 4 commits May 13, 2026 10:32
При холодном старте подкоп вызывает prepare_subscription_caches_for_startup
до запуска sing-box. Если включена опция download_lists_via_proxy=1,
функция пытается скачать подписку через прокси 127.0.0.1:4534
(SB_SERVICE_MIXED_INBOUND), который ещё не существует, потому что sing-box
запускается только после prepare_subscription_caches_for_startup.

В результате wait_for_subscription_connectivity безуспешно делает 12 попыток
по 5 секунд (60 секунд ожидания), после чего стартует retry worker, который
крутится в цикле каждые 10 секунд и тоже использует прокси — но sing-box
никогда не запустится без подписки. Получается deadlock: подписка нужна для
старта sing-box, а sing-box нужен для скачивания подписки через прокси.

Ситуация усугубляется тем, что /tmp/sing-box/subscriptions лежит в tmpfs,
поэтому на холодном старте кэш всегда пустой и cache_needs_refresh=1.

На этапе bootstrap скачиваем подписку напрямую, игнорируя
download_lists_via_proxy. После старта sing-box subscription_update и
list_update продолжают использовать прокси согласно настройке.
Исправить deadlock при холодном старте с download_lists_via_proxy=1
yandexru45 and others added 30 commits June 5, 2026 08:33
Синхронизация с netshift
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.