Files
x/handler/router/router.go
T
ginuerzh e45328d1bb 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.
2026-06-05 20:46:00 +08:00

395 lines
11 KiB
Go

// 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 (
"io"
"sync"
"github.com/go-gost/core/logger"
"github.com/go-gost/relay"
"github.com/go-gost/x/selector"
"github.com/google/uuid"
)
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
host string
w io.Writer
opts *ConnectorOptions
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{}
}
c := &Connector{
rid: rid,
id: cid,
host: host,
w: w,
opts: opts,
log: logger.Default().WithFields(map[string]any{
"router": rid.String(),
"connector": cid.String(),
"host": host,
}),
}
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
}
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
}
if closer, ok := c.w.(io.Closer); ok {
return closer.Close()
}
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 // 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,
id: rid,
connectors: make(map[string][]*Connector),
close: make(chan struct{}),
}
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
}
r.mu.Lock()
defer r.mu.Unlock()
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()
connectors := r.connectors[host]
if len(connectors) == 1 {
return connectors[0]
}
rw := selector.NewRandomWeighted[*Connector]()
found := false
for _, c := range connectors {
weight := c.ID().Weight()
if weight == 0 {
weight = 1
}
if weight == MaxWeight && !found {
rw.Reset()
found = true
}
if weight == MaxWeight || !found {
rw.Add(c, int(weight))
}
}
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()
connectors := r.connectors[host]
for i, c := range connectors {
if c.id.Equal(cid) {
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()
select {
case <-r.close:
default:
for _, cs := range r.connectors {
for _, c := range cs {
c.Close()
}
}
close(r.close)
clear(r.connectors)
}
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,
routers: make(map[relay.TunnelID]*Router),
}
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()
r := p.routers[rid]
if r == nil {
r = NewRouter(p.node, rid)
p.routers[rid] = r
}
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
}
p.mu.RLock()
defer p.mu.RUnlock()
r := p.routers[rid]
if r == nil {
return nil
}
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
}
p.mu.Lock()
defer p.mu.Unlock()
r := p.routers[rid]
if r == nil {
return
}
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
}
p.mu.Lock()
defer p.mu.Unlock()
for _, v := range p.routers {
v.Close()
}
clear(p.routers)
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
}
uuid, _ := uuid.Parse(s)
return relay.NewTunnelID(uuid[:])
}