Блог Clearway

Remnawave subscription: как работает подписка и subscription page

Коротко

Remnawave отдаёт подписку по одному URL вида https://sub.example.com/<shortUuid> и определяет клиента по заголовку User-Agent: тот же адрес возвращает base64-список VLESS для v2rayNG/Happ, YAML для Clash/Mihomo или JSON для Sing-box. Формат задаёт шаблон подписки в панели, а HTML-лендинг с кнопками «добавить в приложение» рисует отдельный контейнер subscription-page. Если remnawave подписка «не обновляется» — почти всегда виноват кэш: локальный кэш клиента (особенно iOS Happ), кэширование ответа на CDN/прокси или слишком большой profile-update-interval. Лечится заголовком Cache-Control: no-store на пути подписки, интервалом обновления в 1 час и, в крайнем случае, повторным добавлением подписки по той же ссылке. Диагностика всегда начинается с одной команды: curl -i с нужным User-Agent.

Две сущности с одним именем: raw-подписка и subscription page

В Remnawave «подпиской» называют две разные вещи, и их путают в 90% жалоб.

Raw subscription — URL вида https://sub.example.com/<shortUuid>, по которому VPN-приложение забирает конфиг. Бэкенд читает заголовок User-Agent, определяет клиент и отдаёт тело в подходящем формате. shortUuid — это не внутренний UUID пользователя, а укороченный публичный идентификатор подписки; при его пересоздании старая ссылка мгновенно перестаёт работать.

Subscription page — HTML-лендинг, который открывается по тому же адресу из браузера (User-Agent = обычный браузер): кнопки «Добавить в Happ / v2rayNG / Streisand», deep-link'и happ:// и v2rayng://, QR-код, остаток трафика. В актуальных сборках это отдельный контейнер remnawave/subscription-page, который проксирует raw-ответ бэкенда и оборачивает его в интерфейс.

Ключевой практический вывод: приложение и браузер бьют по одному URL, но получают разное тело. Поэтому «в браузере красиво, а в приложении пусто» — это не поломка лендинга, а проблема raw-ответа (шаблон или детект клиента). Диагностику всегда начинайте с сырого ответа, а не с UI:

# что реально отдаёт бэкенд конкретному клиенту: статус, заголовки, тело
curl -i -H 'User-Agent: Happ/1.0' https://sub.example.com/<shortUuid>

Шаблоны подписки: base64, Clash, Sing-box и где ломается прод

Формат вывода задаёт шаблон подписки в панели (Subscription templates). Каждый формат привязан к семейству клиентов по User-Agent:

ФорматКлиентыЧто внутри
Base64 (VLESS-список)v2rayNG, Happ, Streisand, NekoBoxплоский список vless://… в base64
Clash / Mihomo (YAML)Clash Meta, Mihomo, FlClashproxies + proxy-groups + rules
Sing-box (JSON)sing-box, Hiddifyoutbounds + route + dns
Xray JSONклиенты на голом Xray-coreполный конфиг Xray

Разница принципиальная. Base64-список — это только ссылки: вся маршрутизация и DNS остаются на дефолтах приложения. А Clash и Sing-box — это полноценный конфиг, где вы сами описываете dns (fake-ip), route/rules, геобазы и селекторы. Именно тут настраивается схема «внутренний трафик РФ мимо VPN, остальное — через ноду», и именно тут чаще всего роняют прод одной правкой: пустой или невалидный шаблон = у клиента «нет серверов», хотя ссылка живая и статус 200.

Happ — особый случай. Он читает base64-список, но многое из «умного» поведения (announce, встроенная маршрутизация, контроль обновления) берёт из своих расширений поверх подписки, а не из шаблона.

Правило безопасной правки шаблона: не редактируйте боевой напрямую. Снимите текущий ответ через curl, поправьте копию, проверьте валидность (sing-box check -c out.json; для Clash — прогон в Mihomo) и только потом заливайте.

Служебные заголовки: трафик, срок, интервал обновления

Remnawave кладёт в HTTP-ответ подписки служебные заголовки — по ним приложение рисует остаток трафика, дату окончания и решает, как часто дёргать URL. Это часть протокола подписок, а не косметика.

  • subscription-userinfo: upload=…; download=…; total=…; expire=… — трафик и срок. total=0 обычно значит «безлимит», expire — unix-время окончания. Неверный остаток в приложении → смотреть сюда.
  • profile-update-interval — в часах, как часто клиент обновляет подписку. Значение 1 = раз в час; поставите 24 — пользователь до суток сидит на старом наборе нод.
  • profile-title — имя профиля (обычно base64), видно в списке подписок.
  • announce — строка-объявление; часть клиентов (в т.ч. Happ) показывает её пользователю. Удобно для сервисных сообщений («сегодня в 03:00 переезд нод»).
  • profile-web-page-url / support-url — ссылки на страницу подписки и в поддержку.

Быстрая проверка, что бэкенд реально их отдаёт и прокси/CDN их не срезал:

curl -sI -H 'User-Agent: v2rayNG/1.9' https://sub.example.com/<shortUuid> \
 | grep -iE 'subscription-userinfo|profile-update-interval|profile-title'

Если subscription-userinfo не приходит — приложение просто не покажет трафик и срок, и пользователь решит, что «подписка сломалась», хотя ноды рабочие. Частый виновник — фронт, который вырезает нестандартные заголовки.

Почему подписка «не обновляется» — разбор по частоте

«Не обновляется» почти всегда значит: приложение показывает старый набор серверов или старый трафик, хотя в панели вы уже всё поменяли. По убыванию частоты:

1. Кэш на стороне клиента (самое частое). Приложения обновляют тело не мгновенно, а по profile-update-interval. Отдельно выделяется iOS Happ: по наблюдениям он кэширует особенно упорно и иногда не подтягивает новый ответ даже при ручном обновлении. Что делать по порядку: уменьшить интервал до 1 часа → попросить ручное обновление → как крайняя мера удалить и заново добавить подписку по той же ссылке (это сбрасывает локальный кэш).

2. Кэш на CDN/прокси перед endpoint'ом. Если ответ закэшировался на фронте, устаревший конфиг разъедется сразу по всем пользователям. Путь подписки обязан отдаваться без кэша:

Cache-Control: no-store, no-cache, must-revalidate
Expires: 0

Ставится на уровне прокси/CDN именно для пути подписки, не для статики лендинга.

3. Сменился shortUuid. Пересоздали ключ — старый URL отдаёт 404 или пусто. Проверяется кодом ответа в curl -i.

4. Клиент не распознан по User-Agent. Экзотический или свежеобновлённый клиент может не попасть в правило детекта — прилетает не тот формат или пустое тело. Проверяется подстановкой нужного User-Agent.

5. Часы устройства. Сильный сдвиг времени ломает и показ срока, и TLS-валидацию у самих нод.

Алгоритм: curl -i с UA нужного клиента → есть ли в теле актуальные ноды и свежий subscription-userinfo. Если да — проблема на клиенте (кэш). Если нет — на бэкенде: шаблон, детект или кэш на фронте.

Подписка за CDN: no-store на файл, whitelist-фронт на ноды

Здесь два требования, которые легко перепутать, — и путаница дорого стоит.

Сам файл подписки кэшировать нельзя. Ответ динамический (трафик, срок, актуальный список нод), поэтому на пути подписки обязателен no-store (см. предыдущий раздел). Закэшируете endpoint на CDN — получите массовое «не обновляется» одномоментно у всех.

А транспорт самих нод за CDN, наоборот, полезен. Сервер в дата-центре при белых списках блокируется тривиально: IP ноды уходит в чёрный список ТСПУ, и рабочий конфиг из подписки ведёт на мёртвый адрес. Устойчивость даёт не endpoint, а фронт трафика нод через whitelisted-CDN: клиент коннектится к белому IP CDN, а тот проксирует на вашу ноду — блокировка по IP ноды не роняет сервис. Детально это разобрано в отдельных статьях про панель и ноду за CDN и про XHTTP за CDN; здесь важно лишь развести ответственность.

Один практический нюанс, который порождает жалобы «в одном приложении работает, в другом нет»: XHTTP за CDN, пропускающим только GET, требует явного mode: packet-up в конфиге. Клиент, который сам его не выбрал, не поднимет соединение. Не убирайте packet-up из боевого шаблона ради «упрощения».

Итог: no-store — на путь подписки, кэш — только на статику лендинга, whitelist-CDN — на транспорт нод, а не на файл подписки.

Чек-лист настройки и быстрая диагностика

Настройка с нуля:

  1. Задан публичный домен подписки (SUB_PUBLIC_DOMAIN / соответствующая настройка), TLS-сертификат валиден.
  2. Заполнены шаблоны под нужные клиенты: минимум base64 (v2rayNG/Happ/Streisand), при необходимости — Sing-box и Clash с вашей маршрутизацией.
  3. profile-update-interval = 1 (час), не 24.
  4. Заполнены profile-title, announce, support-url — пользователю видно, что за подписка и куда писать.
  5. На пути подписки на прокси/CDN выставлен Cache-Control: no-store.
  6. Subscription page отвечает в браузере и ведёт deep-link'ами в приложения.

Диагностика жалобы:

# 1. что отдаётся конкретному клиенту: статус, заголовки, тело
curl -i -H 'User-Agent: Happ/1.0' https://sub.example.com/<shortUuid>

# 2. служебные заголовки и кэш
curl -sI -H 'User-Agent: Happ/1.0' https://sub.example.com/<shortUuid> \
 | grep -iE 'cache-control|subscription-userinfo|profile-update-interval'

# 3. декодировать base64-тело и убедиться, что ноды актуальные
curl -s -H 'User-Agent: v2rayNG/1.9' https://sub.example.com/<shortUuid> | base64 -d | head

Трактовка: тело свежее и no-store на месте, а клиент показывает старое → клиентский кэш (ручное обновление или re-add). Тело устаревшее/пустое → чините шаблон, детект User-Agent или ищите кэш на фронте.

Частые вопросы

Чем subscription page отличается от самой подписки в Remnawave?

Subscription page — HTML-лендинг с кнопками, QR и deep-link'ами, который открывается по адресу подписки из браузера (отдельный контейнер remnawave/subscription-page). Raw-подписка — тот же URL, но при заходе из VPN-приложения бэкенд по User-Agent отдаёт машинный конфиг: base64-список VLESS, Clash YAML или Sing-box JSON. Если приложение видит пустоту, а браузер — красивую страницу, проблема в raw-ответе (шаблон или детект клиента), а не в лендинге.

Почему remnawave подписка не обновляется в приложении?

Чаще всего это кэш. Приложения обновляют конфиг не мгновенно, а по интервалу profile-update-interval; iOS Happ кэширует особенно упорно. Плюс ответ мог закэшироваться на CDN/прокси перед endpoint'ом — тогда старый конфиг видят все сразу. Решение: profile-update-interval = 1 час, Cache-Control: no-store на путь подписки, ручное обновление, а в крайнем случае удалить и заново добавить подписку по той же ссылке.

Как понять, какой формат конфига получает конкретное приложение?

Подставьте User-Agent приложения в curl: curl -i -H 'User-Agent: Happ/1.0' https://sub.example.com/<shortUuid>. Увидите статус, заголовки и тело ровно в том виде, в каком их получает этот клиент. Так за одну команду проверяются и детект клиента, и корректность шаблона, и наличие служебных заголовков.

Почему в приложении не показывается остаток трафика и срок?

Эти данные приходят в заголовке subscription-userinfo (upload/download/total/expire, где total=0 обычно безлимит). Если заголовок не отдаётся или его срезал прокси/CDN, приложение не покажет трафик и дату — и пользователь решит, что подписка сломалась, хотя ноды рабочие. Проверьте наличие заголовка через curl -sI и убедитесь, что фронт его не вырезает.

Можно ли ставить endpoint подписки за CDN?

Можно, но сам файл подписки кэшировать нельзя — на путь подписки обязателен no-store, иначе устаревший конфиг разъедется сразу по всем. За CDN полезнее прятать транспорт самих нод: сервер в дата-центре при белых списках блокируется по IP, а фронт через whitelisted-CDN даёт устойчивость. Для XHTTP за GET-совместимым CDN не забудьте mode packet-up.

Что делать, если один клиент работает, а другой с той же подпиской — нет?

Две типовые причины. Первая — детект User-Agent: клиенты получают разный формат, и для одного шаблон пустой или невалидный; проверяется curl с соответствующим UA. Вторая — транспорт: XHTTP за GET-совместимым CDN требует явного mode packet-up, и клиент, который сам его не выбрал, не поднимет соединение. Сравните сырые ответы curl для обоих клиентов.

Whitelisted-вход для вашего VPN-сервиса

Clearway даёт «белый» CDN-вход перед вашей нодой — устойчивый к троттлингу операторов. Первые 10 ГБ бесплатно, без карты.

Попробовать →