Update Mihomo configuration and documentation for clarity
Publish telemt-api gateway Docker image / test (push) Successful in 28s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m10s

- Revised comments in `config.example.yaml` to enhance understanding of Mihomo integration, including environment variable usage and Docker Compose setup.
- Updated `docker-compose.yml` comments to clarify the relationship between the gateway and Mihomo service.
- Enhanced `GATEWAY_RUN.md` to provide clearer instructions on configuring Mihomo parameters and their usage in the gateway.
This commit is contained in:
Denozordec
2026-03-31 00:54:41 +07:00
parent a24fc90b39
commit ed785cb8f3
6 changed files with 46 additions and 63 deletions
+2 -5
View File
@@ -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
+4 -35
View File
@@ -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
+2 -4
View File
@@ -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:
+4
View File
@@ -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.
## Переменные окружения
| Переменная | Описание |
+19 -15
View File
@@ -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},
})
}
+15 -4
View File
@@ -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<unknown> {
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<MihomoMetaResponse> {
const res = await fetch(mihomoUrl(alias, 'meta'));
const body = (await parseJson(res)) as Record<string, unknown> | null;
const body = (await parseMihomoBody(res)) as Record<string, unknown> | null;
if (res.status === 404) {
throw new ApiError('Mihomo не настроен для этой ноды', 404, body);
}
@@ -45,7 +56,7 @@ export async function fetchMihomoMeta(alias: string): Promise<MihomoMetaResponse
export async function fetchMihomoJson<T>(alias: string, path: string): Promise<T> {
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);
}
}