Files
x/internal/util/forwarder/sniffer.go
T
ginuerzh aba7e2a256 refactor(forwarder): split sniffer monolith into focused files, extract testable helpers
Split ~1250-line sniffer.go into sniffer_http.go (HTTP handling, dial, round-trip),
sniffer_tls.go (TLS MITM, termination), sniffer_h2.go (HTTP/2), sniffer_ws.go
(WebSocket frame copy/record), and sniffer_rewrite.go (upgrade, body rewrite).

Extract clampBodySize, normalizeHost, effectiveReadTimeout as package-level
functions to simplify testing. Add 22 tests covering pure functions, option
setters, HTTP proxy integration, WebSocket frame copy, and edge cases.
2026-05-26 22:48:27 +08:00

187 lines
5.3 KiB
Go

package forwarder
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"net"
"strings"
"time"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/hop"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/recorder"
"github.com/go-gost/x/config"
tls_util "github.com/go-gost/x/internal/util/tls"
xrecorder "github.com/go-gost/x/recorder"
)
const (
// defaultBodySize is the default HTTP body or websocket frame size to record.
defaultBodySize = 64 * 1024 // 64KB
// maxBodySize is the maximum HTTP body or websocket frame size to record.
maxBodySize = 1024 * 1024 // 1MB
)
// DefaultReadTimeout is the default timeout for reading data from connections.
const DefaultReadTimeout = 30 * time.Second
// DefaultCertPool is the default in-memory certificate pool used for TLS MITM.
var DefaultCertPool = tls_util.NewMemoryCertPool()
// HandleOptions holds configuration options for sniffing handlers.
type HandleOptions struct {
service string
dial func(ctx context.Context, network, address string) (net.Conn, error)
httpKeepalive bool
readTimeout time.Duration
node *chain.Node
hop hop.Hop
bypass bypass.Bypass
recorderObject *xrecorder.HandlerRecorderObject
log logger.Logger
}
// HandleOption configures HandleOptions for sniffing handlers.
type HandleOption func(opts *HandleOptions)
// WithService sets the service name for bypass and selection lookups.
func WithService(service string) HandleOption {
return func(opts *HandleOptions) {
opts.service = service
}
}
// WithDial sets the dial function used to establish upstream connections.
func WithDial(dial func(ctx context.Context, network, address string) (net.Conn, error)) HandleOption {
return func(opts *HandleOptions) {
opts.dial = dial
}
}
// WithHTTPKeepalive enables or disables HTTP keep-alive on the upstream connection.
func WithHTTPKeepalive(keepalive bool) HandleOption {
return func(opts *HandleOptions) {
opts.httpKeepalive = keepalive
}
}
// WithNode sets a pre-resolved chain node to connect to, bypassing hop selection.
func WithNode(node *chain.Node) HandleOption {
return func(opts *HandleOptions) {
opts.node = node
}
}
// WithHop sets the hop used for node selection when routing requests.
func WithHop(h hop.Hop) HandleOption {
return func(opts *HandleOptions) {
opts.hop = h
}
}
// WithBypass sets the bypass rules for filtering requests by host.
func WithBypass(bypass bypass.Bypass) HandleOption {
return func(opts *HandleOptions) {
opts.bypass = bypass
}
}
// WithRecorderObject sets the recorder object for capturing traffic metadata.
func WithRecorderObject(ro *xrecorder.HandlerRecorderObject) HandleOption {
return func(opts *HandleOptions) {
opts.recorderObject = ro
}
}
// WithLog sets the logger for the handler.
func WithLog(log logger.Logger) HandleOption {
return func(opts *HandleOptions) {
opts.log = log
}
}
// Sniffer handles HTTP and TLS traffic sniffing, recording, and MITM TLS
// termination for protocol-aware forwarding. It can intercept HTTP requests,
// perform hop/node selection, apply bypass rules, rewrite URLs and response
// bodies, and terminate TLS for content inspection.
type Sniffer struct {
Websocket bool
WebsocketSampleRate float64
Recorder recorder.Recorder
RecorderOptions *recorder.Options
// MITM TLS termination
Certificate *x509.Certificate
PrivateKey crypto.PrivateKey
NegotiatedProtocol string
CertPool tls_util.CertPool
MitmBypass bypass.Bypass
ReadTimeout time.Duration
}
// clampBodySize returns the effective body capture size from recorder options,
// bounded by [defaultBodySize, maxBodySize]. Returns 0 if body recording is
// disabled.
func clampBodySize(opts *recorder.Options) int {
if opts == nil || !opts.HTTPBody {
return 0
}
size := opts.MaxBodySize
if size <= 0 {
size = defaultBodySize
}
if size > maxBodySize {
size = maxBodySize
}
return size
}
// normalizeHost ensures host contains a port component. If host is already
// in host:port form it is returned unchanged; otherwise defaultPort is appended.
// IPv6 addresses are handled correctly (brackets are stripped before joining).
func normalizeHost(host, defaultPort string) string {
if host == "" {
return host
}
if _, _, err := net.SplitHostPort(host); err != nil {
return net.JoinHostPort(strings.Trim(host, "[]"), defaultPort)
}
return host
}
// effectiveReadTimeout returns the read timeout from options, falling back to
// the Sniffer's ReadTimeout, then to DefaultReadTimeout.
func (h *Sniffer) effectiveReadTimeout(ho *HandleOptions) time.Duration {
if ho.readTimeout > 0 {
return ho.readTimeout
}
if h.ReadTimeout > 0 {
return h.ReadTimeout
}
return DefaultReadTimeout
}
// tlsWrapConn wraps cc in a TLS client using the node's TLS settings.
func tlsWrapConn(cc net.Conn, tlsSettings *chain.TLSNodeSettings) net.Conn {
if tlsSettings == nil {
return cc
}
cfg := &tls.Config{
ServerName: tlsSettings.ServerName,
InsecureSkipVerify: !tlsSettings.Secure,
}
tls_util.SetTLSOptions(cfg, &config.TLSOptions{
MinVersion: tlsSettings.Options.MinVersion,
MaxVersion: tlsSettings.Options.MaxVersion,
CipherSuites: tlsSettings.Options.CipherSuites,
ALPN: tlsSettings.Options.ALPN,
})
return tls.Client(cc, cfg)
}