Add best exit selection (#486)

* docs: add best exit selection design

* feat: add best exit selection
This commit is contained in:
sagit
2026-05-01 08:12:17 +08:00
committed by GitHub
parent 5e8d0682c0
commit 3f3159aafd
13 changed files with 1557 additions and 77 deletions
+15
View File
@@ -12,10 +12,25 @@ type chainRegistry struct {
registry[chain.Chainer]
}
func ReplaceChain(name string, v chain.Chainer) error {
if name == "" {
return nil
}
if r, ok := chainReg.(*chainRegistry); ok {
r.replace(name, v)
return nil
}
return chainReg.Register(name, v)
}
func (r *chainRegistry) Register(name string, v chain.Chainer) error {
return r.registry.Register(name, v)
}
func (r *chainRegistry) replace(name string, v chain.Chainer) {
r.m.Store(name, v)
}
func (r *chainRegistry) Get(name string) chain.Chainer {
if name != "" {
return &chainWrapper{name: name, r: r}
+51
View File
@@ -0,0 +1,51 @@
package registry
import (
"context"
"net"
"testing"
"github.com/go-gost/core/chain"
)
type testChainer struct {
route chain.Route
}
func (c testChainer) Route(context.Context, string, string, ...chain.RouteOption) chain.Route {
return c.route
}
type testRoute struct {
nodes []*chain.Node
}
func (r testRoute) Dial(context.Context, string, string, ...chain.DialOption) (net.Conn, error) {
return nil, nil
}
func (r testRoute) Bind(context.Context, string, string, ...chain.BindOption) (net.Listener, error) {
return nil, nil
}
func (r testRoute) Nodes() []*chain.Node {
return r.nodes
}
func TestReplaceChainOverwritesExistingRegistration(t *testing.T) {
name := "replace_chain_tdd"
ChainRegistry().Unregister(name)
defer ChainRegistry().Unregister(name)
if err := ChainRegistry().Register(name, testChainer{route: testRoute{nodes: []*chain.Node{{Name: "old"}}}}); err != nil {
t.Fatalf("register old chain: %v", err)
}
if err := ReplaceChain(name, testChainer{route: testRoute{nodes: []*chain.Node{{Name: "new"}}}}); err != nil {
t.Fatalf("replace chain: %v", err)
}
route := ChainRegistry().Get(name).Route(context.Background(), "tcp", "example.com:443")
if route == nil || len(route.Nodes()) != 1 || route.Nodes()[0].Name != "new" {
t.Fatalf("expected replacement chain route, got %#v", route)
}
}
+6 -6
View File
@@ -38,21 +38,21 @@ func createChain(req createChainRequest) error {
}
func updateChain(req updateChainRequest) error {
name := strings.TrimSpace(req.Chain)
if registry.ChainRegistry().IsRegistered(name) {
registry.ChainRegistry().Unregister(name)
if name == "" {
name = strings.TrimSpace(req.Data.Name)
}
if name == "" {
return errors.New("chain name is required")
}
req.Data.Name = name
v, err := parser.ParseChain(&req.Data, logger.Default())
if err != nil {
return errors.New("create chain " + name + " failed: " + err.Error())
}
if err := registry.ChainRegistry().Register(name, v); err != nil {
if err := registry.ReplaceChain(name, v); err != nil {
return errors.New("chain " + name + " already exists")
}
+69
View File
@@ -0,0 +1,69 @@
package socket
import (
"testing"
corelogger "github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
_ "github.com/go-gost/x/connector/relay"
_ "github.com/go-gost/x/dialer/tcp"
xlogger "github.com/go-gost/x/logger"
"github.com/go-gost/x/registry"
)
func TestUpdateChainParseFailureKeepsExistingChainRegistered(t *testing.T) {
corelogger.SetDefault(xlogger.Nop())
name := "chain_update_parse_failure_tdd"
originalConfig := config.Global()
defer config.Set(originalConfig)
registry.ChainRegistry().Unregister(name)
defer registry.ChainRegistry().Unregister(name)
config.Set(&config.Config{})
valid := config.ChainConfig{
Name: name,
Hops: []*config.HopConfig{{
Name: "hop-valid",
Nodes: []*config.NodeConfig{{
Name: "node-valid",
Addr: "127.0.0.1:443",
Connector: &config.ConnectorConfig{Type: "relay"},
Dialer: &config.DialerConfig{Type: "tcp"},
}},
}},
}
if err := createChain(createChainRequest{Data: valid}); err != nil {
t.Fatalf("create valid chain: %v", err)
}
before := registry.ChainRegistry().Get(name)
if before == nil || !registry.ChainRegistry().IsRegistered(name) {
t.Fatalf("expected chain registered before update")
}
invalid := config.ChainConfig{
Hops: []*config.HopConfig{{
Name: "hop-invalid",
Nodes: []*config.NodeConfig{{
Name: "node-invalid",
Addr: "127.0.0.1:443",
Connector: &config.ConnectorConfig{Type: "connector-does-not-exist"},
Dialer: &config.DialerConfig{Type: "tcp"},
}},
}},
}
err := updateChain(updateChainRequest{Chain: name, Data: invalid})
if err == nil {
t.Fatalf("expected invalid chain update to fail")
}
if !registry.ChainRegistry().IsRegistered(name) {
t.Fatalf("expected old chain to remain registered after failed update")
}
cfg := config.Global()
if len(cfg.Chains) != 1 || cfg.Chains[0] == nil || cfg.Chains[0].Name != name {
t.Fatalf("expected original chain config to remain, got %#v", cfg.Chains)
}
if got := cfg.Chains[0].Hops[0].Name; got != "hop-valid" {
t.Fatalf("expected original chain config to remain, got hop %q", got)
}
}