mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
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.
288 lines
7.6 KiB
Go
288 lines
7.6 KiB
Go
package redirect
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/bypass"
|
|
"github.com/go-gost/core/handler"
|
|
md "github.com/go-gost/core/metadata"
|
|
"github.com/go-gost/core/observer/stats"
|
|
"github.com/go-gost/core/recorder"
|
|
xbypass "github.com/go-gost/x/bypass"
|
|
xctx "github.com/go-gost/x/ctx"
|
|
ictx "github.com/go-gost/x/internal/ctx"
|
|
xnet "github.com/go-gost/x/internal/net"
|
|
"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("red", NewHandler)
|
|
registry.HandlerRegistry().Register("redir", NewHandler)
|
|
registry.HandlerRegistry().Register("redirect", NewHandler)
|
|
}
|
|
|
|
type redirectHandler struct {
|
|
md metadata
|
|
options handler.Options
|
|
recorder recorder.RecorderObject
|
|
certPool tls_util.CertPool
|
|
}
|
|
|
|
func NewHandler(opts ...handler.Option) handler.Handler {
|
|
options := handler.Options{}
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
return &redirectHandler{
|
|
options: options,
|
|
}
|
|
}
|
|
|
|
func (h *redirectHandler) 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()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (h *redirectHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
|
|
defer conn.Close()
|
|
|
|
start := time.Now()
|
|
|
|
ro := &xrecorder.HandlerRecorderObject{
|
|
Network: "tcp",
|
|
Service: h.options.Service,
|
|
RemoteAddr: conn.RemoteAddr().String(),
|
|
LocalAddr: conn.LocalAddr().String(),
|
|
SID: xctx.SidFromContext(ctx).String(),
|
|
Time: start,
|
|
}
|
|
|
|
if srcAddr := xctx.SrcAddrFromContext(ctx); srcAddr != nil {
|
|
ro.ClientAddr = srcAddr.String()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var dstAddr net.Addr
|
|
|
|
if h.md.tproxy {
|
|
dstAddr = conn.LocalAddr()
|
|
} else {
|
|
dstAddr, err = h.getOriginalDstAddr(conn)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
ro.Host = dstAddr.String()
|
|
ro.DstAddr = dstAddr.String()
|
|
|
|
log = log.WithFields(map[string]any{
|
|
"dst": dstAddr.String(),
|
|
"host": dstAddr.String(),
|
|
})
|
|
|
|
// Check bypass on dstAddr before sniffing — the sniffer only matches the
|
|
// sniffed req.Host, so IP/CIDR rules miss connections carrying a hostname.
|
|
if h.options.Bypass != nil &&
|
|
h.options.Bypass.Contains(ctx, dstAddr.Network(), dstAddr.String(), bypass.WithService(h.options.Service)) {
|
|
log.Debug("bypass: ", dstAddr)
|
|
return xbypass.ErrBypass
|
|
}
|
|
|
|
if h.md.sniffing {
|
|
if h.md.sniffingTimeout > 0 {
|
|
conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout))
|
|
}
|
|
|
|
br := bufio.NewReader(conn)
|
|
proto, _ := sniffing.Sniff(ctx, br)
|
|
ro.Proto = proto
|
|
|
|
if h.md.sniffingTimeout > 0 {
|
|
conn.SetReadDeadline(time.Time{})
|
|
}
|
|
|
|
dial := func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
var cc net.Conn
|
|
var err error
|
|
if address != "" {
|
|
host, _, _ := net.SplitHostPort(address)
|
|
if host == "" {
|
|
host = address
|
|
}
|
|
_, port, _ := net.SplitHostPort(dstAddr.String())
|
|
address = net.JoinHostPort(strings.Trim(host, "[]"), port)
|
|
ro.Host = address
|
|
|
|
var buf bytes.Buffer
|
|
cc, err = h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), "tcp", address)
|
|
ro.Route = buf.String()
|
|
if err != nil && !h.md.sniffingFallback {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if cc == nil {
|
|
if h.options.Bypass != nil && h.options.Bypass.Contains(ctx, "tcp", dstAddr.String(), bypass.WithService(h.options.Service)) {
|
|
return nil, xbypass.ErrBypass
|
|
}
|
|
var buf bytes.Buffer
|
|
cc, err = h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), "tcp", dstAddr.String())
|
|
ro.Route = buf.String()
|
|
ro.Host = dstAddr.String()
|
|
}
|
|
|
|
return cc, err
|
|
}
|
|
|
|
// Dial the original dst instead of the sniffed host; sniffing's
|
|
// hostname bypass and recording still run beforehand.
|
|
if h.md.sniffingDialOriginalDst {
|
|
dial = func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
var buf bytes.Buffer
|
|
cc, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), "tcp", dstAddr.String())
|
|
ro.Route = buf.String()
|
|
return cc, err
|
|
}
|
|
}
|
|
|
|
dialTLS := func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) {
|
|
return dial(ctx, network, address)
|
|
}
|
|
|
|
sniffer := &sniffing.Sniffer{
|
|
Websocket: h.md.sniffingWebsocket,
|
|
WebsocketSampleRate: h.md.sniffingWebsocketSampleRate,
|
|
Recorder: h.recorder.Recorder,
|
|
RecorderOptions: h.recorder.Options,
|
|
Certificate: h.md.certificate,
|
|
PrivateKey: h.md.privateKey,
|
|
NegotiatedProtocol: h.md.alpn,
|
|
CertPool: h.certPool,
|
|
MitmBypass: h.md.mitmBypass,
|
|
ReadTimeout: h.md.readTimeout,
|
|
}
|
|
|
|
conn = xnet.NewReadWriteConn(br, conn, conn)
|
|
switch proto {
|
|
case sniffing.ProtoHTTP:
|
|
ro.Time = time.Time{}
|
|
return sniffer.HandleHTTP(ctx, "tcp", conn,
|
|
sniffing.WithService(h.options.Service),
|
|
sniffing.WithDial(dial),
|
|
sniffing.WithDialTLS(dialTLS),
|
|
sniffing.WithBypass(h.options.Bypass),
|
|
sniffing.WithRecorderObject(ro),
|
|
sniffing.WithLog(log),
|
|
)
|
|
case sniffing.ProtoTLS:
|
|
return sniffer.HandleTLS(ctx, ro.Network, conn,
|
|
sniffing.WithService(h.options.Service),
|
|
sniffing.WithDial(dial),
|
|
sniffing.WithDialTLS(dialTLS),
|
|
sniffing.WithBypass(h.options.Bypass),
|
|
sniffing.WithRecorderObject(ro),
|
|
sniffing.WithLog(log),
|
|
)
|
|
}
|
|
}
|
|
|
|
log.Debugf("%s >> %s", conn.RemoteAddr(), dstAddr)
|
|
|
|
var buf bytes.Buffer
|
|
cc, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), dstAddr.Network(), dstAddr.String())
|
|
ro.Route = buf.String()
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
defer cc.Close()
|
|
|
|
log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
|
ro.SrcAddr = cc.LocalAddr().String()
|
|
ro.DstAddr = cc.RemoteAddr().String()
|
|
|
|
t := time.Now()
|
|
log.Infof("%s <-> %s", conn.RemoteAddr(), dstAddr)
|
|
// xnet.Transport(conn, cc)
|
|
xnet.Pipe(ctx, conn, cc)
|
|
log.WithFields(map[string]any{
|
|
"duration": time.Since(t),
|
|
}).Infof("%s >-< %s", conn.RemoteAddr(), dstAddr)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *redirectHandler) checkRateLimit(addr net.Addr) bool {
|
|
if h.options.RateLimiter == nil {
|
|
return true
|
|
}
|
|
host, _, _ := net.SplitHostPort(addr.String())
|
|
if limiter := h.options.RateLimiter.Limiter(host); limiter != nil {
|
|
return limiter.Allow(1)
|
|
}
|
|
|
|
return true
|
|
}
|