Files
x/internal/util/sniffing/sniffer_tls.go
T
ginuerzh e814852ca1 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
2026-06-21 16:37:47 +08:00

243 lines
6.5 KiB
Go

package sniffing
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"errors"
"io"
"net"
"time"
"github.com/go-gost/core/bypass"
dissector "github.com/go-gost/tls-dissector"
xbypass "github.com/go-gost/x/bypass"
xio "github.com/go-gost/x/internal/io"
xnet "github.com/go-gost/x/internal/net"
tls_util "github.com/go-gost/x/internal/util/tls"
xrecorder "github.com/go-gost/x/recorder"
)
// HandleTLS sniffs and proxies a TLS connection. It parses the ClientHello
// for SNI-based routing, optionally performs MITM TLS termination for HTTP
// content inspection, and records TLS handshake metadata.
func (h *Sniffer) HandleTLS(ctx context.Context, network string, conn net.Conn, opts ...HandleOption) error {
var ho HandleOptions
for _, opt := range opts {
opt(&ho)
}
readTimeout := h.effectiveReadTimeout()
buf := new(bytes.Buffer)
clientHello, err := dissector.ParseClientHello(io.TeeReader(conn, buf))
if err != nil {
return err
}
log := ho.log
ro := ho.recorderObject
ro.TLS = &xrecorder.TLSRecorderObject{
ServerName: clientHello.ServerName,
ClientHello: hex.EncodeToString(buf.Bytes()),
}
if len(clientHello.SupportedProtos) > 0 {
ro.TLS.Proto = clientHello.SupportedProtos[0]
}
host := normalizeHost(clientHello.ServerName, "443")
if host == "" {
if log != nil {
log.Debugf("no sni in clienthello from %s", conn.RemoteAddr())
}
return errors.New("tls: sni is empty, closing connection")
}
ro.Host = host
if ho.bypass != nil && ho.bypass.Contains(ctx, network, host, bypass.WithService(ho.service)) {
return xbypass.ErrBypass
}
dial := ho.dial
if dial == nil {
dial = (&net.Dialer{}).DialContext
}
cc, err := dial(ctx, network, host)
if err != nil {
return err
}
defer cc.Close()
log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
ro.SrcAddr = cc.LocalAddr().String()
ro.DstAddr = cc.RemoteAddr().String()
if h.Certificate != nil && h.PrivateKey != nil &&
len(clientHello.SupportedProtos) > 0 && (clientHello.SupportedProtos[0] == "h2" || clientHello.SupportedProtos[0] == "http/1.1") {
if host == "" {
host = ro.Host
}
if h.MitmBypass == nil || !h.MitmBypass.Contains(ctx, network, host, bypass.WithService(ho.service)) {
return h.terminateTLS(ctx, network, xnet.NewReadWriteConn(io.MultiReader(buf, conn), conn, conn), cc, clientHello, &ho)
}
}
if _, err := buf.WriteTo(cc); err != nil {
return err
}
xio.SetReadDeadline(cc, time.Now().Add(readTimeout))
serverHello, serverHelloErr := dissector.ParseServerHello(io.TeeReader(cc, buf))
xio.SetReadDeadline(cc, time.Time{})
if serverHello != nil {
ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String()
ro.TLS.CompressionMethod = serverHello.CompressionMethod
if serverHello.Proto != "" {
ro.TLS.Proto = serverHello.Proto
}
if serverHello.Version > 0 {
ro.TLS.Version = tls_util.Version(serverHello.Version).String()
}
}
if buf.Len() > 0 {
ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes())
}
if _, err := buf.WriteTo(conn); err != nil {
return err
}
log.Infof("%s <-> %s", ro.RemoteAddr, ro.Host)
xnet.Pipe(ctx, conn, cc)
log.WithFields(map[string]any{
"duration": time.Since(ro.Time),
}).Infof("%s >-< %s", ro.RemoteAddr, ro.Host)
if serverHelloErr != nil {
return serverHelloErr
}
return nil
}
// terminateTLS performs MITM TLS termination: handshakes with the upstream
// server as a client, then with the downstream client as a server using a
// dynamically generated certificate. The decrypted traffic is then handled
// as HTTP.
func (h *Sniffer) terminateTLS(ctx context.Context, network string, conn, cc net.Conn, clientHello *dissector.ClientHelloInfo, ho *HandleOptions) error {
ro := ho.recorderObject
log := ho.log
nextProtos := clientHello.SupportedProtos
if h.NegotiatedProtocol != "" {
nextProtos = []string{h.NegotiatedProtocol}
}
cfg := &tls.Config{
ServerName: clientHello.ServerName,
NextProtos: nextProtos,
CipherSuites: clientHello.CipherSuites,
}
if cfg.ServerName == "" {
cfg.InsecureSkipVerify = true
}
clientConn := tls.Client(cc, cfg)
if err := clientConn.HandshakeContext(ctx); err != nil {
return err
}
cs := clientConn.ConnectionState()
ro.TLS.CipherSuite = tls_util.CipherSuite(cs.CipherSuite).String()
ro.TLS.Proto = cs.NegotiatedProtocol
ro.TLS.Version = tls_util.Version(cs.Version).String()
host := cfg.ServerName
if host == "" {
if len(cs.PeerCertificates) > 0 {
host = cs.PeerCertificates[0].Subject.CommonName
}
if host == "" {
host = ro.Host
}
}
if h, _, _ := net.SplitHostPort(host); h != "" {
host = h
}
negotiatedProtocol := cs.NegotiatedProtocol
if h.NegotiatedProtocol != "" {
negotiatedProtocol = h.NegotiatedProtocol
}
nextProtos = nil
if negotiatedProtocol != "" {
nextProtos = []string{negotiatedProtocol}
}
// cache the tls server handshake record.
wb := &bytes.Buffer{}
conn = xnet.NewReadWriteConn(conn, io.MultiWriter(wb, conn), conn)
serverConn := tls.Server(conn, &tls.Config{
NextProtos: nextProtos,
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
certPool := h.CertPool
if certPool == nil {
certPool = DefaultCertPool
}
serverName := chi.ServerName
if serverName == "" {
serverName = host
}
cert, cerr := certPool.Get(serverName)
if cert != nil {
pool := x509.NewCertPool()
pool.AddCert(h.Certificate)
if _, cerr = cert.Verify(x509.VerifyOptions{
DNSName: serverName,
Roots: pool,
}); cerr != nil {
log.Warnf("verify cached certificate for %s: %v", serverName, cerr)
cert = nil
}
}
if cert == nil {
cert, cerr = tls_util.GenerateCertificate(serverName, 7*24*time.Hour, h.Certificate, h.PrivateKey)
certPool.Put(serverName, cert)
}
if cerr != nil {
return nil, cerr
}
return &tls.Certificate{
Certificate: [][]byte{cert.Raw},
PrivateKey: h.PrivateKey,
}, nil
},
})
handshakeErr := serverConn.HandshakeContext(ctx)
if record, _ := dissector.ReadRecord(wb); record != nil {
wb.Reset()
record.WriteTo(wb)
ro.TLS.ServerHello = hex.EncodeToString(wb.Bytes())
}
if handshakeErr != nil {
return handshakeErr
}
opts := []HandleOption{
WithDial(func(ctx context.Context, network, address string) (net.Conn, error) {
return clientConn, nil
}),
WithDialTLS(func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) {
return clientConn, nil
}),
WithRecorderObject(ro),
WithLog(log),
}
return h.HandleHTTP(ctx, network, serverConn, opts...)
}