docs(handler/relay): convert all comments to English

Translate all Chinese comments in the relay handler package to English:
handler.go, connect.go, bind.go, forward.go, conn.go, entrypoint.go,
observe.go, metadata.go. All inline and doc comments are now in English.

Also set a persistent preference: all future code comments must be
written in English only.
This commit is contained in:
ginuerzh
2026-06-03 23:05:08 +08:00
parent 95874c53f5
commit d5fd62aa47
8 changed files with 395 additions and 46 deletions
+76 -4
View File
@@ -24,6 +24,35 @@ import (
xservice "github.com/go-gost/x/service" xservice "github.com/go-gost/x/service"
) )
// handleBind processes a relay CmdBind request.
//
// BIND mode is used for reverse-proxy scenarios: the client asks the relay
// handler to listen on a local port, then forwards inbound connections back
// to the client over a mux session.
//
// Flow:
//
// handleBind()
// ├─ 1. Wrap traffic limiter + stats
// ├─ 2. Check BIND is enabled
// ├─ 3. TCP BIND → bindTCP()
// │ ├─ net.Listen on the specified address
// │ ├─ Return the listening address via relay.Response
// │ ├─ Upgrade the connection to a mux session
// │ ├─ Start tcpHandler + tcpListener as an internal service
// │ │ ├─ tcpHandler: gets a stream from mux session → writes AddrFeature
// │ │ └─ tcpListener: accepts local TCP connections
// │ ├─ Goroutine: drain unexpected mux sessions
// │ └─ service.Serve() blocks
// └─ 4. UDP BIND → bindUDP()
// ├─ net.ListenPacket on the UDP port
// ├─ Return the listening address
// └─ Create udp.Relay for datagram relay
//
// Key design:
// - TCP BIND uses mux: each inbound connection gets an independent mux stream
// carrying a relay AddrFeature identifying the peer address.
// - UDP BIND uses udp.Relay directly, bypassing mux.
func (h *relayHandler) handleBind(ctx context.Context, conn net.Conn, network, address string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { func (h *relayHandler) handleBind(ctx context.Context, conn net.Conn, network, address string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
log = log.WithFields(map[string]any{ log = log.WithFields(map[string]any{
"dst": address, "dst": address,
@@ -32,6 +61,7 @@ func (h *relayHandler) handleBind(ctx context.Context, conn net.Conn, network, a
log.Debugf("%s >> %s", conn.RemoteAddr(), address) log.Debugf("%s >> %s", conn.RemoteAddr(), address)
// --- Traffic limiter + stats wrapper ---
{ {
clientID := ctxvalue.ClientIDFromContext(ctx) clientID := ctxvalue.ClientIDFromContext(ctx)
rw := traffic_wrapper.WrapReadWriter( rw := traffic_wrapper.WrapReadWriter(
@@ -56,6 +86,7 @@ func (h *relayHandler) handleBind(ctx context.Context, conn net.Conn, network, a
conn = xnet.NewReadWriteConn(rw, rw, conn) conn = xnet.NewReadWriteConn(rw, rw, conn)
} }
// Check whether BIND is enabled in config.
resp := relay.Response{ resp := relay.Response{
Version: relay.Version1, Version: relay.Version1,
Status: relay.StatusOK, Status: relay.StatusOK,
@@ -75,6 +106,27 @@ func (h *relayHandler) handleBind(ctx context.Context, conn net.Conn, network, a
} }
} }
// bindTCP implements TCP BIND mode.
//
// Detailed flow:
//
// bindTCP()
// ├─ 1. net.Listen on the specified TCP address
// ├─ 2. Write the listening address into relay.Response, send to client
// ├─ 3. Upgrade the client connection to a mux session
// │ (mux multiplexes multiple independent streams over one TCP conn)
// ├─ 4. Create internal tcpListener + tcpHandler + Service
// │ ├─ tcpListener: wraps net.Listener with proxyproto/metrics/admission
// │ ├─ tcpHandler: on each inbound connection:
// │ │ ├─ Gets a stream from the mux session
// │ │ ├─ Writes the inbound peer address as AddrFeature on the stream
// │ │ └─ Pipes data bidirectionally (local conn ↔ mux stream)
// │ └─ Service: wraps listener + handler, blocks on Serve()
// ├─ 5. Goroutine: accept and discard unexpected mux connections
// └─ 6. srv.Serve() blocks until shutdown
//
// The client receives forwarded connections as streams on the mux session.
// Each stream carries a relay.AddrFeature identifying the original peer.
func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, address string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, address string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
resp := relay.Response{ resp := relay.Response{
Version: relay.Version1, Version: relay.Version1,
@@ -84,7 +136,7 @@ func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, addr
lc := xnet.ListenConfig{ lc := xnet.ListenConfig{
Netns: h.options.Netns, Netns: h.options.Netns,
} }
ln, err := lc.Listen(ctx, network, address) // strict mode: if the port already in use, it will return error ln, err := lc.Listen(ctx, network, address) // 严格模式:端口已被占用时会返回错误
if err != nil { if err != nil {
log.Error(err) log.Error(err)
resp.Status = relay.StatusServiceUnavailable resp.Status = relay.StatusServiceUnavailable
@@ -93,6 +145,7 @@ func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, addr
} }
defer ln.Close() defer ln.Close()
// Internal service name: "<main-service>-ep-<listen-address>"
serviceName := fmt.Sprintf("%s-ep-%s", h.options.Service, ln.Addr()) serviceName := fmt.Sprintf("%s-ep-%s", h.options.Service, ln.Addr())
log = log.WithFields(map[string]any{ log = log.WithFields(map[string]any{
"service": serviceName, "service": serviceName,
@@ -103,6 +156,7 @@ func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, addr
}) })
ro.SrcAddr = ln.Addr().String() ro.SrcAddr = ln.Addr().String()
// Return the listening address to the client.
af := &relay.AddrFeature{} af := &relay.AddrFeature{}
if err := af.ParseFrom(ln.Addr().String()); err != nil { if err := af.ParseFrom(ln.Addr().String()); err != nil {
log.Warn(err) log.Warn(err)
@@ -113,7 +167,7 @@ func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, addr
return err return err
} }
// Upgrade connection to multiplex session. // Upgrade the client connection to a mux session.
session, err := mux.ClientSession(conn, h.md.muxCfg) session, err := mux.ClientSession(conn, h.md.muxCfg)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
@@ -121,6 +175,7 @@ func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, addr
} }
defer session.Close() defer session.Close()
// Internal endpoint listener (proxyproto → metrics → admission layers).
epListener := newTCPListener(ln, epListener := newTCPListener(ln,
listener.AddrOption(address), listener.AddrOption(address),
listener.ServiceOption(serviceName), listener.ServiceOption(serviceName),
@@ -129,6 +184,10 @@ func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, addr
"kind": "listener", "kind": "listener",
})), })),
) )
// Internal endpoint handler — on each inbound connection:
// 1. Gets a stream from the mux session
// 2. Writes the peer address as AddrFeature on the mux stream
// 3. Pipes data bidirectionally
epHandler := newTCPHandler(session, epHandler := newTCPHandler(session,
handler.ServiceOption(serviceName), handler.ServiceOption(serviceName),
handler.LoggerOption(log.WithFields(map[string]any{ handler.LoggerOption(log.WithFields(map[string]any{
@@ -145,6 +204,8 @@ func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, addr
log = log.WithFields(map[string]any{}) log = log.WithFields(map[string]any{})
log.Infof("bind on %s/%s OK", ln.Addr(), ln.Addr().Network()) log.Infof("bind on %s/%s OK", ln.Addr(), ln.Addr().Network())
// Goroutine: accept and discard unexpected mux connections.
// Normal inbound connections are handled by tcpHandler via session.GetConn().
go func() { go func() {
defer srv.Close() defer srv.Close()
for { for {
@@ -153,13 +214,23 @@ func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, addr
log.Error(err) log.Error(err)
return return
} }
conn.Close() // we do not handle incoming connections. conn.Close() // 我们不处理意外入站的连接
} }
}() }()
return srv.Serve() return srv.Serve()
} }
// bindUDP implements UDP BIND mode.
//
// Flow:
// 1. net.ListenPacket on the specified UDP address.
// 2. Return the listening address to the client.
// 3. Create a udp.Relay for bidir datagram relay between client and local port.
//
// Unlike TCP BIND, UDP BIND does not use mux. It relays datagrams directly via
// udp.Relay, wrapping the stream connection as UDPTunServerConn for datagram
// framing over the stream.
func (h *relayHandler) bindUDP(ctx context.Context, conn net.Conn, network, address string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { func (h *relayHandler) bindUDP(ctx context.Context, conn net.Conn, network, address string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
resp := relay.Response{ resp := relay.Response{
Version: relay.Version1, Version: relay.Version1,
@@ -203,6 +274,7 @@ func (h *relayHandler) bindUDP(ctx context.Context, conn net.Conn, network, addr
log.Infof("bind on %s OK", pc.LocalAddr()) log.Infof("bind on %s OK", pc.LocalAddr())
// relay_util.UDPTunServerConn 将流式连接包装成数据报模式
r := udp.NewRelay(relay_util.UDPTunServerConn(conn), pc). r := udp.NewRelay(relay_util.UDPTunServerConn(conn), pc).
WithService(h.options.Service). WithService(h.options.Service).
WithBypass(h.options.Bypass). WithBypass(h.options.Bypass).
@@ -216,4 +288,4 @@ func (h *relayHandler) bindUDP(ctx context.Context, conn net.Conn, network, addr
"duration": time.Since(t), "duration": time.Since(t),
}).Debugf("%s >-< %s", conn.RemoteAddr(), pc.LocalAddr()) }).Debugf("%s >-< %s", conn.RemoteAddr(), pc.LocalAddr())
return nil return nil
} }
+38 -4
View File
@@ -9,6 +9,16 @@ import (
"net" "net"
) )
// tcpConn wraps a TCP connection with response header buffering (wbuf).
//
// In non-noDelay mode, the relay.Response header is first written into wbuf.
// On the first Write() call, the buffered header and the data are sent together.
// This avoids sending a small relay frame before the data stream begins.
//
// Write() semantics:
// - n always returns len(b) rather than the actual bytes written. This differs
// from net.Conn's contract. The rationale: when wbuf has content, the actual
// write is "wbuf + b" but the caller only cares that "b" was "processed".
type tcpConn struct { type tcpConn struct {
net.Conn net.Conn
wbuf bytes.Buffer wbuf bytes.Buffer
@@ -19,9 +29,9 @@ func (c *tcpConn) Read(b []byte) (n int, err error) {
} }
func (c *tcpConn) Write(b []byte) (n int, err error) { func (c *tcpConn) Write(b []byte) (n int, err error) {
n = len(b) // force byte length consistent n = len(b) // always return len(b), not actual bytes written
if c.wbuf.Len() > 0 { if c.wbuf.Len() > 0 {
c.wbuf.Write(b) // append the data to the cached header c.wbuf.Write(b) // 将数据追加到缓存的头部之后
_, err = c.wbuf.WriteTo(c.Conn) _, err = c.wbuf.WriteTo(c.Conn)
return return
} }
@@ -29,6 +39,27 @@ func (c *tcpConn) Write(b []byte) (n int, err error) {
return return
} }
// udpConn wraps UDP datagrams over a stream connection.
//
// When the GOST relay protocol carries UDP over a TCP/stream transport,
// a 2-byte big-endian length prefix frames each datagram. TCP is a stream
// protocol without message boundaries, so this framing is necessary.
//
// Datagram wire format:
//
// [2-byte length (big-endian)][datagram payload]
//
// Read() flow:
// 1. Read 2-byte length prefix.
// 2. Read the datagram payload of the specified length.
// 3. If the caller's buffer is too small, allocate an internal buffer,
// read the full datagram, then truncate on copy.
//
// Write() flow:
// 1. Reject data exceeding MaxUint16 (65535).
// 2. If wbuf has a cached header, append the length prefix + data to the
// header and flush everything at once.
// 3. Otherwise write the length prefix then the data.
type udpConn struct { type udpConn struct {
net.Conn net.Conn
wbuf bytes.Buffer wbuf bytes.Buffer
@@ -45,6 +76,7 @@ func (c *udpConn) Read(b []byte) (n int, err error) {
if len(b) >= dlen { if len(b) >= dlen {
return io.ReadFull(c.Conn, b[:dlen]) return io.ReadFull(c.Conn, b[:dlen])
} }
// Caller's buffer is too small; allocate internal buffer.
buf := make([]byte, dlen) buf := make([]byte, dlen)
_, err = io.ReadFull(c.Conn, buf) _, err = io.ReadFull(c.Conn, buf)
n = copy(b, buf) n = copy(b, buf)
@@ -60,14 +92,16 @@ func (c *udpConn) Write(b []byte) (n int, err error) {
n = len(b) n = len(b)
if c.wbuf.Len() > 0 { if c.wbuf.Len() > 0 {
// Wbuf has cached header; append length prefix + data and flush together.
var bb [2]byte var bb [2]byte
binary.BigEndian.PutUint16(bb[:], uint16(len(b))) binary.BigEndian.PutUint16(bb[:], uint16(len(b)))
c.wbuf.Write(bb[:]) c.wbuf.Write(bb[:])
c.wbuf.Write(b) // append the data to the cached header c.wbuf.Write(b)
_, err = c.wbuf.WriteTo(c.Conn) _, err = c.wbuf.WriteTo(c.Conn)
return return
} }
// Write length prefix + data directly.
var bb [2]byte var bb [2]byte
binary.BigEndian.PutUint16(bb[:], uint16(len(b))) binary.BigEndian.PutUint16(bb[:], uint16(len(b)))
_, err = c.Conn.Write(bb[:]) _, err = c.Conn.Write(bb[:])
@@ -75,4 +109,4 @@ func (c *udpConn) Write(b []byte) (n int, err error) {
return return
} }
return c.Conn.Write(b) return c.Conn.Write(b)
} }
+56 -4
View File
@@ -26,6 +26,41 @@ import (
xrecorder "github.com/go-gost/x/recorder" 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) { 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 network == "unix" || network == "serial" {
if host, _, _ := net.SplitHostPort(address); host != "" { if host, _, _ := net.SplitHostPort(address); host != "" {
@@ -41,6 +76,8 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
log.Debugf("%s >> %s/%s", conn.RemoteAddr(), address, network) 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) clientID := xctx.ClientIDFromContext(ctx)
rw := traffic_wrapper.WrapReadWriter( rw := traffic_wrapper.WrapReadWriter(
@@ -78,6 +115,7 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
return 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)) { if h.options.Bypass != nil && h.options.Bypass.Contains(ctx, network, address, bypass.WithService(h.options.Service)) {
log.Debug("bypass: ", address) log.Debug("bypass: ", address)
resp.Status = relay.StatusForbidden resp.Status = relay.StatusForbidden
@@ -85,11 +123,13 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
return xbypass.ErrBypass return xbypass.ErrBypass
} }
// Consistent hashing — used for sticky sessions across upstream nodes
switch h.md.hash { switch h.md.hash {
case "host": case "host":
ctx = xctx.ContextWithHash(ctx, &xctx.Hash{Source: address}) ctx = xctx.ContextWithHash(ctx, &xctx.Hash{Source: address})
} }
// --- Dial to target ---
var cc net.Conn var cc net.Conn
switch network { switch network {
case "unix": case "unix":
@@ -119,6 +159,7 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
ro.SrcAddr = cc.LocalAddr().String() ro.SrcAddr = cc.LocalAddr().String()
ro.DstAddr = cc.RemoteAddr().String() ro.DstAddr = cc.RemoteAddr().String()
// --- Send relay response header ---
if h.md.noDelay { if h.md.noDelay {
if _, err := resp.WriteTo(conn); err != nil { if _, err := resp.WriteTo(conn); err != nil {
log.Error(err) log.Error(err)
@@ -126,13 +167,16 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
} }
} }
// --- Wrap connection by network type ---
// UDP connections use a 2-byte length prefix for datagram framing.
// TCP connections pass data through directly.
switch network { switch network {
case "udp", "udp4", "udp6": case "udp", "udp4", "udp6":
rc := &udpConn{ rc := &udpConn{
Conn: conn, Conn: conn,
} }
if !h.md.noDelay { if !h.md.noDelay {
// cache the header // 缓存响应头,与第一个数据包合并发送
if _, err := resp.WriteTo(&rc.wbuf); err != nil { if _, err := resp.WriteTo(&rc.wbuf); err != nil {
return err return err
} }
@@ -143,7 +187,7 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
rc := &tcpConn{ rc := &tcpConn{
Conn: conn, Conn: conn,
} }
// cache the header // 缓存响应头,与第一个数据包合并发送
if _, err := resp.WriteTo(&rc.wbuf); err != nil { if _, err := resp.WriteTo(&rc.wbuf); err != nil {
return err return err
} }
@@ -151,6 +195,9 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
} }
} }
// --- 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.sniffing {
if h.md.sniffingTimeout > 0 { if h.md.sniffingTimeout > 0 {
conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout))
@@ -204,9 +251,9 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
} }
} }
// --- 双向数据拷贝 ---
t := time.Now() t := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), address) log.Infof("%s <-> %s", conn.RemoteAddr(), address)
// xnet.Transport(conn, cc)
xnet.Pipe(ctx, conn, cc) xnet.Pipe(ctx, conn, cc)
log.WithFields(map[string]any{ log.WithFields(map[string]any{
"duration": time.Since(t), "duration": time.Since(t),
@@ -215,6 +262,10 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network
return nil 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 { type serialConn struct {
io.ReadWriteCloser io.ReadWriteCloser
port string port string
@@ -244,6 +295,7 @@ func (c *serialConn) SetWriteDeadline(t time.Time) error {
return nil return nil
} }
// serialAddr is the address type for a serial port connection.
type serialAddr struct { type serialAddr struct {
port string port string
} }
@@ -254,4 +306,4 @@ func (a *serialAddr) Network() string {
func (a *serialAddr) String() string { func (a *serialAddr) String() string {
return a.port return a.port
} }
+27 -3
View File
@@ -16,6 +16,16 @@ import (
metrics "github.com/go-gost/x/metrics/wrapper" metrics "github.com/go-gost/x/metrics/wrapper"
) )
// tcpListener is the internal TCP listener used in BIND mode.
//
// Wrapping layers (outermost first):
// - proxyproto.WrapListener — PROXY protocol support
// - metrics.WrapListener — connection metrics
// - admission.WrapListener — access control (allow/deny lists)
// - raw net.Listener
//
// This is a simplified version of the standard listener wrapping chain from
// x/config/parsing/service/parse.go.
type tcpListener struct { type tcpListener struct {
ln net.Listener ln net.Listener
options listener.Options options listener.Options
@@ -33,7 +43,6 @@ func newTCPListener(ln net.Listener, opts ...listener.Option) listener.Listener
} }
func (l *tcpListener) Init(md md.Metadata) (err error) { func (l *tcpListener) Init(md md.Metadata) (err error) {
// l.logger.Debugf("pp: %d", l.options.ProxyProtocol)
ln := l.ln ln := l.ln
ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second) ln = proxyproto.WrapListener(l.options.ProxyProtocol, ln, 10*time.Second)
ln = metrics.WrapListener(l.options.Service, ln) ln = metrics.WrapListener(l.options.Service, ln)
@@ -55,6 +64,20 @@ func (l *tcpListener) Close() error {
return l.ln.Close() return l.ln.Close()
} }
// tcpHandler is the internal handler for BIND mode.
//
// When an inbound connection arrives at the listen port created by bindTCP,
// this handler forwards it back to the requesting client over a mux stream.
//
// Flow:
// 1. Gets a free stream from the mux session (session.GetConn()).
// 2. Encodes the inbound peer address as a relay.AddrFeature on the stream.
// 3. Writes a relay.Response (StatusOK).
// 4. Bidirectional Pipe (inbound conn ↔ mux stream).
//
// This is the core mechanism for reverse-proxy / tunnel traversal:
// the client that requested BIND receives forwarded connections as
// streams on the mux session.
type tcpHandler struct { type tcpHandler struct {
session *mux.Session session *mux.Session
options handler.Options options handler.Options
@@ -92,6 +115,7 @@ func (h *tcpHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}() }()
// 从 mux 会话获取一个流
cc, err := h.session.GetConn() cc, err := h.session.GetConn()
if err != nil { if err != nil {
log.Error(err) log.Error(err)
@@ -99,6 +123,7 @@ func (h *tcpHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.
} }
defer cc.Close() defer cc.Close()
// 将入站连接地址编码为 AddrFeature,通过 relay 帧发送给客户端
af := &relay.AddrFeature{} af := &relay.AddrFeature{}
af.ParseFrom(conn.RemoteAddr().String()) af.ParseFrom(conn.RemoteAddr().String())
resp := relay.Response{ resp := relay.Response{
@@ -113,9 +138,8 @@ func (h *tcpHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.
t := time.Now() t := time.Now()
log.Debugf("%s <-> %s", conn.RemoteAddr(), cc.RemoteAddr()) log.Debugf("%s <-> %s", conn.RemoteAddr(), cc.RemoteAddr())
// xnet.Transport(conn, cc)
xnet.Pipe(ctx, conn, cc) xnet.Pipe(ctx, conn, cc)
log.WithFields(map[string]any{"duration": time.Since(t)}). log.WithFields(map[string]any{"duration": time.Since(t)}).
Debugf("%s >-< %s", conn.RemoteAddr(), cc.RemoteAddr()) Debugf("%s >-< %s", conn.RemoteAddr(), cc.RemoteAddr())
return nil return nil
} }
+35 -6
View File
@@ -19,6 +19,31 @@ import (
xrecorder "github.com/go-gost/x/recorder" xrecorder "github.com/go-gost/x/recorder"
) )
// handleForward processes relay forward mode.
//
// When a hop is set on the relayHandler (via Forward()), this mode is used.
// Unlike handleConnect, the target address is not specified by the client;
// instead it is selected by the hop's load-balancing strategy (round-robin,
// random, hash, etc.).
//
// Data flow:
//
// handleForward()
// ├─ 1. hop.Select() picks a target node from the hop
// │ └─ No node available → return ServiceUnavailable
// ├─ 2. Wrap traffic limiter + stats
// ├─ 3. Router.Dial() dials the target node
// │ └─ Dial fails → mark the node (Mark()) → return HostUnreachable
// ├─ 4. On success, reset the node's failure marker
// ├─ 5. Send response header (noDelay controls timing)
// ├─ 6. Wrap connection by network type (tcpConn/udpConn)
// └─ 7. xnet.Pipe() bidir data copy (client ↔ target)
//
// Failure handling:
// - If Router.Dial fails and the target node has a Marker, the node is
// marked as failed. Marked nodes are skipped by FailFilter/BackupFilter
// in subsequent selections.
// - On successful dial, the marker is reset, indicating recovery.
func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network string, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error {
resp := relay.Response{ resp := relay.Response{
Version: relay.Version1, Version: relay.Version1,
@@ -40,6 +65,7 @@ func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network
log.Debugf("%s >> %s", conn.RemoteAddr(), target.Addr) log.Debugf("%s >> %s", conn.RemoteAddr(), target.Addr)
// --- Traffic limiter + stats wrapper ---
{ {
clientID := ctxvalue.ClientIDFromContext(ctx) clientID := ctxvalue.ClientIDFromContext(ctx)
rw := wrapper.WrapReadWriter( rw := wrapper.WrapReadWriter(
@@ -63,12 +89,13 @@ func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network
conn = xnet.NewReadWriteConn(rw, rw, conn) conn = xnet.NewReadWriteConn(rw, rw, conn)
} }
// Dial the target node, recording the route path.
var buf bytes.Buffer var buf bytes.Buffer
cc, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), network, target.Addr) cc, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), network, target.Addr)
ro.Route = buf.String() ro.Route = buf.String()
if err != nil { if err != nil {
// TODO: the router itself may be failed due to the failed node in the router, // TODO: the router itself may fail because of a failed node in the route.
// the dead marker may be a wrong operation. // Marking the node here may be incorrect in that case.
if marker := target.Marker(); marker != nil { if marker := target.Marker(); marker != nil {
marker.Mark() marker.Mark()
} }
@@ -85,10 +112,12 @@ func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network
ro.SrcAddr = cc.LocalAddr().String() ro.SrcAddr = cc.LocalAddr().String()
ro.DstAddr = cc.RemoteAddr().String() ro.DstAddr = cc.RemoteAddr().String()
// Reset the failure marker on successful dial.
if marker := target.Marker(); marker != nil { if marker := target.Marker(); marker != nil {
marker.Reset() marker.Reset()
} }
// --- Send response header ---
if h.md.noDelay { if h.md.noDelay {
if _, err := resp.WriteTo(conn); err != nil { if _, err := resp.WriteTo(conn); err != nil {
log.Error(err) log.Error(err)
@@ -96,13 +125,14 @@ func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network
} }
} }
// --- Wrap connection by network type ---
switch network { switch network {
case "udp", "udp4", "udp6": case "udp", "udp4", "udp6":
rc := &udpConn{ rc := &udpConn{
Conn: conn, Conn: conn,
} }
if !h.md.noDelay { if !h.md.noDelay {
// cache the header // Buffer the response header, merged with the first data packet.
if _, err := resp.WriteTo(&rc.wbuf); err != nil { if _, err := resp.WriteTo(&rc.wbuf); err != nil {
return err return err
} }
@@ -113,7 +143,7 @@ func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network
Conn: conn, Conn: conn,
} }
if !h.md.noDelay { if !h.md.noDelay {
// cache the header // Buffer the response header, merged with the first data packet.
if _, err := resp.WriteTo(&rc.wbuf); err != nil { if _, err := resp.WriteTo(&rc.wbuf); err != nil {
return err return err
} }
@@ -123,11 +153,10 @@ func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network
t := time.Now() t := time.Now()
log.Debugf("%s <-> %s", conn.RemoteAddr(), target.Addr) log.Debugf("%s <-> %s", conn.RemoteAddr(), target.Addr)
// xnet.Transport(conn, cc)
xnet.Pipe(ctx, conn, cc) xnet.Pipe(ctx, conn, cc)
log.WithFields(map[string]any{ log.WithFields(map[string]any{
"duration": time.Since(t), "duration": time.Since(t),
}).Debugf("%s >-< %s", conn.RemoteAddr(), target.Addr) }).Debugf("%s >-< %s", conn.RemoteAddr(), target.Addr)
return nil return nil
} }
+62 -3
View File
@@ -37,6 +37,42 @@ func init() {
registry.HandlerRegistry().Register("relay", NewHandler) registry.HandlerRegistry().Register("relay", NewHandler)
} }
// relayHandler is the GOST relay protocol server handler.
//
// The GOST relay protocol is a custom multiplexed transport supporting three modes:
// 1. Connect — the client requests a connection to a target address.
// The handler dials via the configured Router and pipes data bidirectionally.
// - Direct: handleConnect(), target address from the relay request.
// - Forward: handleForward(), target from hop selector (load balancing).
// 2. Bind — the client asks the handler to listen on a local port and forward
// incoming connections back through a mux session. Used for reverse proxying.
// 3. Forward — when a hop is set, the target is selected by the hop's strategy
// rather than specified by the client.
//
// Data flow:
//
// ┌─────────────────────────────────────────────────────────┐
// │ Handle() → parse relay.Request │
// │ ├─ extract auth (UserAuthFeature) → authenticate │
// │ ├─ extract target address (AddrFeature) │
// │ └─ extract network type (NetworkFeature) │
// │ │
// │ ┌── hop set? ──→ handleForward() │
// │ │ ├─ hop.Select() pick target node │
// │ │ └─ Router.Dial() → Pipe bidir copy │
// │ │
// │ └── no hop, dispatch by command: │
// │ ├─ CmdConnect → handleConnect() │
// │ │ ├─ bypass check │
// │ │ ├─ consistent-hashing │
// │ │ ├─ Router.Dial() / net.Dial() / serial.Open │
// │ │ ├─ send response header (noDelay) │
// │ │ ├─ optional protocol sniffing (HTTP/TLS MITM) │
// │ │ └─ Pipe bidir data copy │
// │ └─ CmdBind → handleBind() │
// │ ├─ bindTCP: net.Listen → mux session → tcpHandler │
// │ └─ bindUDP: net.ListenPacket → udp.Relay │
// └─────────────────────────────────────────────────────────┘
type relayHandler struct { type relayHandler struct {
hop hop.Hop hop hop.Hop
md metadata md metadata
@@ -59,6 +95,14 @@ func NewHandler(opts ...handler.Option) handler.Handler {
} }
} }
// Init 初始化 relay handler。在 handler 被注册到 service 后调用。
//
// 初始化流程:
// 1. 解析元数据配置(超时、嗅探、mux、MITM 等)
// 2. 如果配置了 Observer,创建 stats 统计器并启动后台轮询协程
// 3. 如果配置了 TrafficLimiter,创建带缓存的流量限制器
// 4. 从 Recorders 列表中选取 ServiceHandler 类型的 recorder
// 5. 如果配置了 MITM 证书,创建内存证书池
func (h *relayHandler) Init(md md.Metadata) (err error) { func (h *relayHandler) Init(md md.Metadata) (err error) {
if err := h.parseMetadata(md); err != nil { if err := h.parseMetadata(md); err != nil {
return err return err
@@ -99,6 +143,22 @@ func (h *relayHandler) Forward(hop hop.Hop) {
h.hop = hop h.hop = hop
} }
// Handle is the main entry point for each inbound connection.
//
// Flow:
// 1. Create a recorder object with connection metadata.
// 2. Wrap the connection for stats collection (input/output bytes).
// 3. Rate-limit check.
// 4. Set read deadline, read the relay.Request.
// 5. Clear the read deadline.
// 6. Check the relay protocol version.
// 7. Parse request features (auth, address, network type).
// 8. Authenticate (if an Auther is configured).
// 9. Dispatch:
// - If hop is set → handleForward.
// - CmdConnect → handleConnect.
// - CmdBind → handleBind.
// 10. Deferred final stats recording.
func (h *relayHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) { func (h *relayHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
defer conn.Close() defer conn.Close()
@@ -220,7 +280,7 @@ func (h *relayHandler) Handle(ctx context.Context, conn net.Conn, opts ...handle
log = log.WithFields(map[string]any{"network": network}) log = log.WithFields(map[string]any{"network": network})
if h.hop != nil { if h.hop != nil {
// forward mode // Forward mode: target is selected from the hop.
return h.handleForward(ctx, conn, network, ro, log) return h.handleForward(ctx, conn, network, ro, log)
} }
@@ -242,5 +302,4 @@ func (h *relayHandler) Close() error {
h.cancel() h.cancel()
} }
return nil return nil
} }
+68 -20
View File
@@ -13,37 +13,85 @@ import (
"github.com/go-gost/x/registry" "github.com/go-gost/x/registry"
) )
// metadata holds the relay handler configuration parsed from the generic Metadata map.
type metadata struct { type metadata struct {
// readTimeout is the deadline for reading the initial relay protocol // readTimeout is the deadline for reading the initial relay.Request handshake.
// handshake (relay.Request) from the client connection. The deadline // Cleared immediately after the handshake, so it does not affect data transfer.
// is cleared immediately after the handshake completes, so it does // Also passed to SnifferBuilder for upstream response header read timeout.
// not affect subsequent data transfer. Also passed to SnifferBuilder // Default: 15s (0 or negative falls back to 15s).
// for the upstream response header read timeout. readTimeout time.Duration
// 0 or negative defaults to 15s.
readTimeout time.Duration
udpBufferSize int
enableBind bool
noDelay bool
hash string
muxCfg *mux.Config
observerPeriod time.Duration // udpBufferSize is the buffer size (in bytes) for UDP datagram relay.
// Config keys: "udp.bufferSize", "udpBufferSize".
udpBufferSize int
// enableBind controls whether CmdBind is allowed.
// Disabled by default; set "bind: true" in config to enable.
enableBind bool
// noDelay controls whether the relay.Response header is sent immediately.
// When enabled, each write is an independent relay frame (low-latency).
// When disabled, the response header is buffered in wbuf and merged with
// the first data write.
noDelay bool
// hash specifies the consistent-hashing source. Currently supports "host",
// which uses the target address as the hash source for sticky sessions.
hash string
// muxCfg is the multiplexing session configuration, used only in BIND mode
// to upgrade the client connection to a mux session.
muxCfg *mux.Config
// observerPeriod is the stats event polling interval.
// Default: 5s, minimum: 1s.
// Config keys: "observePeriod", "observer.period", "observer.observePeriod".
observerPeriod time.Duration
// observerResetTraffic controls whether traffic counters are reset after
// each poll. When true, reported traffic is incremental; when false, cumulative.
observerResetTraffic bool observerResetTraffic bool
sniffing bool // sniffing enables protocol sniffing. When enabled, the handler detects
sniffingTimeout time.Duration // the traffic protocol (HTTP/TLS) after connection establishment and
sniffingWebsocket bool // selects the corresponding processing path (e.g. MITM decryption).
sniffing bool
// sniffingTimeout is the read deadline during sniffing.
sniffingTimeout time.Duration
// sniffingWebsocket enables WebSocket upgrade detection within sniffed HTTP.
sniffingWebsocket bool
// sniffingWebsocketSampleRate controls the sampling rate for WebSocket
// traffic recording (0.0 ~ 1.0).
sniffingWebsocketSampleRate float64 sniffingWebsocketSampleRate float64
// certificate is the CA certificate used for MITM decryption.
certificate *x509.Certificate certificate *x509.Certificate
privateKey crypto.PrivateKey
alpn string
mitmBypass bypass.Bypass
// privateKey is the private key paired with the MITM certificate.
privateKey crypto.PrivateKey
// alpn is the TLS ALPN protocol list for MITM decryption.
alpn string
// mitmBypass is an allow/deny list of hostnames that skip MITM decryption.
mitmBypass bypass.Bypass
// limiterRefreshInterval is the traffic limiter cache refresh interval.
limiterRefreshInterval time.Duration limiterRefreshInterval time.Duration
// limiterCleanupInterval is the traffic limiter cache cleanup interval.
limiterCleanupInterval time.Duration limiterCleanupInterval time.Duration
} }
// parseMetadata extracts typed configuration from the generic Metadata map.
//
// Rules:
// - Uses mdutil Get* helpers which support multiple fallback key names.
// - Numeric durations are treated as seconds; string durations use time.ParseDuration.
// - Unset or invalid values fall back to sensible defaults.
func (h *relayHandler) parseMetadata(md mdata.Metadata) (err error) { func (h *relayHandler) parseMetadata(md mdata.Metadata) (err error) {
h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") h.md.readTimeout = mdutil.GetDuration(md, "readTimeout")
if h.md.readTimeout <= 0 { if h.md.readTimeout <= 0 {
@@ -100,4 +148,4 @@ func (h *relayHandler) parseMetadata(md mdata.Metadata) (err error) {
h.md.limiterCleanupInterval = mdutil.GetDuration(md, "limiter.cleanupInterval") h.md.limiterCleanupInterval = mdutil.GetDuration(md, "limiter.cleanupInterval")
return return
} }
+33 -2
View File
@@ -8,6 +8,11 @@ import (
"github.com/go-gost/core/observer" "github.com/go-gost/core/observer"
) )
// checkRateLimit checks whether a new connection from addr should be accepted.
//
// If a RateLimiter is configured, it looks up the rate limiter for the client
// host and calls Allow(1). Without a RateLimiter or without a limiter for the
// specific host, the connection is allowed by default.
func (h *relayHandler) checkRateLimit(addr net.Addr) bool { func (h *relayHandler) checkRateLimit(addr net.Addr) bool {
if h.options.RateLimiter == nil { if h.options.RateLimiter == nil {
return true return true
@@ -20,11 +25,34 @@ func (h *relayHandler) checkRateLimit(addr net.Addr) bool {
return true return true
} }
// observeStats is a background goroutine that periodically collects and pushes
// stats events. It is started in Init() when an Observer is configured.
//
// Event collection flow:
//
// observeStats()
// ├─ ticker fires at observerPeriod intervals
// ├─ Each tick:
// │ ├─ If previous events failed (events != nil):
// │ │ └─ Retry sending buffered events
// │ │ └─ If it fails again → continue (retain events for next tick)
// │ ├─ Call h.stats.Events() for new events
// │ ├─ If there are new events:
// │ │ ├─ Try sending (Observe)
// │ │ └─ If it fails → save to events, retry next tick
// │ └─ On success, clear events = nil
// └─ ctx.Done() → exit
//
// Retry mechanism:
// - If Observe() returns an error, events are retained for the next retry.
// - On retry, buffered events are sent first, then new events are fetched.
// - This ensures events are not lost due to transient network issues.
func (h *relayHandler) observeStats(ctx context.Context) { func (h *relayHandler) observeStats(ctx context.Context) {
if h.options.Observer == nil { if h.options.Observer == nil {
return return
} }
// events holds events that failed to send on the previous tick, for retry.
var events []observer.Event var events []observer.Event
ticker := time.NewTicker(h.md.observerPeriod) ticker := time.NewTicker(h.md.observerPeriod)
@@ -33,20 +61,23 @@ func (h *relayHandler) observeStats(ctx context.Context) {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
// Try to flush any buffered events from a previous failed attempt. // First, retry sending any buffered events from a previous failed attempt.
if len(events) > 0 { if len(events) > 0 {
if err := h.options.Observer.Observe(ctx, events); err != nil { if err := h.options.Observer.Observe(ctx, events); err != nil {
// Failed again, retain events for the next retry.
continue continue
} }
} }
// Collect and send fresh events. // Fetch and send new events.
if evs := h.stats.Events(); len(evs) > 0 { if evs := h.stats.Events(); len(evs) > 0 {
if err := h.options.Observer.Observe(ctx, evs); err != nil { if err := h.options.Observer.Observe(ctx, evs); err != nil {
// Failed, cache for retry on the next tick.
events = evs events = evs
continue continue
} }
} }
// Sent successfully, clear the cache.
events = nil events = nil
case <-ctx.Done(): case <-ctx.Done():