Files
x/internal/util/sniffing/sniffer.go
T
ginuerzh 722dde5cfc fix(handler/forward): split readTimeout from pipe idleTimeout to prevent 15s download abort
The readTimeout field (default 15s) was being applied via xnet.Pipe to both
directions of a bidirectional proxy connection. During asymmetric transfers
(e.g. HTTP file download), the direction reading from the tunnel sees no
data after the initial request is forwarded, causing SetReadDeadline to fire
after 15s and abort the entire transfer.

Fix: add a separate idleTimeout field (default 0=disabled) to the metadata
structs in both forward/local and forward/remote handlers, and switch
xnet.Pipe to use idleTimeout instead of readTimeout. The readTimeout field
now only applies to the initial protocol sniffing/handshake phase.

Also document readTimeout vs idleTimeout semantics across all 24 locations
in the x/ module where these timeouts appear:
- readTimeout: handshake sniffing deadline (handlers), upstream response
  header timeout (http.Transport), or transport-level read deadline
- idleTimeout: idle read deadline per Pipe direction (0=disabled)
- ReadTimeout on Sniffer/SnifferBuilder: upstream response header/TLS
  handshake read timeout during sniffing
2026-05-31 17:05:21 +08:00

153 lines
4.5 KiB
Go

package sniffing
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"net"
"strings"
"time"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/recorder"
tls_util "github.com/go-gost/x/internal/util/tls"
xrecorder "github.com/go-gost/x/recorder"
)
const (
// DefaultReadTimeout is the default timeout for reading data from connections.
DefaultReadTimeout = 30 * time.Second
// 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
// DefaultSampleRate is the default websocket sample rate (samples per second).
DefaultSampleRate = 10.0
)
// 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)
dialTLS func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error)
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 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
}
}
// WithDialTLS sets the dial function used for TLS-wrapped upstream connections.
func WithDialTLS(dialTLS func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error)) HandleOption {
return func(opts *HandleOptions) {
opts.dialTLS = dialTLS
}
}
// 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.
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 is the deadline for reading the upstream response
// headers during HTTP sniffing (http.ReadResponse) and the TLS
// ServerHello during TLS sniffing. This timeout is applied once
// per request/response pair in the HTTP keep-alive loop and cleared
// after each response is received. It does NOT affect the client
// connection or the response body transfer.
// Default: DefaultReadTimeout (30s) if not set.
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.
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 the Sniffer's ReadTimeout,
// falling back to DefaultReadTimeout.
func (h *Sniffer) effectiveReadTimeout() time.Duration {
if h.ReadTimeout > 0 {
return h.ReadTimeout
}
return DefaultReadTimeout
}