> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Remnawave

> Как Omniflow подключается к панели Remnawave 3.2.2, все вызовы, которые он делает, и граница, которую он никогда не пересекает

Omniflow не заменяет Remnawave. Он стоит рядом и продаёт доступ к нему. Remnawave остаётся
авторитетным источником для VPN-пользователей, счётчиков трафика, подключённых устройств, ссылок
доступа, нод, хостов и Internal Squads; Omniflow владеет идентичностью, каталогом, заказами,
платежами, книгой проводок кошелька, правами на услуги, поддержкой, маркетингом и журналом аудита.
Всё это взаимодействие проходит через один написанный вручную HTTP-адаптер, `internal/remnawave`, и
этот адаптер — единственный код в репозитории, который знает о существовании типов, имеющих форму
Remnawave.

Это не стилистический выбор. Две системы, каждая из которых считает себя владельцем состояния VPN,
порождают класс ошибок, которые никто не сможет свести: клиент, чья подписка активна в одном месте и
отключена в другом, и никакого способа сказать, где правда. Разделение полномочий делает вопрос
разрешимым — ответ Remnawave есть истина о доступе, ответ Omniflow есть истина о деньгах и намерении,
— и позволяет обновлять Remnawave, не заставляя Omniflow иметь мнение о его схеме.

См. [владение данными](https://omniflow-vpn.mintlify.site/ru/architecture/data-ownership.md) для концептуальной версии границы и
[выдачу доступа](https://omniflow-vpn.mintlify.site/ru/commerce/fulfillment.md) для того, что происходит после проведения платежа.

## Какая версия Remnawave

Адаптер рассчитан на **Remnawave 3.2.2**, и пути, которые он вызывает, зашиты в код. Согласования
версий нет, обнаружения возможностей нет, второго адаптера нет: один клиент, один набор маршрутов.

На практике версию фиксирует контрактный тест `internal/remnawave/contract_test.go`, который
утверждает точный метод и путь каждого вызова, ожидаемую Omniflow оболочку ответа `{"response": …}` и
то, как отображается каждый класс отказов. Обновление Remnawave, переносящее маршрут, падает именно
там, а не всплывает позже как ошибка выдачи доступа, которую никто не может классифицировать. Если вы
планируете обновление панели, запуск `go test ./internal/remnawave/...` против новой поверхности —
самая дешёвая из возможных проверок.

> [!NOTE]
>
> Страницы с матрицей совместимости нет. Версия зафиксирована ровно в трёх местах: бейдж в README,
> `AGENTS.md` и контрактный тест.

## Учётные данные и подключение

Обоим процессам, которые выдают доступ, и обоим, которые читают, нужны одни и те же два значения.

| Переменная | По умолчанию | Назначение |
| - | - | - |
| `APP_REMNAWAVE_URL` | нет | Базовый URL API Remnawave. Должен разбираться как абсолютный `http`/`https` URL с хостом; строка запроса и фрагмент отбрасываются. Всё остальное валит запуск с сообщением `Remnawave URL must be an absolute HTTP(S) URL` |
| `APP_REMNAWAVE_TOKEN` | нет | Bearer-учётные данные, отправляются как `Authorization: Bearer <token>` в каждом вызове. Не должны быть пустыми |

Получите токен через собственный механизм API-токенов вашей установки Remnawave. Ему нужно уметь
читать и изменять пользователей, читать подписки и управлять HWID-устройствами — это вся поверхность,
которой касается Omniflow, ничего шире не используется.

**1. Создайте токен в Remnawave**

Выпустите API-токен в панели Remnawave. Отдайте его только Omniflow; токен используется из трёх
процессов, но это одни учётные данные, и ротация означает изменение одного значения.

**2. Положите оба значения в `.env`**

```env theme={null}
APP_REMNAWAVE_URL=https://panel.example.com
APP_REMNAWAVE_TOKEN=…
```

Процессы API, бота и воркера в поставляемом Compose-файле читают один и тот же `.env` — именно
это не даёт трём процессам разойтись в том, с какой панелью они разговаривают.

**3. Перезапустите и проверьте готовность**

`curl -s localhost:8080/readyz` должен сообщить запись `remnawave` с `"healthy": true`. Неверный
токен отображается как нездоровая зависимость, а не как здоровая панель — см.
[Работоспособность и готовность](#работоспособность-и-готовность).

Все три процесса отказываются стартовать без обоих значений. API дополнительно отказывается собирать
свой коммерческий рантайм без `APP_OPERATOR_TOKEN`, 32-байтного `APP_DATA_ENCRYPTION_KEY` и
`APP_VALKEY_URL`. Полный список — в [конфигурации](https://omniflow-vpn.mintlify.site/ru/getting-started/configuration.md).

> [!WARNING]
>
> Ротация токена Remnawave означает правку окружения и перезапуск трёх процессов. На
> `/admin/settings` есть раздел **Remnawave** с базовым URL, полем токена только на запись,
> интервалом сверки и флагом проверенной совместимости — он сохраняет то, что вы вводите, запечатывая
> ключом установки, но **ни один работающий процесс не читает это обратно**. Клиент собирается из
> `APP_REMNAWAVE_URL` и `APP_REMNAWAVE_TOKEN` при старте процесса, и ничто не перечитывает их. См.
> [настройки установки](https://omniflow-vpn.mintlify.site/ru/operations/installation-settings.md).

Кнопки «проверить подключение» для Remnawave тоже нет.
`POST /v1/panel/settings/providers/{provider}/test` проверяет только платёжные адаптеры и отвечает
`422 provider_not_loaded` на любое другое имя. Проверкой подключения служит проба готовности.

## Все вызовы, которые делает Omniflow

Пятнадцать операций. Это полный список — ничто иное в API Remnawave из Omniflow недостижимо,
потому что ничто иное не реализовано.

| Вызов Remnawave | Что его использует |
| - | - |
| `GET /api/users/?start=&size=` | Предпросмотр импорта и постраничный обход; а также проба готовности |
| `GET /api/users/{id}` | Обзор клиента, обнаружение расхождений, чтение после записи |
| `GET /api/users/stream?size=2&telegramId=…` | Поиск аккаунта Telegram при первом запуске |
| `GET /api/users/by-username/{username}` | Принятие существующего пользователя перед созданием нового |
| `POST /api/users/` | Первичная выдача доступа по подписке |
| `PATCH /api/users/` (в теле передаётся `id`) | `create`, `reconcile`, `extend`, `set_limits`, `set_squads` |
| `POST /api/users/{id}/actions/enable` | Операция `enable` |
| `POST /api/users/{id}/actions/disable` | Операция `disable` |
| `POST /api/users/{id}/actions/reset-traffic` | Операция `reset_traffic` |
| `POST /api/users/{id}/actions/revoke` | Смена ссылки доступа по инициативе клиента |
| `GET /api/subscriptions/by-id/{id}` | Чтение текущей ссылки доступа и статуса подписки |
| `GET /api/hwid/devices/{id}` | Списки устройств и счётчики устройств |
| `POST /api/hwid/devices/delete` (тело `{userId, hwid}`) | Удаление одного устройства |
| `POST /api/hwid/devices/delete-all` (тело `{userId}`) | Отключение клиентом всех устройств |
| `GET /api/nodes` | [Отчёты по трафику](https://omniflow-vpn.mintlify.site/ru/operations/traffic-reporting.md): заполнение узлов. Единственный вызов, отсутствие которого — нормальный исход: панель, не отдающая его, даёт на экране прямо названное отсутствие, а не страницу нулей |

Три узких Go-интерфейса делят этот набор по тому, кому разрешено им владеть: `Service` — набор
чтения для клиентских поверхностей, `Provisioner` — набор записи, которым владеет воркер выдачи
доступа, а `Importer` — единственное постраничное чтение, используемое импортом клиентов. Поверхность
только для чтения не может выполнить запись, потому что ей её никогда не давали.

### Что предотвращает сам транспорт

Четыре режима отказа исключены на уровне ниже любой функциональности, и на каждый есть тест:

* **Утечка токена при редиректе.** `CheckRedirect` возвращает `http.ErrUseLastResponse`, поэтому за
  `302` никогда не следуют с прикреплённым заголовком `Authorization`. Неправильно настроенный прокси
  перед Remnawave не сможет унести учётные данные.
* **Исчерпание памяти.** Каждое тело ответа читается через ограничивающий ридер на 1 МиБ, поэтому
  сломанная или враждебная панель не может исчерпать память воркера.
* **Истёкший токен, читаемый как «у этого клиента ничего нет».** Ответ не из класса 2xx становится
  ошибкой API с кодом статуса, а `404` — отдельной ошибкой «не найдено». Ни ту, ни другую нельзя
  принять за пустой успешный ответ — а это и есть разница между «ваша подписка исчезла» и «панель
  ответила отказом».
* **Зависшая панель.** Фиксированный клиентский таймаут в 10 секунд на каждый вызов.

У адаптера **нет собственных повторов и выдержек**. Повторять — работа воркера выдачи доступа, где
повтор долговечен и идемпотентен; путь чтения просто деградирует и сообщает об этом.

## Записи всегда идут через операцию выдачи доступа

Ничто в Omniflow не вызывает Remnawave, чтобы изменить состояние напрямую, — ни оплаченный заказ, ни
клик в панели, ни массовое действие. Каждое изменение сначала записывается в PostgreSQL строкой
`fulfillment_operations` с ключом идемпотентности и желаемым состоянием, а затем применяется
долговечной задачей.

Причина изложена в коде на операторском маршруте: клик в панели, который сам писал бы в Remnawave,
стал бы тем единственным изменением, которое сверка не смогла бы объяснить. У него не было бы ни
ключа идемпотентности, ни политики повторов, ни записи о расхождении, ни истории. Фиксация операции в
той же транзакции, что и изменение состояния, которое её требует, ещё и означает, что падение между
«оплачено» и «доступ выдан» оставляет работу, которую надо доделать, а не деньги, которые надо
сводить.

Поддерживаемые операции: `create`, `reconcile`, `extend`, `set_limits`, `set_squads`, `enable`,
`disable` и `reset_traffic`.

| Операция | Вызов Remnawave |
| - | - |
| `create` / `reconcile` без известного ID на стороне Remnawave | `GET /api/users/by-username/{candidate}` для каждого кандидата; при `404` по всем — `POST /api/users/` |
| `create` / `reconcile` с известным ID на стороне Remnawave | `PATCH /api/users/`; `404` откатывает к пути создания-или-восстановления |
| `extend`, `set_limits`, `set_squads` | `PATCH /api/users/` |
| `enable` / `disable` / `reset_traffic` | Соответствующий `POST /api/users/{id}/actions/…`, затем повторное чтение пользователя |

Оператор ставит операцию в очередь на `/admin/customers/{customerId}` → карточка подписки → панель
действий. Причина обязательна; без неё кнопки остаются неактивными. Панель сообщает **поставлено в
очередь**, а не «выполнено», потому что применяет операцию воркер. Сетевой вызов —
`POST /v1/panel/customers/{customerID}/subscriptions/{subscriptionID}/operations` с правом
`subscriptions.write`, CSRF-токеном и `X-Operator-Reason`; он отвечает `202` с
`{operationId, operation, status}`.

Отказы, о которых стоит знать:

* Слот подписки без права на услугу отвечает `409 not_provisioned` — нечего продлевать, отключать
  или сбрасывать.
* `create` и `reconcile` намеренно отсутствуют в операторском белом списке и отвечают
  `422 unsupported_operation`. Их наличие дало бы оператору способ выпустить право на услугу без
  заказа за ним.
* Ключ идемпотентности выводится на сервере как `panel:<subscriptionID>:<operation>:<requestID>`,
  поэтому дважды отправленная форма переиспользует первую операцию, а не ставит в очередь вторую.

Повторы, выдержки, dead letter и диагностические экраны описаны в
[задачах и очередях](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md) и в [выдаче доступа](https://omniflow-vpn.mintlify.site/ru/commerce/fulfillment.md).

## Детерминированные имена пользователей и восстановление пользователя на стороне Remnawave

Для каждой подписки выдаётся пользователь Remnawave с именем `omniflow_` плюс первые 16
шестнадцатеричных символов UUID подписки. Установки, обновлённые с v0.4, сохраняют для первого слота
устаревшее имя, выведенное из клиента: `omniflow_` плюс первые 16 шестнадцатеричных символов UUID
*клиента*.

Это предотвращает два отказа. Повторный `create` после потерянного ответа не должен создавать второго
пользователя Remnawave, поэтому воркер сначала ищет имя пользователя, а затем создаёт. И обновлённая
установка должна принять уже имеющегося у неё пользователя Remnawave, а не дублировать его, поэтому
первый слот принимает оба имени как кандидатов. Вывод имени из подписки, а не из клиента, — это как
раз то, что позволяет одному клиенту владеть несколькими независимыми пользователями Remnawave.

`PATCH`, вернувший `404` — пользователь панели был удалён в обход Omniflow, — откатывается к пути
создания-или-восстановления и создаёт его заново. Операция, отличная от `create`, без известного
ID на стороне Remnawave падает с `remote_user_not_found` и повторяется: нельзя выполнить `enable` над тем, чего
никогда не выдавали.

## Расхождения

Операция `reconcile` читает пользователя Remnawave и сравнивает его с тем, что подразумевает
Omniflow. Каждое несовпадение записывается строкой `entitlement_drifts` с JSON-полями `expected` и
`observed`.

| Вид | Условие |
| - | - |
| `missing_remote` | ID на стороне Remnawave не записан либо `GET /api/users/{id}` возвращает `404` |
| `expiry` | `expireAt` на стороне Remnawave отличается от окончания права на услугу |
| `traffic` | Настроенный лимит трафика отличается от лимита трафика на стороне Remnawave |
| `device_limit` | Настроенный лимит отличается от лимита HWID-устройств на стороне Remnawave, в том числе если это значение равно null |
| `squads` | Набор активных Internal Squads на стороне панели отличается от желаемого, сравнение без учёта порядка |
| `status` | Отображённый статус на стороне Remnawave отличается от статуса права на услугу |

Смысл в том, что «кто-то отредактировал Remnawave вручную» превращается в запись, которую можно
разобрать, а не в обращение в поддержку. Расхождение всегда оценивается относительно пользователя
Remnawave, которым владеет *именно эта* подписка, поэтому вторая подписка клиента никогда не будет
оценена по состоянию первой на стороне Remnawave.

Читайте их в `/admin` → System → Drift (`GET /v1/panel/system/drift`, `system.read`) или на
поверхности с токеном через `GET /v1/admin/fulfillment/drifts?limit=<1..500>` (по умолчанию 100).

> [!NOTE]
>
> Пустой список расхождений — нормальное состояние, а не доказательство того, что расхождений никогда
> не было. В успешном проходе сверки строки расхождений открываются и закрываются внутри той же
> транзакции, что и корректирующий `PATCH`, поэтому строка остаётся видимой, только пока
> корректирующая работа падает или Remnawave недоступен. Это и есть задуманный сигнал; это не
> универсальный журнал «что изменилось в Remnawave».

`external_edit` присутствует в ограничении базы данных и в перечислении OpenAPI, но ни один путь в
коде его не порождает. Внешне изменённый пользователь на самом деле записывается как конкретное
расхождение по полю из таблицы выше. На каждую пару «право на услугу и вид» существует только одно
открытое расхождение — повтор обновляет строку, а не накапливает новые, — а закрытые строки удаляются
через `APP_RETENTION_DRIFT_DAYS` (по умолчанию 90). Открытые строки не вычищаются никогда.

Сверка выполняется в процессе воркера каждые 15 минут для не более чем 500 прав на услуги, у которых
`reconciled_at` пуст или старше 15 минут. Оба числа зашиты в код. Установка, не запускающая воркер, не
получает ни сверки, ни выдачи доступа вообще.

## Работоспособность и готовность

API регистрирует `remnawave` в том же реестре здоровья, который отвечает на `GET /readyz`, питает
метрику-датчик `omniflow_dependency_up{dependency="remnawave"}` и управляет автоматическим режимом
обслуживания. Один реестр означает, что балансировщик нагрузки, панель оператора и контроллер режима
обслуживания не могут разойтись в том, работает ли панель.

Проба — это `ListUsers(0, 1)`, аутентифицированное чтение, а не запрос к неаутентифицированной
странице, поэтому отозванный токен читается как сбой, а не как здоровье.

```bash theme={null}
curl -s localhost:8080/readyz
```

```json theme={null}
{
  "status": "ready",
  "checks": [
    { "name": "remnawave", "healthy": true, "checkedAt": "2026-08-13T09:15:04Z", "latencyMs": 41 }
  ]
}
```

Отказ несёт стабильную классификацию (`timeout`, `unavailable`) и никогда не сообщение драйвера,
поэтому ответ не может раскрыть URL или учётные данные. Результаты проб кэшируются на 2 секунды, а
каждая проба ограничена 3 секундами, поэтому всплеск проверок готовности не может превратиться во
всплеск вызовов Remnawave. Операторы видят ту же пробу на `GET /v1/panel/overview/health` с правом
`system.read`.

Автоматический режим обслуживания следит за `postgres` и `remnawave`. После
`APP_MAINTENANCE_FAILURE_STREAK` подряд идущих нездоровых замеров (по умолчанию 3, замеры каждые
`APP_MAINTENANCE_PROBE_SECONDS`, по умолчанию 30) покупки приостанавливаются, а исходящая выдача
доступа удерживается, а не отбраковывается; воркер откладывает задачу, и операция остаётся в
ожидании. См. [режим обслуживания](https://omniflow-vpn.mintlify.site/ru/operations/maintenance-mode.md).

> [!NOTE]
>
> `omniflow_remnawave_requests_total` объявлена в реестре метрик, но ни один путь в коде её не
> увеличивает, а счётчик Prometheus не выдаёт ни одной серии до первого использования. Единственный
> сигнал по Remnawave, реально присутствующий на `/metrics`, — это
> `omniflow_dependency_up{dependency="remnawave"}`. Вызовы адаптера трассируются как дочерние спаны
> через `otelhttp`, но провайдер трассировки нигде в Omniflow не установлен и ни одна переменная
> `OTEL_*` не читается, поэтому эти спаны никуда не уходят, пока вы сами не добавите экспортёр.
> [Наблюдаемость](https://omniflow-vpn.mintlify.site/ru/operations/observability.md) описывает то, что существует.

## Импорт клиентов, которые у вас уже есть

У оператора, переходящего на Omniflow, уже есть пользователи в Remnawave. Импорт обходит
`GET /api/users/?start=&size=`, складывает каждого кандидата с отпечатком, классифицирует его как
корректного, конфликтующего или некорректного и только затем применяет. Конфликты вычисляются
относительно существующих сопоставлений с Remnawave, существующих субъектов идентичности Telegram и
Telegram ID, уже отложенных другими страницами того же прогона, — именно последняя проверка не даёт
импортировать сразу двух пользователей Remnawave, делящих один Telegram ID.

Недоступность Remnawave во время предпросмотра не прерывает прогон: он помечается `failed` с
`remnawave_unavailable` в сводке ошибок и может быть возобновлён со своего курсора. Полная процедура
описана в [клиентах и импортах](https://omniflow-vpn.mintlify.site/ru/commerce/customers-imports.md).

## Чего Omniflow никогда не сделает с вашей панелью

* **Он никогда не трогает базу данных Remnawave.** Любое взаимодействие — это один из четырнадцати
  HTTP-вызовов выше.
* **Он никогда не управляет Xray**, нодами, хостами или определениями squad. На squads ссылаются по
  UUID; создаются и настраиваются они в Remnawave.
* **Он никогда не хранит ссылку доступа.** Колонки с URL подписки нет нигде в схеме;
  `GET /api/subscriptions/by-id/{id}` вызывается вживую на каждый запрос, а ответ несёт
  `Cache-Control: no-store` и `Referrer-Policy: no-referrer`. Ссылка — это учётные данные на
  предъявителя, и Omniflow обращается с ней именно так.
* **Он никогда не раскрывает аппаратный идентификатор.** К устройствам обращаются по дайджесту —
  усечённому SHA-256 в панели оператора, непрозрачному дескриптору в клиентской панели, — а при
  удалении дайджест разрешается обратно на сервере. Значение, которое никогда не доходит до браузера,
  не может из него утечь.
* **Он никогда не создаёт второго пользователя Remnawave** для подписки, у которой он уже есть;
  сначала он пытается восстановить по имени пользователя.
* **Он никогда не воссоздаёт молча то состояние**, исчезновения которого не ожидал:
  стёртый на стороне панели пользователь сначала фиксируется как расхождение `missing_remote`, и лишь
  следующий проход создаёт его заново.
* **Он никогда не проксирует Remnawave.** Ни один эндпоинт Omniflow не пересылает запрос в панель;
  каждый вызов Remnawave исходит от конкретной названной функции.

## Когда Remnawave недоступен

| Поверхность | Что происходит |
| - | - |
| Обзор клиента (`/account`) | Каждая подписка помечается `live: false`, а полезная нагрузка — `degraded`. Страница рендерится из собственной записи Omniflow, а не падает |
| Клиентские маршруты устройств | `503 upstream_unavailable` |
| Операторские маршруты устройств (`/v1/panel`) | `502 remnawave_unavailable` |
| Выдача доступа | Операция остаётся в ожидании; задача повторяется с выдержкой `5s × 2^min(attempt, 8)` до 12 попыток, а после трёх неудач в очередь ставится дедуплицированное уведомление оператору |
| Готовность | Проверка `remnawave` становится нездоровой; после серии неудач открывается режим обслуживания |

Два разных кода для одного условия — это реальная несогласованность в API: клиентская библиотека или
инструкция, которая трактует «Remnawave лежит» как один случай, должна знать оба.

Уведомление оператору несёт только идентификаторы и классифицированный код ошибки — никогда тело
ответа Remnawave. То же и с историей выдачи доступа: она фиксирует дату окончания, были ли настроены
лимиты трафика и устройств, и *количество* squads, но никогда не идентификаторы squads и не ссылки.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.