fix(handler/router): 3 bugs fixed + comprehensive data-flow documentation

Bug fixes:
- packetConn.Read: fix slice bounds panic when dlen > len(b);
  n was set to dlen (the wire size) but only len(b) bytes were
  copied to b; callers doing b[:n] would panic. Clamp n via copy().
- lockWriter.Close: add mutex lock to prevent data race with Write;
  Router.Close calls connector.Close (lockWriter.Close) while
  handlePacket concurrently calls Writer.Write — both touch w.w.
- Router.DelConnector: delete empty host slices from the map after
  removing the last connector, preventing unbounded map growth.

Documentation:
- Package-level doc with architecture diagram, data flow, component
  hierarchy, thread-safety invariants, and connector weighting rules.
- Documented every exported type (Connector, Router, ConnectorPool,
  ConnectorOptions, routerHandler, metadata, lockWriter, packetConn)
  and all non-trivial methods with purpose, parameters, lifecycle,
  and algorithmic details (e.g., GetConnector weighted selection).
- Annotated critical code paths with step-by-step comments
  (handleAssociate stages, handlePacket routing algorithm,
   handleEntrypoint packet format and forwarding logic).
- Explained observeStats retry pattern and metadata key conventions.

Verification: build, vet, 98 tests race-clean.
This commit is contained in:
ginuerzh
2026-06-04 23:01:12 +08:00
parent e6c9952ad4
commit e45328d1bb
8 changed files with 510 additions and 241 deletions
+145 -3
View File
@@ -1,3 +1,48 @@
// Package router implements the "router" handler for the GOST framework.
//
// # Overview
//
// The router handler acts as the ingress point of a VPN-like tunnel mesh. It
// receives relay protocol connections (over TCP) from client-side GOST
// instances, authenticates them, and routes IP packets through the mesh of
// tunnel connectors.
//
// # Data flow
//
// Client TCP ──► Handle() ──► [auth + relay handshake]
// │
// └──► handleAssociate() ──► packetConn.Read() loop
// │
// └──► handlePacket()
// │
// ├── 1. Parse IP header (v4/v6)
// ├── 2. getRoute() to find gateway
// ├── 3. pool.Get() — forward via connector
// └── 4. getAddrforRoute() — fallback to
// epConn.WriteTo() to another node
//
// External UDP ──► handleEntrypoint() ──► pool.Get() ──► connector.Write()
//
// # Component hierarchy
//
// ConnectorPool (node-level)
// └── Router (per tunnel ID)
// └── Connector (per host:port)
// └── lockWriter → packetConn → net.Conn (back to client)
//
// # Thread safety
//
// Router and ConnectorPool use sync.RWMutex for all map operations.
// lockWriter serializes writes to the underlying connection, since
// handlePacket and handleEntrypoint may call Write concurrently.
//
// # Connector weighting
//
// Each connector carries a weight embedded in its ConnectorID. When
// multiple connectors exist for the same host, GetConnector uses
// weighted random selection. A weight of MaxWeight (0xff) has special
// meaning: only MaxWeight connectors are selected, providing a
// priority mechanism.
package router
import (
@@ -12,11 +57,28 @@ import (
)
const (
// MaxWeight is the maximum connector weight. A connector with this
// weight takes priority over all other connectors for the same host.
MaxWeight uint8 = 0xff
)
// ConnectorOptions holds optional configuration for a Connector.
// Currently empty but reserved for future use.
type ConnectorOptions struct{}
// Connector represents a single tunnel endpoint bound to a remote client.
//
// A connector is identified by its ConnectorID and associated with a
// TunnelID (router). It pairs a host address with an io.Writer — the
// framed TCP connection back to the client. When the router receives an
// IP packet destined for this connector's host, it writes the raw packet
// to the Writer, and the client-side packetConn decapsulates it.
//
// Lifecycle:
// 1. Created by NewConnector in handleAssociate after the relay handshake.
// 2. Added to the ConnectorPool (and underlying Router) for routing.
// 3. Removed by ConnectorPool.Del when handleAssociate exits (deferred).
// 4. Closed by Router.Close when the router shuts down.
type Connector struct {
id relay.ConnectorID
rid relay.TunnelID
@@ -26,6 +88,14 @@ type Connector struct {
log logger.Logger
}
// NewConnector creates a new Connector.
//
// Parameters:
// - rid: tunnel/route identifier this connector belongs to
// - cid: unique connector identifier (embedding weight)
// - host: the destination host:port this connector forwards to
// - w: the writer for sending IP packets back to the client
// - opts: optional configuration (nil is replaced with zero value)
func NewConnector(rid relay.TunnelID, cid relay.ConnectorID, host string, w io.Writer, opts *ConnectorOptions) *Connector {
if opts == nil {
opts = &ConnectorOptions{}
@@ -47,10 +117,18 @@ func NewConnector(rid relay.TunnelID, cid relay.ConnectorID, host string, w io.W
return c
}
// ID returns the connector's unique identifier.
func (c *Connector) ID() relay.ConnectorID {
return c.id
}
// Writer returns the io.Writer for sending data to the remote client.
// Returns nil if the receiver is nil or the writer was not set.
//
// The returned writer is typically a lockWriter wrapping a packetConn
// wrapping the underlying TCP connection — so Write calls are
// automatically framed with a 2-byte length prefix and are
// mutex-protected against concurrent access.
func (c *Connector) Writer() io.Writer {
if c == nil {
return nil
@@ -59,6 +137,8 @@ func (c *Connector) Writer() io.Writer {
return c.w
}
// Close closes the underlying writer if it implements io.Closer.
// Safe to call on nil receiver or nil writer.
func (c *Connector) Close() error {
if c == nil || c.w == nil {
return nil
@@ -70,14 +150,23 @@ func (c *Connector) Close() error {
return nil
}
// Router manages a set of connectors for a single tunnel (TunnelID).
//
// Connectors are grouped by host address. When a packet arrives destined
// for a particular host, the router selects the appropriate connector
// using weighted random selection.
//
// All methods are safe for concurrent use — the embedded RWMutex guards
// the connectors map.
type Router struct {
node string
id relay.TunnelID
connectors map[string][]*Connector
close chan struct{}
connectors map[string][]*Connector // host → ordered list of connectors
close chan struct{} // closed when the router is shut down
mu sync.RWMutex
}
// NewRouter creates a new Router identified by node name and tunnel ID.
func NewRouter(node string, rid relay.TunnelID) *Router {
r := &Router{
node: node,
@@ -88,10 +177,13 @@ func NewRouter(node string, rid relay.TunnelID) *Router {
return r
}
// ID returns the router's tunnel identifier.
func (r *Router) ID() relay.TunnelID {
return r.id
}
// AddConnector registers a connector in the router. Nil connectors are
// silently ignored.
func (r *Router) AddConnector(c *Connector) {
if c == nil {
return
@@ -103,6 +195,17 @@ func (r *Router) AddConnector(c *Connector) {
r.connectors[c.host] = append(r.connectors[c.host], c)
}
// GetConnector selects a connector for the given host using weighted
// random selection.
//
// Selection rules:
// - Single connector → returned directly.
// - Multiple connectors → weighted random selection.
// - A connector with weight == MaxWeight takes priority: only
// MaxWeight connectors are considered.
// - Weight 0 is treated as weight 1.
//
// Returns nil if no connector exists for the host.
func (r *Router) GetConnector(host string) *Connector {
r.mu.RLock()
defer r.mu.RUnlock()
@@ -134,6 +237,10 @@ func (r *Router) GetConnector(host string) *Connector {
return rw.Next()
}
// DelConnector removes a connector identified by its host and connector ID.
// If the removed connector was the last one for the host, the host entry
// is deleted from the map to prevent accumulation of empty slices.
// If no matching connector is found, the call is a no-op.
func (r *Router) DelConnector(host string, cid relay.ConnectorID) {
r.mu.Lock()
defer r.mu.Unlock()
@@ -141,12 +248,23 @@ func (r *Router) DelConnector(host string, cid relay.ConnectorID) {
connectors := r.connectors[host]
for i, c := range connectors {
if c.id.Equal(cid) {
r.connectors[host] = append(connectors[:i], connectors[i+1:]...)
connectors = append(connectors[:i], connectors[i+1:]...)
break
}
}
if len(connectors) == 0 {
delete(r.connectors, host)
} else {
r.connectors[host] = connectors
}
}
// Close shuts down the router: closes all connectors, clears the map,
// and marks the router as closed. Subsequent calls are no-ops.
//
// The double-close protection uses a select on r.close — under the
// write lock — so it is race-free.
func (r *Router) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
@@ -167,12 +285,20 @@ func (r *Router) Close() error {
return nil
}
// ConnectorPool manages routers keyed by tunnel ID for a single node.
//
// This is the top-level data structure for connector management. Each
// node has one ConnectorPool, and each pool contains one Router per
// active tunnel.
//
// All methods are nil-safe — calling on a nil *ConnectorPool is valid.
type ConnectorPool struct {
node string
routers map[relay.TunnelID]*Router
mu sync.RWMutex
}
// NewConnectorPool creates a new ConnectorPool for the given node.
func NewConnectorPool(node string) *ConnectorPool {
p := &ConnectorPool{
node: node,
@@ -182,6 +308,9 @@ func NewConnectorPool(node string) *ConnectorPool {
return p
}
// Add creates or retrieves a Router for the given tunnel ID and adds
// the connector to it. If no router exists for the tunnel ID, one is
// created automatically.
func (p *ConnectorPool) Add(rid relay.TunnelID, c *Connector) {
p.mu.Lock()
defer p.mu.Unlock()
@@ -194,6 +323,9 @@ func (p *ConnectorPool) Add(rid relay.TunnelID, c *Connector) {
r.AddConnector(c)
}
// Get retrieves a connector for the given tunnel ID and host address.
// Returns nil if the pool is nil, the router doesn't exist, or no
// connector matches the host.
func (p *ConnectorPool) Get(rid relay.TunnelID, host string) *Connector {
if p == nil {
return nil
@@ -210,6 +342,8 @@ func (p *ConnectorPool) Get(rid relay.TunnelID, host string) *Connector {
return r.GetConnector(host)
}
// Del removes a connector from a specific router.
// Safe to call on a nil pool.
func (p *ConnectorPool) Del(rid relay.TunnelID, host string, cid relay.ConnectorID) {
if p == nil {
return
@@ -226,6 +360,8 @@ func (p *ConnectorPool) Del(rid relay.TunnelID, host string, cid relay.Connector
r.DelConnector(host, cid)
}
// Close shuts down all routers and clears the pool. Safe to call on a
// nil pool. Subsequent calls are no-ops (delegates to Router.Close).
func (p *ConnectorPool) Close() error {
if p == nil {
return nil
@@ -242,6 +378,12 @@ func (p *ConnectorPool) Close() error {
return nil
}
// parseRouterID converts a UUID string into a relay.TunnelID.
// Returns a zero-value TunnelID if the string is empty or not a valid UUID.
//
// Ingress rules store router identifiers as UUID strings. This function
// bridges the gap between the string form and the binary TunnelID form
// used internally.
func parseRouterID(s string) (rid relay.TunnelID) {
if s == "" {
return