f09a7a69b3
When TLS ClientHello has no SNI extension (ServerName is empty), normalizeHost returns empty string. Both sniffer_tls.go paths (sniffing and forwarder packages) previously still called dial with an empty host, cascading 'missing port in address' errors through connector, router, and service layers. Now returns early with a debug-level log, silently closing the connection instead of attempting to dial upstream.
242 lines
6.5 KiB
Go
242 lines
6.5 KiB
Go
package sniffing
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"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 nil
|
|
}
|
|
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...)
|
|
}
|