Files
x/handler/router/handler.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

314 lines
8.8 KiB
Go

package router
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"time"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/limiter"
"github.com/go-gost/core/limiter/traffic"
"github.com/go-gost/core/logger"
md "github.com/go-gost/core/metadata"
"github.com/go-gost/relay"
xctx "github.com/go-gost/x/ctx"
xnet "github.com/go-gost/x/internal/net"
"github.com/go-gost/x/internal/util/cache"
stats_util "github.com/go-gost/x/internal/util/stats"
rate_limiter "github.com/go-gost/x/limiter/rate"
cache_limiter "github.com/go-gost/x/limiter/traffic/cache"
"github.com/go-gost/x/registry"
"github.com/google/uuid"
)
// Error sentinel values returned by the router handler.
var (
ErrBadVersion = errors.New("bad version")
ErrUnknownCmd = errors.New("unknown command")
ErrRouterID = errors.New("invalid router ID")
ErrUnauthorized = errors.New("unauthorized")
)
func init() {
registry.HandlerRegistry().Register("router", NewHandler)
}
// routerHandler is the main handler for the GOST relay router protocol.
//
// It acts as the server-side component of a tunnel mesh: accepts TCP
// connections from client nodes, authenticates them, and routes IP
// packets between the mesh participants.
//
// # Architecture
//
// ┌───────────────┐
// │ routerHandler │
// ├───────────────┤
// │ pool │ ── ConnectorPool: manages all active connectors
// │ epConn │ ── UDP packet conn for inter-node forwarding
// │ sdCache │ ── Cache for service discovery lookups
// │ routeCache │ ── Cache for route lookups
// │ stats │ ── Per-connector traffic statistics
// │ limiter │ ── Traffic rate limiter (per-client)
// └───────────────┘
//
// The handler only supports relay.CmdAssociate (IP packet forwarding).
// Other commands return ErrUnknownCmd.
type routerHandler struct {
id string
options handler.Options
pool *ConnectorPool
epConn net.PacketConn // UDP socket for inter-node packet forwarding
md metadata
log logger.Logger
stats *stats_util.HandlerStats
limiter traffic.TrafficLimiter
cancel context.CancelFunc // cancels background goroutines (observeStats)
sdCache *cache.Cache // service discovery address cache
routeCache *cache.Cache // route lookup cache
}
// NewHandler creates a new router handler.
//
// Caches are initialized with a 1-minute default TTL; individual entries
// may override this via metadata configuration.
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
return &routerHandler{
options: options,
sdCache: cache.NewCache(time.Minute),
routeCache: cache.NewCache(time.Minute),
}
}
// Init initializes the handler with the given metadata.
//
// Initialization sequence:
// 1. Parse metadata (read timeout, buffer size, entrypoint, etc.)
// 2. Generate a random node ID (UUID)
// 3. Create the connector pool
// 4. Initialize the UDP entrypoint listener (if configured)
// 5. Start the observer stats goroutine (if an observer is set)
// 6. Initialize the traffic limiter (if configured)
func (h *routerHandler) Init(md md.Metadata) (err error) {
if err := h.parseMetadata(md); err != nil {
return err
}
uuid, err := uuid.NewRandom()
if err != nil {
return err
}
h.id = uuid.String()
h.log = h.options.Logger.WithFields(map[string]any{
"node": h.id,
})
h.pool = NewConnectorPool(h.id)
if err = h.initEntrypoint(); err != nil {
return
}
ctx, cancel := context.WithCancel(context.Background())
h.cancel = cancel
if h.options.Observer != nil {
h.stats = stats_util.NewHandlerStats(h.options.Service, h.md.observerResetTraffic)
go h.observeStats(ctx)
}
if h.options.Limiter != nil {
h.limiter = cache_limiter.NewCachedTrafficLimiter(h.options.Limiter,
cache_limiter.RefreshIntervalOption(h.md.limiterRefreshInterval),
cache_limiter.CleanupIntervalOption(h.md.limiterCleanupInterval),
cache_limiter.ScopeOption(limiter.ScopeClient),
)
}
return nil
}
// initEntrypoint creates the UDP listening socket for inter-node
// packet forwarding and starts the background read loop.
//
// The entrypoint is optional — if no entrypoint address is configured,
// this node cannot receive packets from other mesh nodes (it can only
// forward packets to connectors that were established via TCP).
func (h *routerHandler) initEntrypoint() (err error) {
if h.md.entryPoint == "" {
return
}
network := "udp"
if xnet.IsIPv4(h.md.entryPoint) {
network = "udp4"
}
pc, err := net.ListenPacket(network, h.md.entryPoint)
if err != nil {
h.log.Error(err)
return
}
h.epConn = pc
log := h.log.WithFields(map[string]any{
"service": fmt.Sprintf("%s-ep-%s", h.options.Service, pc.LocalAddr()),
"listener": "udp",
"handler": "entrypoint",
"kind": "service",
})
go h.handleEntrypoint(log)
h.log.Infof("entrypoint: %s", pc.LocalAddr())
return
}
// Handle processes an incoming TCP connection using the relay protocol.
//
// # Protocol flow
//
// 1. Read the relay request from the client (with optional read timeout).
// 2. Validate the protocol version.
// 3. Parse features: user authentication, addresses, tunnel ID, network.
// 4. Authenticate the client (if an auther is configured).
// 5. Dispatch to handleAssociate for CmdAssociate, or reject with error.
//
// The connection is always closed on return (via defer).
//
// # Feature parsing order
//
// The relay protocol allows multiple AddrFeatures. The first AddrFeature
// is treated as the source address, the second as the destination. This
// mirrors the behavior of other relay-based handlers (e.g., handler/relay).
func (h *routerHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
defer conn.Close()
start := time.Now()
log := h.log.WithFields(map[string]any{
"network": conn.LocalAddr().Network(),
"remote": conn.RemoteAddr().String(),
"local": conn.LocalAddr().String(),
"sid": xctx.SidFromContext(ctx).String(),
})
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
defer func() {
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}()
if !h.checkRateLimit(conn.RemoteAddr()) {
return rate_limiter.ErrRateLimit
}
if h.md.readTimeout > 0 {
conn.SetReadDeadline(time.Now().Add(h.md.readTimeout))
}
req := relay.Request{}
if _, err := req.ReadFrom(conn); err != nil {
return err
}
// Clear the read deadline — subsequent data transfer (IP packets
// through the packetConn) should not be time-limited.
conn.SetReadDeadline(time.Time{})
resp := relay.Response{
Version: relay.Version1,
Status: relay.StatusOK,
}
if req.Version != relay.Version1 {
resp.Status = relay.StatusBadRequest
resp.WriteTo(conn)
return ErrBadVersion
}
var user, pass string
var srcAddr, dstAddr string
network := "ip"
var routerID relay.TunnelID
for _, f := range req.Features {
switch f.Type() {
case relay.FeatureUserAuth:
if feature, _ := f.(*relay.UserAuthFeature); feature != nil {
user, pass = feature.Username, feature.Password
}
case relay.FeatureAddr:
if feature, _ := f.(*relay.AddrFeature); feature != nil {
v := net.JoinHostPort(feature.Host, strconv.Itoa(int(feature.Port)))
if srcAddr != "" {
dstAddr = v
} else {
srcAddr = v
}
}
case relay.FeatureTunnel:
if feature, _ := f.(*relay.TunnelFeature); feature != nil {
routerID = feature.ID
}
case relay.FeatureNetwork:
if feature, _ := f.(*relay.NetworkFeature); feature != nil {
network = feature.Network.String()
}
}
}
if user != "" {
log = log.WithFields(map[string]any{"user": user})
}
// Authenticate before establishing the tunnel association.
if h.options.Auther != nil {
clientID, ok := h.options.Auther.Authenticate(ctx, user, pass, auth.WithService(h.options.Service))
if !ok {
resp.Status = relay.StatusUnauthorized
resp.WriteTo(conn)
return ErrUnauthorized
}
ctx = xctx.ContextWithClientID(ctx, xctx.ClientID(clientID))
}
switch req.Cmd & relay.CmdMask {
case relay.CmdAssociate:
host, _, _ := net.SplitHostPort(dstAddr)
log.Debugf("associate %s/%s -> %s", host, network, routerID)
return h.handleAssociate(ctx, conn, network, host, routerID, log)
default:
resp.Status = relay.StatusBadRequest
resp.WriteTo(conn)
return ErrUnknownCmd
}
}
// Close shuts down the handler: closes the entrypoint UDP socket,
// closes the connector pool (which closes all connectors), and
// cancels background goroutines.
//
// Implements io.Closer.
func (h *routerHandler) Close() error {
if h.epConn != nil {
h.epConn.Close()
}
h.pool.Close()
if h.cancel != nil {
h.cancel()
}
return nil
}