Files
ginuerzh 486a77665d fix: prevent double-recording when HTTP response cache is enabled
The cache-hit path (serveCachedResponse) recorded on the same HandlerRecorderObject
as the calling handler's deferred Record(), producing two records per request.
Copy ro at HandleHTTP entry so the sniffer's internal recording stays isolated
from the caller's defer. Also zero ro.Time when the sniffer takes over, so the
handler's defer Record() skips the now-redundant connection-level record.

Fixes 10 files across sniffer and all 8 handlers that support sniffed HTTP forwarding.
2026-07-26 20:11:36 +08:00

238 lines
7.4 KiB
Go

// Package remote implements a reverse forwarding handler for connections
// received from remote GOST nodes. It listens for "rtcp", "rudp", and "runix" protocols
// and forwards the accepted connections to the configured hop.
//
// The handler is registered under the names "rtcp", "rudp", and "runix" via
// NewHandler in init().
//
// # Connection processing flow
//
// Each inbound net.Conn is processed by Handle, which wraps the connection
// with I/O stats, checks the rate limiter, and dispatches to one of two
// forwarding paths:
//
// Handle()
// ├─ stats_wrapper.WrapConn (per-connection I/O counters)
// ├─ newRecorderObject (session metadata: service, SID, addresses)
// ├─ checkRateLimit (connection rate limiter, if configured)
// ├─ Router nil check → errRouterNotAvailable
// ├─ [sniffing enabled] sniffing.Sniff (peek buffer, detect protocol)
// │ └─ handleSniffedProtocol()
// │ ├─ sniffing.ProtoHTTP → SnifferBuilder.Build().HandleHTTP()
// │ ├─ sniffing.ProtoTLS → SnifferBuilder.Build().HandleTLS()
// │ └─ default → fall through to raw forwarding
// └─ [sniffing disabled / unrecognised] handleRawForwarding()
//
// # Protocol sniffing dispatch (handleSniffedProtocol)
//
// When sniffing is enabled and the initial bytes match HTTP or TLS, the
// connection is delegated to the forwarder package for protocol-aware
// handling:
//
// - HTTP: parses the request, applies MITM if configured, forwards to
// the upstream, and records WebSocket frames when enabled.
// - TLS: parses the ClientHello to extract the server name, applies
// MITM or SNI-based routing through the hop, and records the TLS
// handshake.
//
// SnifferBuilder is populated once during Init and reused via Build()
// to create per-connection forwarder.Sniffer instances.
//
// # Raw forwarding (handleRawForwarding)
//
// The default path for unrecognised traffic. It selects a target node
// from context metadata or the hop, dials the upstream through the
// router, wraps the connection with proxy protocol (HAProxy) when
// enabled, and pipes data bidirectionally:
//
// 1. Node selection — context host metadata takes priority, then
// getHop().Select() with optional protocol hint.
// 2. Router dial — Router.Dial() through the configured chain.
// 3. Proxy protocol — proxyproto.WrapClientConn() prepends HAProxy
// header when metadata proxyProtocol is set.
// 4. Bidirectional pipe — xnet.Pipe(ctx, conn, cc) with read timeout.
//
// # Hop management
//
// The handler implements handler.Forwarder. Forward() is called during
// service startup (and on reload) to install or update the hop. The hop
// is protected by a mutex because Forward() runs on the loader goroutine
// while Handle() reads it from per-connection goroutines.
package remote
import (
"bufio"
"context"
"net"
"sync"
"time"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/hop"
md "github.com/go-gost/core/metadata"
"github.com/go-gost/core/observer/stats"
"github.com/go-gost/core/recorder"
xnet "github.com/go-gost/x/internal/net"
"github.com/go-gost/x/internal/util/httpcache"
"github.com/go-gost/x/internal/util/sniffing"
tls_util "github.com/go-gost/x/internal/util/tls"
rate_limiter "github.com/go-gost/x/limiter/rate"
xstats "github.com/go-gost/x/observer/stats"
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
xrecorder "github.com/go-gost/x/recorder"
"github.com/go-gost/x/registry"
)
func init() {
registry.HandlerRegistry().Register("rtcp", NewHandler)
registry.HandlerRegistry().Register("rudp", NewHandler)
registry.HandlerRegistry().Register("runix", NewHandler)
}
type forwardHandler struct {
hop hop.Hop
hopMu sync.Mutex
md metadata
options handler.Options
recorder recorder.RecorderObject
certPool tls_util.CertPool
sniffer *SnifferBuilder
}
// NewHandler creates a remote forwarding handler with the given options.
// The handler registers for "rtcp", "rudp", and "runix" protocols.
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
return &forwardHandler{
options: options,
}
}
func (h *forwardHandler) Init(md md.Metadata) (err error) {
if err = h.parseMetadata(md); err != nil {
return
}
for _, ro := range h.options.Recorders {
if ro.Record == xrecorder.RecorderServiceHandler {
h.recorder = ro
break
}
}
if h.md.certificate != nil && h.md.privateKey != nil {
h.certPool = tls_util.NewMemoryCertPool()
}
h.sniffer = &SnifferBuilder{
Websocket: h.md.sniffingWebsocket,
WebsocketSampleRate: h.md.sniffingWebsocketSampleRate,
Recorder: h.recorder.Recorder,
RecorderOptions: h.recorder.Options,
Certificate: h.md.certificate,
PrivateKey: h.md.privateKey,
ALPN: h.md.alpn,
CertPool: h.certPool,
MitmBypass: h.md.mitmBypass,
ReadTimeout: h.md.readTimeout,
Cache: httpcache.FromMetadata(h.options.Cache, md),
}
return
}
// Forward implements handler.Forwarder.
func (h *forwardHandler) Forward(hop hop.Hop) {
h.hopMu.Lock()
h.hop = hop
h.hopMu.Unlock()
}
func (h *forwardHandler) getHop() hop.Hop {
h.hopMu.Lock()
defer h.hopMu.Unlock()
return h.hop
}
// Handle forwards the accepted connection to a node selected from the hop.
// When sniffing is enabled it inspects the initial bytes to detect HTTP or TLS
// traffic and delegates to the protocol-specific sniffer; otherwise it pipes the
// raw stream through the router.
func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
defer conn.Close()
start := time.Now()
ro := h.newRecorderObject(ctx, conn, start)
network := ro.Network
log := h.options.Logger.WithFields(map[string]any{
"network": ro.Network,
"remote": conn.RemoteAddr().String(),
"local": conn.LocalAddr().String(),
"client": ro.ClientAddr,
"sid": ro.SID,
})
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
pStats := xstats.Stats{}
conn = stats_wrapper.WrapConn(conn, &pStats)
defer func() {
if err != nil {
ro.Err = err.Error()
}
ro.InputBytes = pStats.Get(stats.KindInputBytes)
ro.OutputBytes = pStats.Get(stats.KindOutputBytes)
ro.Duration = time.Since(start)
if err := ro.Record(ctx, h.recorder.Recorder); err != nil {
log.Errorf("record: %v", err)
}
log.WithFields(map[string]any{
"duration": time.Since(start),
"inputBytes": ro.InputBytes,
"outputBytes": ro.OutputBytes,
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}()
if !h.checkRateLimit(conn.RemoteAddr()) {
return rate_limiter.ErrRateLimit
}
if h.options.Router == nil {
log.Error(errRouterNotAvailable)
return errRouterNotAvailable
}
var proto string
if network == "tcp" && h.md.sniffing {
if h.md.sniffingTimeout > 0 {
conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout))
}
br := bufio.NewReader(conn)
proto, err = sniffing.Sniff(ctx, br)
ro.Proto = proto
if err != nil {
log.Debugf("sniff: %v", err)
}
if h.md.sniffingTimeout > 0 {
conn.SetReadDeadline(time.Time{})
}
conn = xnet.NewReadWriteConn(br, conn, conn)
handled, sniffErr := h.handleSniffedProtocol(ctx, conn, ro, log, proto)
if handled {
ro.Time = time.Time{}
return sniffErr
}
}
return h.handleRawForwarding(ctx, conn, ro, log, network, proto)
}