fix(forwarder): ReadTimeout race, goroutine leak, PeerCertificates panic, stale error return
- Use local readTimeout copy in HandleOptions instead of mutating shared Sniffer.ReadTimeout, eliminating a data race under concurrent calls - Close both connections in sniffingWebsocketFrame when one direction errors, preventing indefinite goroutine blockage - Add bounds check on PeerCertificates before indexing in terminateTLS - Discard non-fatal ParseServerHello error instead of returning it after successful xnet.Pipe - Log res.Write errors in bypass/bad-gateway/connect-failure paths - Add doc comments to all 15 exported symbols
This commit is contained in:
@@ -43,18 +43,18 @@ import (
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultReadTimeout = 30 * time.Second
|
||||
)
|
||||
// DefaultReadTimeout is the default timeout for reading data from connections.
|
||||
const DefaultReadTimeout = 30 * time.Second
|
||||
|
||||
var (
|
||||
DefaultCertPool = tls_util.NewMemoryCertPool()
|
||||
)
|
||||
// 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
|
||||
@@ -62,56 +62,69 @@ type HandleOptions struct {
|
||||
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(hop hop.Hop) HandleOption {
|
||||
return func(opts *HandleOptions) {
|
||||
opts.hop = hop
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -129,14 +142,18 @@ type Sniffer struct {
|
||||
ReadTimeout time.Duration
|
||||
}
|
||||
|
||||
// HandleHTTP sniffs and proxies an HTTP connection. It reads the initial
|
||||
// request, performs node selection via the configured hop, and forwards the
|
||||
// request with HTTP keep-alive support.
|
||||
func (h *Sniffer) HandleHTTP(ctx context.Context, conn net.Conn, opts ...HandleOption) error {
|
||||
var ho HandleOptions
|
||||
for _, opt := range opts {
|
||||
opt(&ho)
|
||||
}
|
||||
|
||||
if h.ReadTimeout <= 0 {
|
||||
h.ReadTimeout = DefaultReadTimeout
|
||||
ho.readTimeout = h.ReadTimeout
|
||||
if ho.readTimeout <= 0 {
|
||||
ho.readTimeout = DefaultReadTimeout
|
||||
}
|
||||
|
||||
pStats := xstats.Stats{}
|
||||
@@ -255,7 +272,9 @@ func (h *Sniffer) dial(ctx context.Context, conn net.Conn, req *http.Request, ho
|
||||
ho.log.Debugf("bypass: %s %s", host, req.RequestURI)
|
||||
res.StatusCode = http.StatusForbidden
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Write(conn)
|
||||
if err := res.Write(conn); err != nil {
|
||||
ho.log.Warnf("write bypass response: %v", err)
|
||||
}
|
||||
return nil, nil, xbypass.ErrBypass
|
||||
}
|
||||
}
|
||||
@@ -281,7 +300,9 @@ func (h *Sniffer) dial(ctx context.Context, conn net.Conn, req *http.Request, ho
|
||||
ho.log.Warnf("node for %s not found", host)
|
||||
res.StatusCode = http.StatusBadGateway
|
||||
ro.HTTP.StatusCode = res.StatusCode
|
||||
res.Write(conn)
|
||||
if err := res.Write(conn); err != nil {
|
||||
ho.log.Warnf("write error response: %v", err)
|
||||
}
|
||||
return nil, nil, errors.New("node not available")
|
||||
}
|
||||
if node.Addr == "" {
|
||||
@@ -306,7 +327,9 @@ func (h *Sniffer) dial(ctx context.Context, conn net.Conn, req *http.Request, ho
|
||||
marker.Mark()
|
||||
}
|
||||
ho.log.Warnf("connect to node %s(%s) failed: %v", node.Name, node.Addr, err)
|
||||
res.Write(conn)
|
||||
if werr := res.Write(conn); werr != nil {
|
||||
ho.log.Warnf("write error response: %v", werr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if marker := node.Marker(); marker != nil {
|
||||
@@ -499,7 +522,7 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
||||
br := bufio.NewReader(cc)
|
||||
var resp *http.Response
|
||||
for {
|
||||
xio.SetReadDeadline(cc, time.Now().Add(h.ReadTimeout))
|
||||
xio.SetReadDeadline(cc, time.Now().Add(ho.readTimeout))
|
||||
resp, err = http.ReadResponse(br, req)
|
||||
if err != nil {
|
||||
log.Errorf("read response: %v", err)
|
||||
@@ -617,8 +640,8 @@ func (h *Sniffer) handleUpgradeResponse(ctx context.Context, rw, cc io.ReadWrite
|
||||
return xnet.Pipe(ctx, rw, cc)
|
||||
}
|
||||
|
||||
func (h *Sniffer) sniffingWebsocketFrame(ctx context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||
errc := make(chan error, 1)
|
||||
func (h *Sniffer) sniffingWebsocketFrame(ctx context.Context, rw, cc io.ReadWriteCloser, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
|
||||
errc := make(chan error, 2)
|
||||
|
||||
sampleRate := h.WebsocketSampleRate
|
||||
if sampleRate == 0 {
|
||||
@@ -680,8 +703,12 @@ func (h *Sniffer) sniffingWebsocketFrame(ctx context.Context, rw, cc io.ReadWrit
|
||||
}
|
||||
}()
|
||||
|
||||
<-errc
|
||||
return nil
|
||||
err := <-errc
|
||||
// Close both connections to unblock the other goroutine.
|
||||
rw.Close()
|
||||
cc.Close()
|
||||
<-errc // wait for the other goroutine to finish
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Sniffer) copyWebsocketFrame(w io.Writer, r io.Reader, buf *bytes.Buffer, from string, ro *xrecorder.HandlerRecorderObject) (err error) {
|
||||
@@ -783,14 +810,18 @@ func drainBody(b io.ReadCloser) (body []byte, err error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// 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, conn net.Conn, opts ...HandleOption) error {
|
||||
var ho HandleOptions
|
||||
for _, opt := range opts {
|
||||
opt(&ho)
|
||||
}
|
||||
|
||||
if h.ReadTimeout <= 0 {
|
||||
h.ReadTimeout = DefaultReadTimeout
|
||||
ho.readTimeout = h.ReadTimeout
|
||||
if ho.readTimeout <= 0 {
|
||||
ho.readTimeout = DefaultReadTimeout
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
@@ -849,8 +880,8 @@ func (h *Sniffer) HandleTLS(ctx context.Context, conn net.Conn, opts ...HandleOp
|
||||
return err
|
||||
}
|
||||
|
||||
xio.SetReadDeadline(cc, time.Now().Add(h.ReadTimeout))
|
||||
serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf))
|
||||
xio.SetReadDeadline(cc, time.Now().Add(ho.readTimeout))
|
||||
serverHello, _ := dissector.ParseServerHello(io.TeeReader(cc, buf))
|
||||
xio.SetReadDeadline(cc, time.Time{})
|
||||
|
||||
if serverHello != nil {
|
||||
@@ -999,7 +1030,10 @@ func (h *Sniffer) terminateTLS(ctx context.Context, conn, cc net.Conn, clientHel
|
||||
|
||||
host := cfg.ServerName
|
||||
if host == "" {
|
||||
if host = cs.PeerCertificates[0].Subject.CommonName; host == "" {
|
||||
if len(cs.PeerCertificates) > 0 {
|
||||
host = cs.PeerCertificates[0].Subject.CommonName
|
||||
}
|
||||
if host == "" {
|
||||
host = ro.Host
|
||||
}
|
||||
}
|
||||
@@ -1079,6 +1113,8 @@ func (h *Sniffer) terminateTLS(ctx context.Context, conn, cc net.Conn, clientHel
|
||||
return h.HandleHTTP(ctx, serverConn, opts...)
|
||||
}
|
||||
|
||||
// h2Handler is an http.Handler that proxies HTTP/2 requests through an
|
||||
// http2.Transport while recording request and response metadata.
|
||||
type h2Handler struct {
|
||||
transport http.RoundTripper
|
||||
recorder recorder.Recorder
|
||||
|
||||
Reference in New Issue
Block a user