Перейти к содержанию

Управление туннелями — рычаги администратора

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


Цепочка «заморозки» туннеля

Когда администратор нажимает «заморозить» туннель пользователя в админке, система последовательно выполняет три шага.

Шаг 1 — Источник истины: база данных (PostgreSQL)

POST /api/admin/tunnel/{userId}/freeze

TunnelController.java обновляет поле tunnel_frozen_by в таблице users:

"none"   →   "admin"

Это единственный источник истины — он переживёт перезапуск любого контейнера. Поле может принимать три значения:

Значение Кто заморозил
none Туннель активен
user Пользователь сам выключил туннель
admin Заморожен администратором — пользователь не может разморозить

Шаг 2 — Оперативный кэш прокси: ETS-таблица (Elixir)

Сразу после записи в БД modulauth делает внутренний HTTP-вызов на dixu_proxy:

POST http://dixu_proxy:4000/internal/tunnels/{slug}/freeze
{"by": "admin"}

Elixir-прокси вписывает slug в ETS-таблицу :frozen_tunnels (FrozenRegistry). ETS — это in-memory key-value хранилище внутри Erlang VM, работающее как атомарный hashmap в RAM. Поиск по ней занимает наносекунды при любом трафике.

# FrozenRegistry.ex
def freeze(slug, by \\ "admin"), do: :ets.insert(@table, {slug, by})
def unfreeze(slug),              do: :ets.delete(@table, slug)
def frozen_by(slug) do
  case :ets.lookup(@table, slug) do
    [{_, by}] -> by
    []        -> "none"
  end
end

Шаг 3 — Каждый входящий запрос к туннелю

При любом HTTP-запросе на slug.dix.su прокси первым делом проверяет ETS:

# proxy_controller.ex
frozen_by = DixuProxy.FrozenRegistry.frozen_by(device_slug)
if frozen_by != "none" do
  html = if frozen_by == "admin",
    do:   frozen_admin_html(device_slug),   # «заморожено администратором»
    else: frozen_user_html(device_slug)     # «отключено владельцем»
  # возвращает 503

Посетитель получает HTML-страницу вместо содержимого туннеля.

Активные соединения не убиваются

Freeze блокирует новые запросы, но не разрывает уже установленное WebSocket-соединение device_client'а. Устройство продолжает быть подключено — новый трафик через него просто не проходит.


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

При каждом старте dixu_proxy восстанавливает ETS из БД:

GET http://modulauth:8080/internal/tunnels/frozen
→ {"slugs": ["slug1", "slug2", ...]}

Modulauth возвращает все slug'и с tunnel_frozen_by != 'none'. Elixir инициализирует :frozen_tunnels из этого списка.

Заморозка выдержит перезапуск, обновление, пересборку контейнера.


Схема потоков данных

Администратор (Админка)
    ▼ POST /api/admin/tunnel/{id}/freeze
modulauth (Java/Spring)
    │  UPDATE users SET tunnel_frozen_by = 'admin'   ──▶  PostgreSQL
    ▼ POST http://dixu_proxy:4000/internal/tunnels/{slug}/freeze
dixu_proxy (Elixir)
    │  :ets.insert(:frozen_tunnels, {slug, "admin"})  ──▶  ETS (RAM)
    ▼ при каждом запросе на slug.dix.su
    check ETS → frozen → return 503 HTML

Три рычага управления

Рычаг Где Эффект Обратимость
Заморозка туннеля (tunnel_frozen_by = 'admin') Вкладка «Туннели» Новые запросы к slug получают 503. Пользователь не может разморозить сам. Мгновенно отменяется из той же вкладки.
Смена тарифа Вкладка «Клиенты» Убирает привилегии (TCP, аналитика, compression, приватный режим). Туннель HTTP продолжает работать. Мгновенно.
Удаление аккаунта Вкладка «Клиенты» Slug обнуляется. Device_client теряет возможность подключиться. Туннель становится недоступен навсегда (slug освобождается). Необратимо.

Уведомления

При каждой административной заморозке/разморозке система автоматически отправляет email двум сторонам — администратору (подтверждение действия) и пользователю (уведомление о блокировке с предложением обратиться в поддержку).

// TunnelController.java
.then(emailService.notifyAdminTunnelFroze(callerEmail, user.getEmail(), user.getSlug(), true))

Смотрите также