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:
+145
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user