fix(agent): retire replaced tunnel sessions (#541)

This commit is contained in:
sagit
2026-08-07 14:35:30 +08:00
committed by GitHub
parent 9e6f80019d
commit 0b23d6f7d7
22 changed files with 701 additions and 19 deletions
+46 -6
View File
@@ -2,6 +2,8 @@ package chain
import (
"context"
"errors"
"io"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/hop"
@@ -38,11 +40,12 @@ type chainNamer interface {
}
type Chain struct {
name string
hops []hop.Hop
marker selector.Marker
metadata metadata.Metadata
logger logger.Logger
name string
hops []hop.Hop
ownedHops []hop.Hop
marker selector.Marker
metadata metadata.Metadata
logger logger.Logger
}
func NewChain(name string, opts ...ChainOption) *Chain {
@@ -61,8 +64,15 @@ func NewChain(name string, opts ...ChainOption) *Chain {
}
}
func (c *Chain) AddHop(hop hop.Hop) {
func (c *Chain) AddHop(hop hop.Hop, owned ...bool) {
c.hops = append(c.hops, hop)
isOwned := true
if len(owned) > 0 {
isOwned = owned[0]
}
if isOwned {
c.ownedHops = append(c.ownedHops, hop)
}
}
// Metadata implements metadata.Metadatable interface.
@@ -112,6 +122,36 @@ func (c *Chain) Route(ctx context.Context, network, address string, opts ...chai
return rt
}
// Retire gracefully drains resources owned by a chain that has been replaced.
func (c *Chain) Retire() {
if c == nil {
return
}
for _, h := range c.ownedHops {
if retirer, ok := h.(interface{ Retire() }); ok {
retirer.Retire()
continue
}
if closer, ok := h.(io.Closer); ok {
_ = closer.Close()
}
}
}
// Close immediately releases all resources owned by the chain.
func (c *Chain) Close() error {
if c == nil {
return nil
}
var errs []error
for _, h := range c.ownedHops {
if closer, ok := h.(io.Closer); ok {
errs = append(errs, closer.Close())
}
}
return errors.Join(errs...)
}
type chainGroup struct {
chains []chain.Chainer
selector selector.Selector[chain.Chainer]
+64
View File
@@ -0,0 +1,64 @@
package chain
import (
"context"
"testing"
corechain "github.com/go-gost/core/chain"
corehop "github.com/go-gost/core/hop"
)
type lifecycleTestHop struct {
selected int
retired int
closed int
}
func (h *lifecycleTestHop) Select(context.Context, ...corehop.SelectOption) *corechain.Node {
h.selected++
return nil
}
func (h *lifecycleTestHop) Retire() {
h.retired++
}
func (h *lifecycleTestHop) Close() error {
h.closed++
return nil
}
func TestChainRoutesThroughSharedHopWithoutOwningLifecycle(t *testing.T) {
hop := &lifecycleTestHop{}
chain := NewChain("shared-hop")
chain.AddHop(hop, false)
if route := chain.Route(context.Background(), "tcp", "example.com:443"); route == nil {
t.Fatal("route is nil")
}
if hop.selected != 1 {
t.Fatalf("shared hop selected %d times, want 1", hop.selected)
}
chain.Retire()
if err := chain.Close(); err != nil {
t.Fatalf("close chain: %v", err)
}
if hop.retired != 0 || hop.closed != 0 {
t.Fatalf("shared hop lifecycle changed: retired=%d closed=%d", hop.retired, hop.closed)
}
}
func TestChainRetiresAndClosesOwnedHop(t *testing.T) {
hop := &lifecycleTestHop{}
chain := NewChain("owned-hop")
chain.AddHop(hop)
chain.Retire()
if err := chain.Close(); err != nil {
t.Fatalf("close chain: %v", err)
}
if hop.retired != 1 || hop.closed != 1 {
t.Fatalf("owned hop lifecycle: retired=%d closed=%d, want 1/1", hop.retired, hop.closed)
}
}
+32 -1
View File
@@ -2,6 +2,8 @@ package chain
import (
"context"
"errors"
"io"
"net"
"github.com/go-gost/core/chain"
@@ -102,5 +104,34 @@ func (tr *Transport) Options() *chain.TransportOptions {
func (tr *Transport) Copy() chain.Transporter {
tr2 := &Transport{}
*tr2 = *tr
return tr
return tr2
}
// Retire prevents long-lived dialer sessions owned by an obsolete chain from
// accepting new streams while allowing existing streams to drain.
func (tr *Transport) Retire() {
if tr == nil {
return
}
if retirer, ok := tr.dialer.(interface{ Retire() }); ok {
retirer.Retire()
}
if retirer, ok := tr.connector.(interface{ Retire() }); ok {
retirer.Retire()
}
}
// Close immediately releases transport-owned dialer and connector resources.
func (tr *Transport) Close() error {
if tr == nil {
return nil
}
var errs []error
if closer, ok := tr.dialer.(io.Closer); ok {
errs = append(errs, closer.Close())
}
if closer, ok := tr.connector.(io.Closer); ok {
errs = append(errs, closer.Close())
}
return errors.Join(errs...)
}
+45
View File
@@ -0,0 +1,45 @@
package chain
import (
"context"
"net"
"testing"
corechain "github.com/go-gost/core/chain"
)
type copyTestRoute struct{}
func (copyTestRoute) Dial(context.Context, string, string, ...corechain.DialOption) (net.Conn, error) {
return nil, nil
}
func (copyTestRoute) Bind(context.Context, string, string, ...corechain.BindOption) (net.Listener, error) {
return nil, nil
}
func (copyTestRoute) Nodes() []*corechain.Node {
return nil
}
func TestTransportCopyReturnsIndependentTransport(t *testing.T) {
originalRoute := copyTestRoute{}
replacementRoute := &copyTestRoute{}
original := NewTransport(nil, nil, corechain.RouteTransportOption(originalRoute))
copied, ok := original.Copy().(*Transport)
if !ok {
t.Fatalf("copy type = %T, want *Transport", original.Copy())
}
if copied == original {
t.Fatal("Copy returned the original transport")
}
copied.Options().Route = replacementRoute
if original.Options().Route != originalRoute {
t.Fatal("mutating copied transport changed original route")
}
if copied.Options().Route != replacementRoute {
t.Fatal("copied transport did not retain its independent route")
}
}