From e814852ca170b755250713e1492fda6a5c73d749 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 21 Jun 2026 16:37:47 +0800 Subject: [PATCH] fix(sniffing): re-dial upstream on HTTP Host change + surface no-SNI errors sniffer_http: When DNS override directs multiple domains to the same proxy IP, the browser may reuse a keep-alive connection for a different host. The HTTP keep-alive loop now detects Host header changes and re-dials a new upstream connection with correct node selection, preventing CDN errors (Fastly unknown domain, CloudFront 403). sniffer_tls: Return a descriptive error when TLS ClientHello has no SNI, instead of silently returning nil. This makes the connection drop visible in logs and recorder output. Fixes go-gost/gost#479 --- internal/util/forwarder/sniffer_http.go | 39 ++++++++++++++++++++++++- internal/util/forwarder/sniffer_tls.go | 2 +- internal/util/sniffing/sniffer_http.go | 24 ++++++++++++++- internal/util/sniffing/sniffer_tls.go | 3 +- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/internal/util/forwarder/sniffer_http.go b/internal/util/forwarder/sniffer_http.go index b71458e7..a037cf77 100644 --- a/internal/util/forwarder/sniffer_http.go +++ b/internal/util/forwarder/sniffer_http.go @@ -81,7 +81,9 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, conn net.Conn, opts ...HandleO if err != nil { return err } - defer cc.Close() + defer func() { cc.Close() }() + + upstreamHost := normalizeHost(ro.HTTP.Host, "80") ho.log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()}) log = ho.log @@ -112,6 +114,41 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, conn net.Conn, opts ...HandleO log.Trace(string(dump)) } + // When DNS override directs multiple domains to the same proxy IP, + // the browser may reuse a keep-alive connection for a different host. + // Re-dial to ensure requests reach the correct upstream. + if reqHost := normalizeHost(req.Host, "80"); reqHost != "" && reqHost != upstreamHost { + cc.Close() + newNode, res, resolveErr := resolveHTTPNode(ctx, reqHost, req, &ho) + if resolveErr != nil { + ro.HTTP.StatusCode = res.StatusCode + res.Write(conn) + return resolveErr + } + dial := ho.dial + if dial == nil { + dial = (&net.Dialer{}).DialContext + } + newCC, dialErr := dial(ctx, "tcp", newNode.Addr) + if dialErr != nil { + return dialErr + } + newCC = tlsWrapConn(newCC, newNode.Options().TLS) + upstreamHost = reqHost + node = newNode + cc = newCC + ro.Host = reqHost + ro.SrcAddr = cc.LocalAddr().String() + ro.DstAddr = cc.RemoteAddr().String() + log = log.WithFields(map[string]any{ + "host": reqHost, + "node": node.Name, + "dst": node.Addr, + "src": cc.LocalAddr().String(), + }) + ho.log = log + } + if shouldClose, err := h.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), cc, node, req, &pStats, &ho); err != nil || shouldClose { return err } diff --git a/internal/util/forwarder/sniffer_tls.go b/internal/util/forwarder/sniffer_tls.go index b0bb72fa..af966184 100644 --- a/internal/util/forwarder/sniffer_tls.go +++ b/internal/util/forwarder/sniffer_tls.go @@ -54,7 +54,7 @@ func (h *Sniffer) HandleTLS(ctx context.Context, conn net.Conn, opts ...HandleOp if ho.log != nil { ho.log.Debugf("no sni in clienthello from %s", conn.RemoteAddr()) } - return nil + return errors.New("tls: sni is empty, closing connection") } ro.Host = host diff --git a/internal/util/sniffing/sniffer_http.go b/internal/util/sniffing/sniffer_http.go index 1e855278..12245292 100644 --- a/internal/util/sniffing/sniffer_http.go +++ b/internal/util/sniffing/sniffer_http.go @@ -93,7 +93,9 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, network string, conn net.Conn, if err != nil { return err } - defer cc.Close() + defer func() { cc.Close() }() + + upstreamHost := host log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()}) @@ -122,6 +124,26 @@ func (h *Sniffer) HandleHTTP(ctx context.Context, network string, conn net.Conn, log.Trace(string(dump)) } + // When DNS override directs multiple domains to the same proxy IP, + // the browser may reuse a keep-alive connection for a different host. + // Re-dial to ensure requests reach the correct upstream. + if reqHost := normalizeHost(req.Host, "80"); reqHost != upstreamHost { + cc.Close() + cc, err = dial(ctx, network, reqHost) + if err != nil { + return err + } + upstreamHost = reqHost + ro.Host = reqHost + ro.SrcAddr = cc.LocalAddr().String() + ro.DstAddr = cc.RemoteAddr().String() + log = log.WithFields(map[string]any{ + "host": reqHost, + "src": cc.LocalAddr().String(), + "dst": cc.RemoteAddr().String(), + }) + } + if shouldClose, err := h.httpRoundTrip(ctx, xio.NewReadWriteCloser(br, conn, conn), cc, req, readTimeout, ro, &pStats, log); err != nil || shouldClose { return err } diff --git a/internal/util/sniffing/sniffer_tls.go b/internal/util/sniffing/sniffer_tls.go index 69c8b31e..2209f2b0 100644 --- a/internal/util/sniffing/sniffer_tls.go +++ b/internal/util/sniffing/sniffer_tls.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/hex" + "errors" "io" "net" "time" @@ -52,7 +53,7 @@ func (h *Sniffer) HandleTLS(ctx context.Context, network string, conn net.Conn, if log != nil { log.Debugf("no sni in clienthello from %s", conn.RemoteAddr()) } - return nil + return errors.New("tls: sni is empty, closing connection") } ro.Host = host