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:
+76
-4
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user