aba7e2a256
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.
187 lines
5.3 KiB
Go
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)
|
|
}
|