diff --git a/config.example.yaml b/config.example.yaml index bb8a875..795cf80 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -8,6 +8,10 @@ listen: ":8080" +# Диагностика: при 502 от прокси в JSON добавляется error.detail с текстом ошибки RoundTrip (TLS, DNS, таймаут). +# Включайте только во внутренней сети — строка может содержать хосты/порты upstream. +# expose_upstream_errors: true + # If true, IP whitelist is not enforced (development only). allow_all: false diff --git a/docs/GATEWAY_RUN.md b/docs/GATEWAY_RUN.md index 38541c0..8408a52 100644 --- a/docs/GATEWAY_RUN.md +++ b/docs/GATEWAY_RUN.md @@ -96,6 +96,18 @@ servers: | `mihomo_base_url_env` | Имя переменной окружения; если задано и значение **непустое**, URL контроллера берётся из `os.Getenv` при старте (удобно в Docker без хардкода IP). Если env пустой, используется `mihomo_base_url`. | | `mihomo_authorization_env` | Имя env: **полное** значение заголовка `Authorization` (например `Bearer `), как у `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 при старте шлюза. diff --git a/internal/config/config.go b/internal/config/config.go index 333a4fa..839bff8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,8 +15,10 @@ var aliasRe = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`) // Config is the gateway YAML configuration. type Config struct { - Listen string `yaml:"listen"` - AllowAll bool `yaml:"allow_all"` + Listen string `yaml:"listen"` + // ExposeUpstreamErrors puts RoundTrip error text in JSON 502 bodies (error.detail). For private ops only. + ExposeUpstreamErrors bool `yaml:"expose_upstream_errors"` + AllowAll bool `yaml:"allow_all"` WhitelistCIDRs []string `yaml:"whitelist_cidrs"` TrustedProxies []string `yaml:"trusted_proxies"` CorsAllowedOrigins []string `yaml:"cors_allowed_origins"` diff --git a/internal/server/gateway.go b/internal/server/gateway.go index 8a00c44..f9633bf 100644 --- a/internal/server/gateway.go +++ b/internal/server/gateway.go @@ -36,6 +36,19 @@ type Gateway struct { webUI http.Handler } +func writeBadGatewayJSON(w http.ResponseWriter, expose bool, code, message string, upstreamErr error) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusBadGateway) + errObj := map[string]any{"code": code, "message": message} + if expose && upstreamErr != nil { + errObj["detail"] = upstreamErr.Error() + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "ok": false, + "error": errObj, + }) +} + // NewGateway builds handlers and reverse proxies from parsed config. func NewGateway(p *config.Parsed, log *slog.Logger, geo *geoip.Service) (*Gateway, error) { t := proxy.DirectTransport() @@ -54,6 +67,7 @@ func NewGateway(p *config.Parsed, log *slog.Logger, geo *geoip.Service) (*Gatewa transport: t, promHandler: promhttp.Handler(), } + exposeErr := p.Config.ExposeUpstreamErrors for i := range p.Config.Servers { s := &p.Config.Servers[i] u, err := url.Parse(s.BaseURL) @@ -64,12 +78,7 @@ func NewGateway(p *config.Parsed, log *slog.Logger, geo *geoip.Service) (*Gatewa strip := "/api/" + s.Alias g.proxies[s.Alias] = proxy.NewAliasForward(u, strip, s.PathPrefix, auth, t, func(w http.ResponseWriter, r *http.Request, err error) { log.Error("upstream error", "alias", s.Alias, "err", err) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusBadGateway) - _ = json.NewEncoder(w).Encode(map[string]any{ - "ok": false, - "error": map[string]string{"code": "bad_gateway", "message": "upstream unreachable"}, - }) + writeBadGatewayJSON(w, exposeErr, "bad_gateway", "upstream unreachable", err) }) } for alias, m := range p.MihomoByAlias { @@ -77,12 +86,7 @@ func NewGateway(p *config.Parsed, log *slog.Logger, geo *geoip.Service) (*Gatewa strip := "/api/" + a + "/mihomo" g.mihomo[a] = proxy.NewMihomoForward(m.Base, strip, m.Auth, t, func(w http.ResponseWriter, r *http.Request, err error) { log.Error("mihomo upstream error", "alias", a, "err", err) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusBadGateway) - _ = json.NewEncoder(w).Encode(map[string]any{ - "ok": false, - "error": map[string]string{"code": "bad_gateway", "message": "mihomo upstream unreachable"}, - }) + writeBadGatewayJSON(w, exposeErr, "bad_gateway", "mihomo upstream unreachable", err) }) } var aggCacheTTL time.Duration