diff --git a/config.compose.yaml b/config.compose.yaml index fb071b5..15ee7d7 100644 --- a/config.compose.yaml +++ b/config.compose.yaml @@ -1,14 +1,11 @@ # Пример для docker compose: без whitelist (только для локальной проверки). # На Linux в docker-compose.yml задано extra_hosts host.docker.internal:host-gateway. +# Без Mihomo: как ниже. С Mihomo в той же сети — см. config.example.yaml и docs/GATEWAY_RUN.md. listen: ":8080" allow_all: true whitelist_cidrs: [] trusted_proxies: [] -# UI отдаётся с того же порта, что и API (образ шлюза) — отдельный CORS для панели не нужен. +# UI отдаётся с тем же портом, что и API (образ шлюза) — отдельный CORS для панели не нужен. servers: - alias: main_srv base_url: http://host.docker.internal:9091 - # Раскомментируйте при настройке Mihomo и общей сети с контейнером mihomo: - # mihomo_base_url: http://mihomo:9090 - # или: mihomo_base_url_env: MIHOMO_CONTROLLER_URL - # mihomo_authorization_env: TELEMT_MIHOMO_AUTH diff --git a/config.example.yaml b/config.example.yaml index 111928a..bb8a875 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -52,41 +52,10 @@ servers: base_url: http://127.0.0.1:9091 path_prefix: /v1 # authorization_env: TELEMT_API_AUTH - # - # --- Mihomo (Clash Meta), раздел «Mihomo» в панели (/servers/{alias}/mihomo) --- - # Клиент: GET /api/{alias}/mihomo/… и WebSocket — шлюз проксирует на external-controller с Bearer. - # - # Типичный compose для самого Mihomo (отдельный файл или сервис рядом с gateway): - # mihomo: - # build: ./mihomo - # container_name: mihomo - # environment: - # - SUB_URL=https://example.com/sub - # - CLASH_SECRET=your-secret-here - # - UPDATE_INTERVAL=86400 - # # ports: # публиковать 9090 на хост не обязательно для панели - # # - "9080:9090" - # networks: - # proxy-net: - # ipv4_address: 172.20.0.2 # пример; контроллер слушает на 9090 внутри контейнера - # - # Контейнер gateway (telemt-api) должен быть в той же сети proxy-net, чтобы: - # - резолвилось имя сервиса http://mihomo:9090, или - # - использовался IP внутри сети, например http://172.20.0.2:9090 - # - # В environment сервиса gateway (не класть секреты в YAML в git): - # MIHOMO_CONTROLLER_URL=http://mihomo:9090 - # TELEMT_MIHOMO_AUTH=Bearer <тот же секрет, что CLASH_SECRET у Mihomo> - # - # Вариант A — URL в YAML: - # mihomo_base_url: http://mihomo:9090 - # mihomo_authorization_env: TELEMT_MIHOMO_AUTH - # - # Вариант B — URL из env (удобно в compose, без правки смонтированного config.yaml): - # mihomo_base_url_env: MIHOMO_CONTROLLER_URL - # mihomo_authorization_env: TELEMT_MIHOMO_AUTH - # - # Значение mihomo_authorization_env: полная строка заголовка Authorization (как у authorization_env для Telemt). + # Mihomo: тот же compose-сервис (container_name: mihomo, сеть proxy-net, external-controller :9090). + # У процесса/контейнера gateway в environment: TELEMT_MIHOMO_AUTH=Bearer ${CLASH_SECRET} (как у mihomo). + mihomo_base_url: http://172.20.0.2:9090 + mihomo_authorization_env: Bearer Ifwrbqifwrbq1995 # ivx: HTTPS + nginx location /api/ → Telemt; base_url должен заканчиваться на /api/ - alias: gt1 diff --git a/docker-compose.yml b/docker-compose.yml index 8e449e8..9c2b379 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,13 +8,11 @@ services: - ./config.compose.yaml:/etc/telemt-gateway/config.yaml:ro environment: CONFIG_PATH: /etc/telemt-gateway/config.yaml - # Опционально: Mihomo в той же Docker-сети, что и gateway (см. docs/GATEWAY_RUN.md) - # MIHOMO_CONTROLLER_URL: http://mihomo:9090 - # TELEMT_MIHOMO_AUTH: Bearer ${CLASH_SECRET} + # Вместе с Mihomo в config.yaml: TELEMT_MIHOMO_AUTH: Bearer ${CLASH_SECRET} # Доступ к Telemt на хосте Linux (host.docker.internal в config.compose.yaml): extra_hosts: - "host.docker.internal:host-gateway" - # При необходимости подключите к сети с контейнером Mihomo: + # С контейнером mihomo в сети proxy-net: # networks: # - proxy-net # networks: diff --git a/docs/GATEWAY_RUN.md b/docs/GATEWAY_RUN.md index 0155c4a..2ae65fe 100644 --- a/docs/GATEWAY_RUN.md +++ b/docs/GATEWAY_RUN.md @@ -86,6 +86,8 @@ servers: - Шлюз проксирует на `{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](../docker-compose.yml). + Поля в конфиге: | Поле | Описание | @@ -112,6 +114,8 @@ services: За **reverse proxy** (nginx) перед панелью убедитесь, что для WebSocket проксируются заголовки `Upgrade` и `Connection`. +Если раздел Mihomo отдаёт **400** и в браузере «Ответ не JSON»: проверьте, что `mihomo_authorization_env` — это **имя** env (например `TELEMT_MIHOMO_AUTH`), а полный заголовок `Bearer …` задан в **environment** контейнера gateway (не в YAML). После обновления шлюза прокси Mihomo использует `Rewrite` и сбрасывает `RequestURI` на исходящем запросе — без этого строгий upstream может отвечать 400. + ## Переменные окружения | Переменная | Описание | diff --git a/internal/proxy/mihomo.go b/internal/proxy/mihomo.go index 16edcd8..78202cc 100644 --- a/internal/proxy/mihomo.go +++ b/internal/proxy/mihomo.go @@ -9,6 +9,8 @@ import ( ) // NewMihomoForward proxies /api/{alias}/mihomo/... to Mihomo external-controller (REST + WebSocket). +// Используется Rewrite (Go 1.20+): очищается RequestURI и задаётся абсолютный URL — иначе строгий upstream +// и WebSocket upgrade могут отвечать 400 (см. аналогично Telemt в forward.go). func NewMihomoForward( target *url.URL, stripPrefix string, @@ -23,27 +25,30 @@ func NewMihomoForward( rt = http.DefaultTransport } rp := &httputil.ReverseProxy{ - Director: func(req *http.Request) { - NormalizeRequestURLPath(req) - p := req.URL.Path + Rewrite: func(pr *httputil.ProxyRequest) { + NormalizeRequestURLPath(pr.In) + p := pr.In.URL.Path if !strings.HasPrefix(p, stripPrefix) { return } - rest := strings.TrimPrefix(p, stripPrefix) - rest = strings.TrimPrefix(rest, "/") - q := req.URL.RawQuery + rest := strings.TrimPrefix(strings.TrimPrefix(p, stripPrefix), "/") dest := JoinPathPrefix(target, "/", rest) - req.URL.Scheme = dest.Scheme - req.URL.Host = dest.Host - req.URL.Path = dest.Path - req.URL.RawQuery = q - req.Host = dest.Host - req.Header.Del("Authorization") + du := *dest + du.RawQuery = pr.In.URL.RawQuery + out := pr.Out + out.URL = &du + out.Host = du.Host + out.RequestURI = "" + out.Proto = "HTTP/1.1" + out.ProtoMajor = 1 + out.ProtoMinor = 1 + out.Header.Del("Authorization") if auth != "" { - req.Header.Set("Authorization", auth) + out.Header.Set("Authorization", auth) } }, - Transport: rt, + Transport: rt, + FlushInterval: -1, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { errHandler(w, r, err) }, @@ -70,4 +75,3 @@ func MihomoJSONError(w http.ResponseWriter, code, msg string) { "error": map[string]string{"code": code, "message": msg}, }) } - diff --git a/web/src/lib/api/client.ts b/web/src/lib/api/client.ts index 4eeb351..2d97721 100644 --- a/web/src/lib/api/client.ts +++ b/web/src/lib/api/client.ts @@ -28,9 +28,20 @@ export function mihomoWsUrl(alias: string, path: string): string { return `${wsBase}/api/${encodeURIComponent(alias)}/mihomo/${p}`; } +async function parseMihomoBody(res: Response): Promise { + const text = await res.text(); + if (!text) return null; + try { + return JSON.parse(text); + } catch { + const snip = text.length > 200 ? `${text.slice(0, 200)}…` : text; + throw new ApiError(`Ответ не JSON (HTTP ${res.status}): ${snip}`, res.status, text); + } +} + export async function fetchMihomoMeta(alias: string): Promise { const res = await fetch(mihomoUrl(alias, 'meta')); - const body = (await parseJson(res)) as Record | null; + const body = (await parseMihomoBody(res)) as Record | null; if (res.status === 404) { throw new ApiError('Mihomo не настроен для этой ноды', 404, body); } @@ -45,7 +56,7 @@ export async function fetchMihomoMeta(alias: string): Promise(alias: string, path: string): Promise { const res = await fetch(mihomoUrl(alias, path)); - const body = await parseJson(res); + const body = await parseMihomoBody(res); if (res.status === 404) { throw new ApiError('Mihomo не настроен для этой ноды', 404, body); } @@ -62,11 +73,11 @@ export async function mihomoPut(alias: string, path: string, jsonBody: unknown): body: JSON.stringify(jsonBody) }); if (res.status === 404) { - const body = await parseJson(res); + const body = await parseMihomoBody(res); throw new ApiError('Mihomo не настроен для этой ноды', 404, body); } if (!res.ok) { - const body = await parseJson(res); + const body = await parseMihomoBody(res); throw new ApiError(`Mihomo PUT HTTP ${res.status}`, res.status, body); } }