Skip to main content
Omniflow не заменяет Remnawave. Он стоит рядом и продаёт доступ к нему. Remnawave остаётся авторитетным источником для VPN-пользователей, счётчиков трафика, подключённых устройств, ссылок доступа, нод, хостов и Internal Squads; Omniflow владеет идентичностью, каталогом, заказами, платежами, книгой проводок кошелька, правами на услуги, поддержкой, маркетингом и журналом аудита. Всё это взаимодействие проходит через один написанный вручную HTTP-адаптер, internal/remnawave, и этот адаптер — единственный код в репозитории, который знает о существовании типов, имеющих форму Remnawave. Это не стилистический выбор. Две системы, каждая из которых считает себя владельцем состояния VPN, порождают класс ошибок, которые никто не сможет свести: клиент, чья подписка активна в одном месте и отключена в другом, и никакого способа сказать, где правда. Разделение полномочий делает вопрос разрешимым — ответ Remnawave есть истина о доступе, ответ Omniflow есть истина о деньгах и намерении, — и позволяет обновлять Remnawave, не заставляя Omniflow иметь мнение о его схеме. См. владение данными для концептуальной версии границы и выдачу доступа для того, что происходит после проведения платежа.

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

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

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

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

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

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

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

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

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

curl -s localhost:8080/readyz должен сообщить запись remnawave с "healthy": true. Неверный токен отображается как нездоровая зависимость, а не как здоровая панель — см. Работоспособность и готовность.
Все три процесса отказываются стартовать без обоих значений. API дополнительно отказывается собирать свой коммерческий рантайм без APP_OPERATOR_TOKEN, 32-байтного APP_DATA_ENCRYPTION_KEY и APP_VALKEY_URL. Полный список — в конфигурации.
Ротация токена Remnawave означает правку окружения и перезапуск трёх процессов. На /admin/settings есть раздел Remnawave с базовым URL, полем токена только на запись, интервалом сверки и флагом проверенной совместимости — он сохраняет то, что вы вводите, запечатывая ключом установки, но ни один работающий процесс не читает это обратно. Клиент собирается из APP_REMNAWAVE_URL и APP_REMNAWAVE_TOKEN при старте процесса, и ничто не перечитывает их. См. настройки установки.
Кнопки «проверить подключение» для Remnawave тоже нет. POST /v1/panel/settings/providers/{provider}/test проверяет только платёжные адаптеры и отвечает 422 provider_not_loaded на любое другое имя. Проверкой подключения служит проба готовности.

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

Пятнадцать операций. Это полный список — ничто иное в API Remnawave из Omniflow недостижимо, потому что ничто иное не реализовано. Три узких 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. Оператор ставит операцию в очередь на /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 и диагностические экраны описаны в задачах и очередях и в выдаче доступа.

Детерминированные имена пользователей и восстановление пользователя на стороне 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. Смысл в том, что «кто-то отредактировал Remnawave вручную» превращается в запись, которую можно разобрать, а не в обращение в поддержку. Расхождение всегда оценивается относительно пользователя Remnawave, которым владеет именно эта подписка, поэтому вторая подписка клиента никогда не будет оценена по состоянию первой на стороне Remnawave. Читайте их в /admin → System → Drift (GET /v1/panel/system/drift, system.read) или на поверхности с токеном через GET /v1/admin/fulfillment/drifts?limit=<1..500> (по умолчанию 100).
Пустой список расхождений — нормальное состояние, а не доказательство того, что расхождений никогда не было. В успешном проходе сверки строки расхождений открываются и закрываются внутри той же транзакции, что и корректирующий 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), аутентифицированное чтение, а не запрос к неаутентифицированной странице, поэтому отозванный токен читается как сбой, а не как здоровье.
Отказ несёт стабильную классификацию (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) покупки приостанавливаются, а исходящая выдача доступа удерживается, а не отбраковывается; воркер откладывает задачу, и операция остаётся в ожидании. См. режим обслуживания.
omniflow_remnawave_requests_total объявлена в реестре метрик, но ни один путь в коде её не увеличивает, а счётчик Prometheus не выдаёт ни одной серии до первого использования. Единственный сигнал по Remnawave, реально присутствующий на /metrics, — это omniflow_dependency_up{dependency="remnawave"}. Вызовы адаптера трассируются как дочерние спаны через otelhttp, но провайдер трассировки нигде в Omniflow не установлен и ни одна переменная OTEL_* не читается, поэтому эти спаны никуда не уходят, пока вы сами не добавите экспортёр. Наблюдаемость описывает то, что существует.

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

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

Чего 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 недоступен

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