From 5f8cedabd8bc17a28fb80d6b7f0325c2d2e2f172 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sat, 28 Feb 2026 13:55:42 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(update-proxy):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=9C=AC=E5=9C=B0=E4=BB=A3=E7=90=86=E4=B8=8B=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E6=9B=B4=E6=96=B0=20TLS=20=E8=AF=81=E4=B9=A6=E6=9C=AA?= =?UTF-8?q?=E7=9F=A5=E9=A2=81=E5=8F=91=E8=80=85=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在全局代理 HTTP 传输层增加本地回环代理兼容回退能力 - 回退触发条件限制为 unknown authority 且仅 GET/HEAD 请求 - 保留默认 TLS 校验策略并输出告警日志便于审计定位 - refs #139 --- internal/app/global_proxy.go | 104 ++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/internal/app/global_proxy.go b/internal/app/global_proxy.go index 57db384..4dc8686 100644 --- a/internal/app/global_proxy.go +++ b/internal/app/global_proxy.go @@ -1,6 +1,9 @@ package app import ( + "crypto/tls" + "crypto/x509" + "errors" "fmt" "net" "net/http" @@ -26,6 +29,12 @@ var globalProxyRuntime = struct { proxy connection.ProxyConfig }{} +type localProxyTLSFallbackTransport struct { + primary *http.Transport + fallback *http.Transport + proxyEndpoint string +} + func currentGlobalProxyConfig() globalProxySnapshot { globalProxyRuntime.mu.RLock() defer globalProxyRuntime.mu.RUnlock() @@ -139,7 +148,7 @@ func newHTTPClientWithGlobalProxy(timeout time.Duration) *http.Client { return client } -func buildHTTPTransportWithGlobalProxy() *http.Transport { +func buildHTTPTransportWithGlobalProxy() http.RoundTripper { baseTransport, ok := http.DefaultTransport.(*http.Transport) if !ok || baseTransport == nil { return nil @@ -160,7 +169,98 @@ func buildHTTPTransportWithGlobalProxy() *http.Transport { } transport.Proxy = http.ProxyURL(proxyURL) - return transport + if !isLoopbackProxyHost(snapshot.Proxy.Host) { + return transport + } + + fallbackTransport := transport.Clone() + fallbackTransport.TLSClientConfig = cloneTLSConfigWithInsecureSkipVerify(fallbackTransport.TLSClientConfig) + return &localProxyTLSFallbackTransport{ + primary: transport, + fallback: fallbackTransport, + proxyEndpoint: proxyURL.Redacted(), + } +} + +func (t *localProxyTLSFallbackTransport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := t.primary.RoundTrip(req) + if err == nil { + return resp, nil + } + if !isTLSFallbackCandidate(req.Method, err) { + return nil, err + } + + retryReq, cloneErr := cloneRequestForRetry(req) + if cloneErr != nil { + return nil, err + } + logger.Warnf("检测到本地代理 TLS 证书不受信任,启用兼容回退:代理=%s 目标=%s 错误=%v", t.proxyEndpoint, req.URL.String(), err) + return t.fallback.RoundTrip(retryReq) +} + +func isTLSFallbackCandidate(method string, err error) bool { + if !isIdempotentRequestMethod(method) { + return false + } + return isUnknownAuthorityError(err) +} + +func isIdempotentRequestMethod(method string) bool { + switch strings.ToUpper(strings.TrimSpace(method)) { + case http.MethodGet, http.MethodHead: + return true + default: + return false + } +} + +func cloneRequestForRetry(req *http.Request) (*http.Request, error) { + cloned := req.Clone(req.Context()) + if req.Body == nil || req.Body == http.NoBody { + return cloned, nil + } + if req.GetBody == nil { + return nil, fmt.Errorf("request body not replayable") + } + body, err := req.GetBody() + if err != nil { + return nil, err + } + cloned.Body = body + return cloned, nil +} + +func isUnknownAuthorityError(err error) bool { + var unknownErr x509.UnknownAuthorityError + if errors.As(err, &unknownErr) { + return true + } + return strings.Contains(strings.ToLower(err.Error()), "x509: certificate signed by unknown authority") +} + +func cloneTLSConfigWithInsecureSkipVerify(base *tls.Config) *tls.Config { + if base == nil { + return &tls.Config{InsecureSkipVerify: true} + } + cloned := base.Clone() + cloned.InsecureSkipVerify = true + return cloned +} + +func isLoopbackProxyHost(host string) bool { + trimmed := strings.TrimSpace(host) + if trimmed == "" { + return false + } + if strings.EqualFold(trimmed, "localhost") { + return true + } + ip := net.ParseIP(trimmed) + if ip == nil { + return false + } + return ip.IsLoopback() } func buildProxyURLFromConfig(proxyConfig connection.ProxyConfig) (*url.URL, error) {