Базовые сервисы для новой архитектуры
Управляющий кластер развернут. Данные мигрированы. API задеплоен и отвечает по HTTPS. Казалось бы — всё готово.
И тут в голове появляется список.
Длинный такой список. Архитектура 2.0 — это не просто «API вместо скриптов». Это полноценная операционная система для сети: полный CRUD по пользователям и устройствам, ротация нод, механизм апелляций, CLI оператора, новый Telegram-бот, периодические снапшоты состояния, перевод Core-нод на схему phone-home… Да много чего еще, на самом деле.
Хочется всего и сразу. Но реальность такова: чтобы сеть заработала хотя бы в минимальном виде, нужно не «всё и сразу», а ровно тот минимум, без которого физически не включить ни одного клиента.
Я сформулировал этот минимум как семь задач — и занимался ими последние пару дней.
NodeService — phone-home для Core-нод. Нода поднимается, отправляет запрос с IP, доменом и именами сервисов, регистрируется в etcd. Без этого кластер не знает о существовании ноды — она есть физически, но для сети её нет.
TokenService — механизм join-токенов. Оператор генерирует одноразовый токен с TTL 24 часа. При регистрации нода предъявляет его и получает право на запись. Без этого — любой желающий мог бы зарегистрировать что угодно одним POST-запросом.
CellService — логическая абстракция над нодами. Ячейка — это Core-нода с назначенным доменом: отдельная точка выхода для трафика. CellService читает ноды из etcd и отдает домен плюс имена сервисов. Сам ничего не пишет — только смотрит.
RouteService — маршруты устройств. Каждое устройство пользователя привязано к конкретной Core-ноде. RouteService знает эту привязку: взять маршрут, обновить, получить список. Без маршрутов subscription не знает, куда слать трафик клиента.
UserService и DeviceService — оба пока read-only. Первый проверяет, что пользователь существует и активен. Второй перечисляет его устройства и отдает UUID каждого — это идентификатор клиента в VLESS-протоколе. Полноценное управление появится позже. Сейчас нужна только возможность читать.
И наконец — GET /subscription/{user_id}. Публичный эндпоинт, без авторизации: пользователь не обязан знать никакие токены, только свой ID. Сервис собирает цепочку: проверяет пользователя → берет устройства → для каждого находит маршрут → смотрит домен ячейки → формирует VLESS-ссылку. Возвращает plain text, по одной ссылке на строку. Это и есть subscription URL — именно его пользователь вставляет в клиент.
В результате: семь сервисов написаны. 151 тест зелёный. Пуш в кластер.
Но первая же грубая проверка в условиях, приближенных к боевым, выявляет три бага:
Первый — в TokenService, в операции отзыва. Я ожидал, что revoke удаляет токен. Он не удалял — он записывал флаг used=true, оставляя запись в etcd нетронутой. Отозванный токен продолжал существовать в хранилище, просто с пометкой. Исправление: удалять все ключи транзакцией. Токен должен исчезать, а не помечаться.
Второй — в NodeService, в регистрации. При повторном POST с уже известным IP нода молча перезаписывала домен и имена сервисов. Статус сохранялся, всё остальное — нет. Один лишний запрос — и нода живёт с чужой конфигурацией, без единого предупреждения. Теперь повторная регистрация возвращает 409. Хочешь изменить ноду — есть PATCH.
Третий — в ApiTokenService. Каждый запрос к API проходит проверку Bearer-токена, но метка last_used_at не обновлялась. Токен мог использоваться сотни раз подряд, а в etcd — тишина, как будто его никто никогда не трогал. Теперь при каждой успешной аутентификации сервис пишет метку в хранилище.
Доработано, протестировано, задеплоено. Вроде бы все работает, как запланировано. Что ж…
Следующий этап — входной кластер. Существующие серверы входных узлов подключаются как K8s worker nodes, на них разворачиваются Entry-поды. Первый шаг к тому, чтобы трафик пользователей пошел по новой схеме — от клиента до интернета.