From 0273bc6921b13e40e3278f63e8ff4677585f606f Mon Sep 17 00:00:00 2001 From: root Date: Wed, 4 Feb 2026 06:36:03 +0000 Subject: [PATCH 1/2] docs: add AGENTS.md for go-gost/x/registry --- go-gost/x/registry/AGENTS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 go-gost/x/registry/AGENTS.md diff --git a/go-gost/x/registry/AGENTS.md b/go-gost/x/registry/AGENTS.md new file mode 100644 index 0000000..10f5ab1 --- /dev/null +++ b/go-gost/x/registry/AGENTS.md @@ -0,0 +1,29 @@ +# GO-GOST REGISTRY KNOWLEDGE BASE + +**Generated:** Wed Feb 04 2026 + +## OVERVIEW +Central registration point for all pluggable GOST components (handlers, listeners, dialers, etc.). +Allows the configuration system to resolve string types (e.g., "socks5") to actual Go implementations. + +## STRUCTURE +One file per component type, exporting a standard Registry interface. +``` +go-gost/x/registry/ +├── handler.go # RegisterHandler(name, newFunc) +├── listener.go # RegisterListener(name, newFunc) +├── dialer.go # RegisterDialer(name, newFunc) +└── ... # Same pattern for auth, bypass, admission +``` + +## WHERE TO LOOK +| Task | Location | Notes | +|------|----------|-------| +| Register a new component | `go-gost/x/registry/{type}.go` | Use `Register{Type}(name, creator)` | +| Component lookup | `go-gost/x/registry/{type}.go` | `Get{Type}(name)` returns the creator function | +| Default registrations | `go-gost/x/` (init functions) | Most components register themselves in their package `init()` | + +## CONVENTIONS +- Thread-safe maps used for storage. +- Names are case-sensitive (usually lowercase). +- Components must be registered *before* the configuration parser runs (usually done via `import _ "..."` in `main.go`). From 3e046fc80e8277204896221737fedcb48cc0da9a Mon Sep 17 00:00:00 2001 From: root Date: Wed, 4 Feb 2026 07:16:34 +0000 Subject: [PATCH 2/2] fix(gost): restore single-node bypass and preserve FailFilter backoff Address reviewer feedback from PR #14 fix: 1. Single-node case: Bypass selector/FailFilter to ensure availability. This matches upstream go-gost/x behavior - single nodes should always be attempted regardless of recent failures. 2. Multi-node case: Preserve FailFilter's backoff contract. When all nodes are marked as failed, return nil to signal 'no healthy nodes' rather than falling back to a known-bad node. This prevents hammering unhealthy nodes and respects the failTimeout window. The handler's retry loop with ExcludeNodes context handles the multi-node failover properly - this change ensures hop.Select() provides correct information about node health status. Fixes intermittent forwarding failures introduced by #14. --- go-gost/x/hop/hop.go | 23 +++++++++++++++++++---- go-gost/x/selector/filter.go | 9 +++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/go-gost/x/hop/hop.go b/go-gost/x/hop/hop.go index 60076eb..53cdcd3 100644 --- a/go-gost/x/hop/hop.go +++ b/go-gost/x/hop/hop.go @@ -201,11 +201,26 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain. return nodes[0] } - // Always go through selector for proper FailFilter evaluation, - // even when there's only one node. This ensures failed nodes - // can be filtered out properly. + // For single-node case: bypass selector/FailFilter to ensure availability. + // The marker system still works for metrics, but we don't block requests + // based on recent failures - the connection will be attempted regardless. + // This matches upstream go-gost/x behavior. + if len(nodes) == 1 { + return nodes[0] + } + + // Multi-node case: use selector with FailFilter for proper failover. + // FailFilter will exclude recently-failed nodes, allowing traffic to + // be routed to healthy alternatives. if s := p.options.selector; s != nil { - return s.Select(ctx, nodes...) + if node := s.Select(ctx, nodes...); node != nil { + return node + } + // All nodes filtered out by FailFilter - all are marked as failed. + // Return nil to signal "no healthy nodes available" to the caller. + // The handler's retry loop will handle this appropriately. + log.Debugf("all %d nodes filtered out by FailFilter, no healthy nodes available", len(nodes)) + return nil } // Fallback: return first node if no selector configured diff --git a/go-gost/x/selector/filter.go b/go-gost/x/selector/filter.go index 69befe3..c841ad6 100644 --- a/go-gost/x/selector/filter.go +++ b/go-gost/x/selector/filter.go @@ -24,11 +24,12 @@ func FailFilter[T any](maxFails int, timeout time.Duration) selector.Filter[T] { } // Filter filters dead objects. -// Note: We intentionally do NOT skip filtering when len(vs) <= 1. -// This ensures that even a single dead node gets filtered out, -// allowing the caller to know that no healthy nodes are available -// and potentially trigger failover behavior. +// For single-node case, skip filtering to ensure availability (matches upstream). +// For multi-node case, filter out failed nodes to enable failover. func (f *failFilter[T]) Filter(ctx context.Context, vs ...T) []T { + if len(vs) <= 1 { + return vs + } var l []T for _, v := range vs { maxFails := f.maxFails