Files
ginuerzh 6e9ff675b5 x: extract duplicated cmd-probe logic into probe.RunCmdProbe
chainGroup and hopGroup had verbatim copies of the cmd-probe block (timeout
default, CmdProber run, marker Mark/Reset, log). Move it to
internal/probe.RunCmdProbe(cfg, marker, log) so both call sites share one
implementation (cleanup5 from the chainGroup review).
2026-07-28 23:31:15 +08:00

369 lines
9.1 KiB
Go

// Package chain implements the core routing infrastructure for GOST.
//
// It provides three key abstractions:
//
// - Router: top-level entry point that resolves addresses, selects routes
// via a Chainer, retries on failure, and records telemetry.
//
// - Chain: a named sequence of proxy hops (nodes). Each hop selects a node
// from its group, and the resulting Route carries traffic through every
// selected node in order.
//
// - Transport: bundles a dialer and connector for a single chain node. It
// handles Dial, Handshake, Connect, and Bind — the four steps needed to
// move traffic through a proxy hop.
//
// # Route traversal
//
// For a chain of N nodes, the first node is reached via Dial → Handshake,
// and each subsequent node via Connect → Handshake through the previous
// connection. On failure, connections are cleaned up and nodes are marked
// so selectors can deprioritize them.
//
// # Multiplexing
//
// When a node's transport supports multiplexing, Chain splits the route at
// that point: nodes before the multiplex-capable node form a sub-route that is
// copied into the transport, establishing a reusable tunnel for subsequent
// connections.
package chain
import (
"context"
"io"
"time"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/hop"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/metadata"
"github.com/go-gost/core/routing"
"github.com/go-gost/core/selector"
"github.com/go-gost/x/internal/probe"
)
var (
_ chain.Chainer = (*chainGroup)(nil)
)
type ChainOptions struct {
Metadata metadata.Metadata
Logger logger.Logger
}
type ChainOption func(*ChainOptions)
func MetadataChainOption(md metadata.Metadata) ChainOption {
return func(opts *ChainOptions) {
opts.Metadata = md
}
}
func LoggerChainOption(logger logger.Logger) ChainOption {
return func(opts *ChainOptions) {
opts.Logger = logger
}
}
type chainNamer interface {
Name() string
}
type Chain struct {
name string
hops []hop.Hop
marker selector.Marker
metadata metadata.Metadata
logger logger.Logger
}
// NewChain creates a new Chain with the given name and options.
func NewChain(name string, opts ...ChainOption) *Chain {
var options ChainOptions
for _, opt := range opts {
if opt != nil {
opt(&options)
}
}
return &Chain{
name: name,
metadata: options.Metadata,
marker: selector.NewFailMarker(),
logger: options.Logger,
}
}
// AddHop appends a hop to the chain. Hops are traversed in order during
// route construction.
func (c *Chain) AddHop(hop hop.Hop) {
c.hops = append(c.hops, hop)
}
// Metadata returns the chain's metadata.
// Implements metadata.Metadatable interface.
func (c *Chain) Metadata() metadata.Metadata {
return c.metadata
}
// Marker implements selector.Markable interface.
func (c *Chain) Marker() selector.Marker {
return c.marker
}
func (c *Chain) Name() string {
return c.name
}
// Close stops all probe goroutines by cascading to each hop's Close,
// which in turn cascades to node.Close() for every node.
func (c *Chain) Close() error {
for _, h := range c.hops {
if closer, ok := h.(io.Closer); ok {
closer.Close()
}
}
return nil
}
// Route builds a route by selecting one node from each hop. If a node
// supports multiplexing, the route is split — nodes before it form a
// sub-route that is copied into the transport for reuse.
func (c *Chain) Route(ctx context.Context, network, address string, opts ...chain.RouteOption) chain.Route {
if c == nil || len(c.hops) == 0 {
return nil
}
var options chain.RouteOptions
for _, opt := range opts {
opt(&options)
}
rt := NewRoute(ChainRouteOption(c))
for _, h := range c.hops {
node := h.Select(ctx,
hop.NetworkSelectOption(network),
hop.AddrSelectOption(address),
hop.HostSelectOption(options.Host),
)
if node == nil {
return rt
}
if node.Options().Transport.Multiplex() {
tr := node.Options().Transport.Copy()
tr.Options().Route = rt
node = node.Copy()
node.Options().Transport = tr
rt = NewRoute(ChainRouteOption(c))
}
rt.addNode(node)
}
return rt
}
type chainGroup struct {
entries []*ChainEntry
selector selector.Selector[chain.Chainer]
logger logger.Logger
cancelFunc context.CancelFunc
}
// NewChainGroup creates a chain group that selects one Chainer from the
// given list using the configured selector (round-robin by default).
func NewChainGroup(chains ...chain.Chainer) *chainGroup {
entries := make([]*ChainEntry, len(chains))
for i, c := range chains {
entries[i] = NewChainEntry(c, nil, nil)
}
return &chainGroup{entries: entries}
}
func (p *chainGroup) WithSelector(s selector.Selector[chain.Chainer]) *chainGroup {
p.selector = s
return p
}
// WithGroupEntries sets the chain entries for the group (replaces defaults)
// and starts probe goroutines for entries that have probe configurations.
func (p *chainGroup) WithGroupEntries(entries ...*ChainEntry) *chainGroup {
// Cancel any previous probe goroutines before replacing entries.
if p.cancelFunc != nil {
p.cancelFunc()
}
p.entries = entries
ctx, cancel := context.WithCancel(context.Background())
p.cancelFunc = cancel
for _, e := range entries {
if e.probe != nil {
go p.runEntryProbe(ctx, e)
}
}
return p
}
// WithGroupLogger sets the logger for the group.
func (p *chainGroup) WithGroupLogger(log logger.Logger) *chainGroup {
p.logger = log
return p
}
func (p *chainGroup) Route(ctx context.Context, network, address string, opts ...chain.RouteOption) chain.Route {
if c := p.selectChain(ctx, network, address, opts...); c != nil {
return c.Route(ctx, network, address, opts...)
}
return nil
}
func (p *chainGroup) selectChain(ctx context.Context, network, address string, opts ...chain.RouteOption) chain.Chainer {
if p == nil || len(p.entries) == 0 {
return nil
}
var options chain.RouteOptions
for _, opt := range opts {
opt(&options)
}
// When no entry has a matcher, the eligible pool equals all entries; the
// selector must still run (FailFilter excludes probe-failed chains), so
// skip the per-call allocation and pass entries directly.
anyMatcher := false
for _, e := range p.entries {
if e.matcher != nil {
anyMatcher = true
break
}
}
if !anyMatcher {
eligible := make([]chain.Chainer, len(p.entries))
for i, e := range p.entries {
eligible[i] = e
}
return p.selectFrom(ctx, eligible...)
}
// Stage 1: matcher filter — build eligible pool.
req := routing.Request{
Network: network,
Host: options.Host,
}
eligible := make([]chain.Chainer, 0, len(p.entries))
for _, e := range p.entries {
if e.matcher != nil {
if !e.matcher.Match(&req) {
continue
}
if p.logger != nil {
p.logger.Debugf("chain entry matched request %s %s", req.Network, req.Host)
}
}
eligible = append(eligible, e) // *ChainEntry as chain.Chainer, FailFilter reads e.Marker()
}
if len(eligible) == 0 {
return nil
}
// Stage 2: group selector — FailFilter + strategy.
return p.selectFrom(ctx, eligible...)
}
func (p *chainGroup) selectFrom(ctx context.Context, eligible ...chain.Chainer) chain.Chainer {
if p.selector != nil {
return p.selector.Select(ctx, eligible...)
}
return eligible[0]
}
// Close stops all probe goroutines.
func (p *chainGroup) Close() error {
if p.cancelFunc != nil {
p.cancelFunc()
}
return nil
}
// runEntryProbe periodically probes a chain and marks
// the chain entry (not the chain's nodes) on failure.
func (p *chainGroup) runEntryProbe(ctx context.Context, e *ChainEntry) {
cfg := e.probe
interval := cfg.Interval
if interval <= 0 {
interval = 30 * time.Second
}
p.probeEntry(ctx, e, cfg) // first probe immediately
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
p.probeEntry(ctx, e, cfg)
case <-ctx.Done():
return
}
}
}
func (p *chainGroup) probeEntry(ctx context.Context, e *ChainEntry, cfg *chain.ProbeConfig) {
// cmd probe runs a shell command — no chain routing needed.
if cfg.Type == chain.ProbeTypeCmd {
probe.RunCmdProbe(cfg, e.marker, p.logger)
return
}
// TCP/HTTP probe: route through the chain to probe target.
addr := cfg.Addr
if addr == "" {
e.marker.Mark()
if p.logger != nil {
p.logger.Debug("chain entry probe: no probe address configured")
}
return
}
// Route through the probe lifecycle context so cancellation (reload/close)
// aborts an in-flight probe instead of blocking forever on a stuck route.
route := e.chainer.Route(ctx, "tcp", addr)
if route == nil {
e.marker.Mark()
if p.logger != nil {
p.logger.Debug("chain entry probe: no route available")
}
return
}
timeout := cfg.Timeout
if timeout <= 0 {
timeout = 10 * time.Second
}
probeCtx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
conn, err := route.Dial(probeCtx, "tcp", addr)
if err != nil {
e.marker.Mark()
if p.logger != nil {
p.logger.Debugf("chain entry probe dial %s failed: %v", addr, err)
}
return
}
defer conn.Close()
if cfg.Type == chain.ProbeTypeHTTP {
if err := probe.NewHTTPProber(cfg).Probe(conn); err != nil {
e.marker.Mark()
if p.logger != nil {
p.logger.Debugf("chain entry probe http %s failed: %v", addr, err)
}
return
}
}
e.marker.Reset()
}