- Added `expose_upstream_errors` option to the configuration, allowing detailed error messages in JSON responses for 502 errors. - Implemented `writeBadGatewayJSON` function to streamline JSON error responses, including upstream error details when enabled. - Updated documentation to reflect changes in configuration and error handling behavior for improved diagnostics.
24 KiB
Запуск Telemt API Gateway (Docker)
Оглавление:
- Назначение
- Требования
- Минимальная конфигурация
- Переменные окружения
- Готовый образ из registry
- Локальная сборка образа
- Запуск через Docker CLI
- Запуск через Docker Compose
- Проверка
- Обновление и CI/CD
- Устранение неполадок
- Mihomo (external-controller) — при 400 на REST: отладка
Назначение
Шлюз — это один HTTP‑вход для нескольких экземпляров Telemt Control API:
- Web UI (в образе Docker): статика панели на
GET /(и клиентские маршруты SPA), агрегаты и прокси на/api/…. Тот же порт, что и у API (например8080). Исходники UI — каталог web/, сборка встроена в Dockerfile (стадия Node +embedв Go). - Белый список IP (CIDR): кто может обращаться к шлюзу (кроме
GET /health, см. ниже). - Маршрутизация по alias: клиент вызывает
GET /api/{alias}/health, шлюз проксирует на{base_url}/v1/healthу соответствующего сервера. - Метрики Prometheus:
GET /metrics(под тем же правилом whitelist, что и API). - Доверенные прокси: если прямой TCP‑peer входит в
trusted_proxies, для проверки whitelist берётся первый адрес изX-Forwarded-ForилиX-Real-IP.
Эндпоинты и контракт ответов бэкенда описаны в API.md.
Требования
- Установленные Docker и при необходимости Docker Compose v2.
- Для локальной сборки Docker-образа из репозитория: Docker сам подтянет Node на стадии сборки фронта и Go 1.22+ на стадии компиляции (см. Dockerfile).
- Для
go test ./...без Docker на машине нужен только Go 1.22+.
Минимальная конфигурация
Скопируйте config.example.yaml в свой config.yaml и отредактируйте.
Минимальный рабочий фрагмент для разработки (без проверки IP):
listen: ":8080"
allow_all: true
servers:
- alias: main_srv
base_url: http://127.0.0.1:9091
Минимальный фрагмент для продакшена (только перечисленные сети/хосты):
listen: ":8080"
allow_all: false
whitelist_cidrs:
- "203.0.113.10/32"
- "10.0.0.0/8"
servers:
- alias: main_srv
base_url: http://telemt-internal:9091
Правила:
- При
allow_all: falseи пустомwhitelist_cidrsдоступ будет закрыт для всех (кромеGET /health). - В
whitelist_cidrsиtrusted_proxiesдопустимы CIDR (10.0.0.0/8) и одиночный IPv4/IPv6 без маски (87.103.241.8эквивалентно87.103.241.8/32). GET /healthна шлюзе не проверяется по whitelist — так проще настроить DockerHEALTHCHECKи оркестраторы.- Поле
path_prefixпо умолчанию равно/v1(префикс Telemt Control API).
Опционально для бэкенда с включённым auth_header в Telemt задайте в конфиге имя переменной окружения, значение которой будет отправлено как заголовок Authorization на этот upstream:
servers:
- alias: main_srv
base_url: http://telemt:9091
authorization_env: TELEMT_API_AUTH
Значение должно точно совпадать с настроенным в Telemt auth_header (см. API.md).
Mihomo (external-controller)
Опционально для каждого servers[] можно включить проксирование Mihomo (Clash Meta) API: в Web UI появится раздел «Mihomo» для выбранного alias.
- Клиент (браузер) обращается к шлюзу:
GET /api/{alias}/mihomo/proxies, WebSocket…/mihomo/trafficи т.д. - Шлюз проксирует на
{mihomo_base_url}/proxies,…/trafficи т.д. с заголовкомAuthorizationиз переменной окружения (секрет не попадает в фронтенд). - Служебный ответ:
GET /api/{alias}/mihomo/meta— JSON с полемcontroller_base(без учётных данных) для строки «Подключено к: …» в панели.
В config.yaml задаются только параметры шлюза (mihomo_base_url / mihomo_base_url_env, mihomo_authorization_env). Сервис контейнера Mihomo (build, CLASH_SECRET, сеть proxy-net и т.д.) описывается в вашем Docker Compose отдельно; связка URL и переменных для gateway — ниже и в docker-compose.yml.
Поля в конфиге:
| Поле | Описание |
|---|---|
mihomo_base_url |
Базовый URL контроллера, например http://mihomo:9090 (порт контроллера по умолчанию в контейнере Mihomo — 9090). |
mihomo_base_url_env |
Имя переменной окружения; если задано и значение непустое, URL контроллера берётся из os.Getenv при старте (удобно в Docker без хардкода IP). Если env пустой, используется mihomo_base_url. |
mihomo_authorization_env |
Имя env: полное значение заголовка Authorization (например Bearer <secret>), как у authorization_env для Telemt. Должно совпадать с секретом на стороне Mihomo (secret / CLASH_SECRET в конфиге ядра). |
У нескольких записей servers[] можно задать разные имена переменных (mihomo_authorization_env: TELEMT_MIHOMO_MTG и …_GT1), если секреты контроллеров на нодах различаются. Одно и то же имя env для всех нод — нормально, если везде один и тот же токен.
Локальный Mihomo (HTTP в Docker) работает, удалённый (HTTPS) даёт 502 через шлюз
Прямой curl с хоста к https://…:8443 может быть успешен, а шлюз при этом отдаёт 502 с mihomo upstream unreachable: исходящий запрос делает процесс шлюза (часто контейнер). Отличия от «рабочего» curl:
- другая сеть/DNS из контейнера;
- другой исходящий IP на стороне nginx Mihomo (whitelist
allow); - ошибка TLS при проверке сертификата из окружения процесса.
Включите в config.yaml expose_upstream_errors: true, перезапустите шлюз и повторите запрос: в JSON появится error.detail с текстом ошибки (x509: …, dial tcp …, lookup … и т.д.). После диагностики флаг отключите.
Правила:
- Если указан
mihomo_base_urlилиmihomo_base_url_env, обязательно задайтеmihomo_authorization_envи непустые значения в env при старте шлюза. - Контейнер gateway должен иметь сетевую связность с контроллером Mihomo (лучше одна пользовательская Docker-сеть; имя сервиса
http://mihomo:9090предпочтительнее статического IP). Публиковать порт 9090 на хост не обязательно: браузер ходит в шлюз, шлюз — в контейнер Mihomo по overlay-сети. - Пример
environmentдля compose (секреты не в git — через.env):
services:
gateway:
environment:
MIHOMO_CONTROLLER_URL: http://mihomo:9090
TELEMT_MIHOMO_AUTH: Bearer ${CLASH_SECRET}
и в config.yaml для нужного сервера: mihomo_base_url_env: MIHOMO_CONTROLLER_URL, mihomo_authorization_env: TELEMT_MIHOMO_AUTH.
За reverse proxy (nginx) перед панелью убедитесь, что для WebSocket проксируются заголовки Upgrade и Connection. Панель Mihomo тянет трафик и память потоковым HTTP (не WS); при задержке или «залипании» графиков за nginx включите для upstream шлюза proxy_buffering off (или эквивалент), чтобы не буферизовать длинный ответ.
Отладка 400 на маршрутах Mihomo (/api/{alias}/mihomo/…)
Если напрямую к контроллеру всё ок (curl -H "Authorization: Bearer …" http://mihomo:9090/version → JSON), а через шлюз те же пути дают 400 и в логах шлюза duration_ms около 1 — проблема в сборке исходящего HTTP на шлюзе, а не в секрете Mihomo.
Что зафиксировано в коде и не стоит «упрощать» назад без причины:
| Что | Зачем |
|---|---|
REST (GET /version, /proxies, /connections, POST к API контроллера и т.д.) |
Тот же путь, что и прокси к Telemt: http.NewRequest + RoundTrip (internal/proxy/forward.go, общая логика newAliasForward). Так совпадает с рабочим curl к upstream. |
Не единый httputil.ReverseProxy на весь Mihomo |
Для обычного HTTP ReverseProxy нередко даёт 400 у строгих upstream при том, что прямой запрос работает. |
WebSocket (/traffic, /memory, …) |
На upstream — отдельно httputil.ReverseProxy + Rewrite (internal/proxy/mihomo.go). Веб-панель использует нативный WebSocket к шлюзу для метрик и параллельно короткие HTTP GET (резерв); шлюз подставляет Authorization к Mihomo, токен в браузер не передаётся. |
| Заголовок Host | Не пересылается с клиента; на upstream уходит authority из mihomo_base_url (как для Telemt и base_url). |
Проверки конфигурации:
mihomo_authorization_envв YAML — это имя переменной; полная строкаAuthorization(напримерBearer <secret>) задаётся в environment контейнера gateway, не в YAML.- После изменений в этом месте пересоберите образ / задеплойте актуальный бинарь — иначе будет старое поведение.
Переменные окружения
| Переменная | Описание |
|---|---|
CONFIG_PATH |
Путь к YAML внутри контейнера. По умолчанию: /etc/telemt-gateway/config.yaml. |
TELEMT_API_AUTH |
Пример: секрет для authorization_env в конфиге (имя может быть любым). |
MIHOMO_CONTROLLER_URL |
Пример: URL для mihomo_base_url_env (если используете в конфиге). |
TELEMT_MIHOMO_AUTH |
Пример: Bearer … для mihomo_authorization_env (если используете). |
Готовый образ из registry
CI публикует образ в Container Registry Gitea. Для репозитория denozord/telemt-api на git.shts.su стабильный тег:
docker pull git.shts.su/denozord/telemt-api:latest
Если registry не публичный, сначала войдите (логин — пользователь Gitea, пароль — personal access token с правом read:package):
docker login git.shts.su
Другие полезные теги из того же workflow: имя ветки (с / заменённым на -) и sha-<12 символов коммита> — см. раздел Обновление и CI/CD.
Локальная сборка образа
Если нужно собрать образ самостоятельно из клона репозитория:
docker build -t telemt-api-gateway:local .
Сборка многостадийная: сначала npm ci + npm run build в каталоге web/ (в бандл вшивается пустой PUBLIC_TELEMT_GATEWAY_URL, запросы API с того же origin), затем компиляция Go со встраиванием web/build через embed. В командах docker run ниже вместо имени из registry подставьте telemt-api-gateway:local.
Запуск через Docker CLI
Пример для Linux (подставьте путь к config.yaml; ниже — файл из текущего каталога). Используется готовый образ:
docker run -d --name telemt-gateway \
-p 8080:8080 \
-v "$(pwd)/config.yaml:/etc/telemt-gateway/config.yaml:ro" \
-e CONFIG_PATH=/etc/telemt-gateway/config.yaml \
git.shts.su/denozord/telemt-api:latest
Проверка:
curl -sS -i http://127.0.0.1:8080/health
curl -sS -i http://127.0.0.1:8080/api/main_srv/health
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/
Второй запрос проксируется на {base_url}/v1/health для alias main_srv. Третий — HTML панели (в образах, собранных с Web UI; ожидайте 200).
Панель в браузере: http://127.0.0.1:8080/ (при allow_all: false ваш IP должен быть в whitelist_cidrs, иначе для / будет 403, как и для API).
Остановка и удаление:
docker stop telemt-gateway
docker rm telemt-gateway
Запуск через Docker Compose
В репозитории есть docker-compose.yml (один сервис gateway) и пример config.compose.yaml с allow_all: true и base_url: http://host.docker.internal:9091 (Telemt на хосте; на Docker Desktop для Linux это обычно работает из коробки). Образ по умолчанию скачивается из registry, локальная сборка не требуется. UI доступен на том же порту, что и шлюз: http://127.0.0.1:8080/.
docker compose pull
docker compose up -d
docker compose logs -f gateway
docker compose down
Чтобы пересобрать образ из исходников вместо pull, в docker-compose.yml временно замените блок image: на build: . (и нужный тег при необходимости).
На Linux без host.docker.internal сделайте одно из:
- в
docker-compose.ymlдля сервисаgatewayдобавьтеextra_hosts: ["host.docker.internal:host-gateway"](Docker Engine 20.10+); - либо замените в
config.compose.yamlзначениеbase_urlна IP хоста в docker‑bridge (часто172.17.0.1) или на имя сервиса Telemt в той же сети compose.
Проверка
| Сценарий | Ожидание |
|---|---|
GET /health |
200, JSON {"status":"ok"} |
GET / (образ с Web UI) |
200, HTML панели |
| Разрешённый IP, корректный alias | ответ бэкенда (например 200 для /v1/health) |
| IP не в whitelist | 403, JSON с code: forbidden |
| Неизвестный alias | 404, JSON с code: not_found |
| Бэкенд недоступен | 502, JSON с code: bad_gateway |
GET /metrics |
текст метрик Prometheus (при разрешённом IP) |
Обновление и CI/CD
-
Образ: подтяните свежий тег (
docker pull git.shts.su/denozord/telemt-api:latestилиdocker compose pull), пересоздайте контейнер (docker compose up -dили новыйdocker runс тем же volume конфига). Локальная пересборка нужна только если вы меняете Dockerfile/код и не пользуетесь CI. Образы, собранные до добавления стадииweb/в Dockerfile, могут отдавать на/только заглушку — нужен образ из актуального CI или локальныйdocker build. -
Конфиг: отредактируйте файл на хосте и перезапустите контейнер (шлюз не перечитывает конфиг на лету).
-
Gitea Actions: workflow .gitea/workflows/docker.yaml сначала выполняет
go mod tidy && go test ./..., затем собирает образ через Buildx по Dockerfile (стадии Node дляweb/и Go) и пушит в Container Registry Gitea.- В репозитории должен быть secret
ACTIONS_PAT— personal access token пользователя с правомwrite:package(и при необходимостиread:package), как для обычногоdocker loginк registry. - Логин в registry: пользователь
gitea.actor(кто запустил workflow), пароль — этот PAT. - Хост registry задаётся в workflow в
env.REGISTRY(по умолчаниюgit.shts.su); при другом инстансе Gitea измените значение в.gitea/workflows/docker.yaml. - Теги образа:
latest, имя ветки/тега (с/заменённым на-), иsha-<первые 12 символов коммита>. Полный путь:{REGISTRY}/{gitea.repository}:<тег>.
- В репозитории должен быть secret
Устранение неполадок
- Nginx с
location /api/иproxy_pass http://…:9091/;(со слэшем в конце) на бэкенд уходит путь без префикса/api/(например запрос к nginxGET /api/v1/usersпревращается вGET /v1/usersна Telemt). Шлюз приbase_url: https://gt2.example/api/должен запрашивать именно/api/v1/…на стороне nginx. Если вbase_urlнет пути/api/(толькоhttps://gt2.example), шлюз обратится кhttps://gt2.example/v1/…— часто это не попадает вlocation /api/, и nginx отдаёт чужой vhost / заглушку. Задавайтеbase_urlс завершающим слэшем:https://gt2.example/api/. - Заголовок
Host: шлюз выставляетHostравным хосту изbase_url(как у обычного клиента к этому имени). Если после обновления образа проблема остаётся, с хоста шлюза проверьте:curl -sv -o /dev/null https://gt2…/api/v1/healthи сравните с запросом через шлюз. 400на/api/{alias}/…при локальномbase_url(напримерhttp://172.20.0.3:9091), хотяcurlк:9091/v1/…даёт200: частая причина — несовпадение заголовкаHost: браузер шлётHost: публичное_имя:8888, а при прямомcurlк IP вHostпопадает172.20.0.3:9091. Строгий upstream (часто hyper/Rust) отвечает400, еслиHostне совпадает с ожидаемым authority. Шлюз при проксировании не пересылает клиентскийHostи выставляет authority изbase_url(как серверные запросы агрегатора). Убедитесь также, что в YAML нет пробела/переноса в концеbase_url(поля обрезаютсяTrimSpace), и в URL нет лишнего/api//…(путь под/apiнормализуется).400только на/api/{alias}/mihomo/…, при этом прямойcurlк контроллеру Mihomo ок: см. Отладка 400 (Mihomo) в разделе Mihomo — REST к Mihomo идёт через тот же механизм, что и к Telemt; не путать с отдельным WebSocket-прокси.- Список пользователей через шлюз: запрос
GETилиHEADна/api/{alias}/usersшлюз перенаправляет на upstreamGET/HEAD /v1/stats/users(как и агрегатор). Так совместимы сборки Telemt, где прямойGET /v1/usersдаёт ошибку (например400), а/v1/stats/usersработает.POST /api/{alias}/users(создание) иGET /api/{alias}/users/{username}по-прежнему идут на/v1/usersи/v1/users/{username}. Явный путь/api/{alias}/stats/usersне меняется. См. API.md. docker pull:unauthorized/denied: выполнитеdocker login git.shts.suс учётной записью Gitea и PAT сread:package.403 forbiddenс хоста приallow_all: false: добавьте CIDR клиента вwhitelist_cidrs. Запросы из контейнера к самому себе идут с127.0.0.1— при необходимости добавьте127.0.0.1/32.- За reverse proxy: укажите CIDR прокси в
trusted_proxies, иначе whitelist видит IP прокси, а не клиента. 502 bad_gateway: проверьтеbase_url, DNS в Docker‑сети и то, что Telemt слушает API ([server.api].enabled=trueи корректныйlisten).- Сборка Go без Docker: в корне репозитория выполните
go mod tidy && go test ./....