Files
x/handler/relay/connect.go
T
ginuerzh a32ffd5608 fix(sniffing): pass bypass option to sniffer HandleHTTP/HandleTLS calls
When sniffing is enabled, the sniffer extracts the real domain from HTTP
Host header or TLS SNI and checks bypass rules against it. However, most
handlers were not passing WithBypass(...) to the sniffer, so the sniffer
internal bypass check was always skipped.

Additionally, the TLS-to-HTTP fallback paths in internal util sniffer
and forwarder also omitted bypass when constructing options for the
recursive HandleHTTP call after decrypting TLS.

Add WithBypass to all sniffer call sites that were missing it across
handlers (http, relay, socks4, socks5, ss, sshd, unix) and internal
TLS fallback paths.

Also fix import ordering in handler/http/connect.go.

Fixes go-gost/gost#874
2026-06-23 20:54:26 +08:00

312 lines
8.8 KiB
Go

package relay
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"errors"
"io"
"net"
"time"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/limiter"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/observer/stats"
"github.com/go-gost/relay"
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"
serial "github.com/go-gost/x/internal/util/serial"
"github.com/go-gost/x/internal/util/sniffing"
traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper"
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
xrecorder "github.com/go-gost/x/recorder"
)
// handleConnect processes a relay CmdConnect request.
//
// This is the core function of the relay handler: the client connects to a
// target address through the relay.
//
// Data flow:
//
// handleConnect()
// ├─ 1. Clean address (unix/serial special handling)
// ├─ 2. Wrap traffic limiter + stats
// ├─ 3. Check target address is non-empty
// ├─ 4. Bypass check
// ├─ 5. Set consistent-hashing source (e.g. "host")
// ├─ 6. Dial by network type:
// │ ├─ "unix" → net.Dialer.DialContext("unix")
// │ ├─ "serial" → serial.OpenPort()
// │ └─ other → h.options.Router.Dial() ← chain routing
// ├─ 7. Send response header (noDelay controls timing)
// │ ├─ noDelay=true → write relay.Response immediately
// │ └─ noDelay=false → buffer in wbuf, merged with first data packet
// ├─ 8. Wrap connection by network type:
// │ ├─ UDP → udpConn (2-byte length prefix)
// │ └─ TCP → tcpConn (passthrough)
// ├─ 9. Optional protocol sniffing:
// │ ├─ HTTP → sniffer.HandleHTTP()
// │ ├─ TLS → sniffer.HandleTLS()
// │ └─ other → continue to step 10
// └─ 10. xnet.Pipe() bidir copy (client ↔ target)
//
// Key design decisions:
// - noDelay: low-latency mode sends each write as an independent relay frame.
// Without it, the relay response header is merged with the first data packet.
// - Sniffing reads the first few bytes after connection to detect the protocol.
// HTTP/TLS traffic can be MITM-decrypted or recorded.
// - Route info (node chain) is recorded via ictx.ContextWithBuffer into ro.Route.
func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network, address string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) {
if network == "unix" || network == "serial" {
if host, _, _ := net.SplitHostPort(address); host != "" {
address = host
}
}
log = log.WithFields(map[string]any{
"dst": address,
"cmd": "connect",
"host": address,
})
log.Debugf("%s >> %s/%s", conn.RemoteAddr(), address, network)
// --- Traffic limiter + stats wrapper ---
// clientID is used as the key for traffic limiting and stats collection.
{
clientID := xctx.ClientIDFromContext(ctx)
rw := traffic_wrapper.WrapReadWriter(
h.limiter,
conn,
string(clientID),
limiter.ScopeOption(limiter.ScopeClient),
limiter.ServiceOption(h.options.Service),
limiter.NetworkOption(network),
limiter.AddrOption(address),
limiter.ClientOption(string(clientID)),
limiter.SrcOption(conn.RemoteAddr().String()),
)
if h.options.Observer != nil {
pstats := h.stats.Stats(string(clientID))
pstats.Add(stats.KindTotalConns, 1)
pstats.Add(stats.KindCurrentConns, 1)
defer pstats.Add(stats.KindCurrentConns, -1)
rw = stats_wrapper.WrapReadWriter(rw, pstats)
}
conn = xnet.NewReadWriteConn(rw, rw, conn)
}
resp := relay.Response{
Version: relay.Version1,
Status: relay.StatusOK,
}
if address == "" {
resp.Status = relay.StatusBadRequest
resp.WriteTo(conn)
err = errors.New("target not specified")
log.Error(err)
return
}
// Bypass check — if the target is in the bypass list, return Forbidden
if h.options.Bypass != nil && h.options.Bypass.Contains(ctx, network, address, bypass.WithService(h.options.Service)) {
log.Debug("bypass: ", address)
resp.Status = relay.StatusForbidden
resp.WriteTo(conn)
return xbypass.ErrBypass
}
// Consistent hashing — used for sticky sessions across upstream nodes
switch h.md.hash {
case "host":
ctx = xctx.ContextWithHash(ctx, &xctx.Hash{Source: address})
}
// --- Dial to target ---
var cc net.Conn
switch network {
case "unix":
cc, err = (&net.Dialer{}).DialContext(ctx, "unix", address)
case "serial":
var port io.ReadWriteCloser
port, err = serial.OpenPort(serial.ParseConfigFromAddr(address))
if err == nil {
cc = &serialConn{
ReadWriteCloser: port,
port: address,
}
}
default:
var buf bytes.Buffer
cc, err = h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), network, address)
ro.Route = buf.String()
}
if err != nil {
resp.Status = relay.StatusNetworkUnreachable
resp.WriteTo(conn)
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()
// --- Send relay response header ---
if h.md.noDelay {
if _, err := resp.WriteTo(conn); err != nil {
log.Error(err)
return err
}
}
// --- Wrap connection by network type ---
// UDP connections use a 2-byte length prefix for datagram framing.
// TCP connections pass data through directly.
switch network {
case "udp", "udp4", "udp6":
rc := &udpConn{
Conn: conn,
}
if !h.md.noDelay {
// Buffer the response header, merged with the first data packet.
if _, err := resp.WriteTo(&rc.wbuf); err != nil {
return err
}
}
conn = rc
default:
if !h.md.noDelay {
rc := &tcpConn{
Conn: conn,
}
// Buffer the response header, merged with the first data packet.
if _, err := resp.WriteTo(&rc.wbuf); err != nil {
return err
}
conn = rc
}
}
// --- Optional protocol sniffing ---
// After connection establishment, sniff the first bytes of client traffic
// to detect HTTP or TLS. When detected, MITM decryption can be applied.
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) {
return cc, nil
}
dialTLS := func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) {
return cc, nil
}
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:
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, "tcp", conn,
sniffing.WithService(h.options.Service),
sniffing.WithDial(dial),
sniffing.WithDialTLS(dialTLS),
sniffing.WithBypass(h.options.Bypass),
sniffing.WithRecorderObject(ro),
sniffing.WithLog(log),
)
}
}
// --- Bidirectional data copy ---
t := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), address)
xnet.Pipe(ctx, conn, cc)
log.WithFields(map[string]any{
"duration": time.Since(t),
}).Infof("%s >-< %s", conn.RemoteAddr(), address)
return nil
}
// serialConn wraps a serial port connection as net.Conn.
// Serial ports have no network addresses, so LocalAddr/RemoteAddr return
// custom serialAddr values. Deadline methods are no-ops since serial
// ports do not support deadlines.
type serialConn struct {
io.ReadWriteCloser
port string
}
func (c *serialConn) LocalAddr() net.Addr {
return &serialAddr{
port: "@",
}
}
func (c *serialConn) RemoteAddr() net.Addr {
return &serialAddr{
port: c.port,
}
}
func (c *serialConn) SetDeadline(t time.Time) error {
return nil
}
func (c *serialConn) SetReadDeadline(t time.Time) error {
return nil
}
func (c *serialConn) SetWriteDeadline(t time.Time) error {
return nil
}
// serialAddr is the address type for a serial port connection.
type serialAddr struct {
port string
}
func (a *serialAddr) Network() string {
return "serial"
}
func (a *serialAddr) String() string {
return a.port
}