fix: add bootstraps for relayset
Kim committed
Apr 13, 2026 at 14:10 UTC
9afbbd854ac4080eb72bf74f6a0ae4bfdb970ca8
7 files changed
+16
-127
cmd/relay-server/main.go
+1
-1
@@ -67,7 +67,7 @@ func runServeCommand(args []string) error {
67
68
utils.StringFlagEnv(fs, &cfg.PortalURL, "portal-url", "https://localhost:4017", "portal base URL", "PORTAL_URL")
69
utils.StringFlagEnv(fs, &cfg.IdentityPath, "identity-path", "./.portal-certs", "directory path for relay identity, admin state, and keyless materials", "IDENTITY_PATH")
70
- utils.StringFlagEnv(fs, &cfg.Bootstraps, "bootstraps", "", "additional bootstrap relay API URLs used for discovery expansion", "BOOTSTRAPS")
70
+ utils.StringFlagEnv(fs, &cfg.Bootstraps, "bootstraps", "", "bootstrap relay API URLs; merged with public registry relays when discovery is enabled", "BOOTSTRAPS")
71
utils.BoolFlagEnv(fs, &cfg.DiscoveryEnabled, "discovery", false, "serve relay discovery endpoints and poll discovery peers", "DISCOVERY")
72
utils.IntFlagEnv(fs, &cfg.WireGuardPort, "wireguard-port", overlay.DefaultListenPort, utils.ParsePortNumber, "public and listen UDP port for relay overlay", "WIREGUARD_PORT")
73
portal/discovery/relayset.go
+5
-33
@@ -20,20 +20,12 @@ type RelaySet struct {
20
mu sync.RWMutex
21
relays map[string]RelayState
22
policy RelayPolicy
23
- self RelayState
23
}
24
26
-func NewRelaySet(identity types.Identity, relayURL string, bootstrapRelayURLs []string) (*RelaySet, error) {
25
+func NewRelaySet(bootstrapRelayURLs []string) (*RelaySet, error) {
26
set := &RelaySet{
27
relays: make(map[string]RelayState),
28
policy: DefaultRelayPolicy{},
30
- self: RelayState{
31
- Descriptor: types.RelayDescriptor{
32
- Identity: identity,
33
- RelayID: relayURL,
34
- APIHTTPSAddr: relayURL,
35
- },
36
- },
29
}
30
if err := set.SetBootstrapRelayURLs(bootstrapRelayURLs); err != nil {
31
return nil, err
@@ -54,19 +46,12 @@ func (s *RelaySet) SetBootstrapRelayURLs(inputs []string) error {
46
s.mu.Lock()
47
defer s.mu.Unlock()
48
57
- filtered := utils.RemoveRelayURL(inputs, s.self.Descriptor.APIHTTPSAddr)
58
- keep := make(map[string]struct{}, len(filtered))
59
- for _, relayURL := range filtered {
49
+ keep := make(map[string]struct{}, len(inputs))
50
+ for _, relayURL := range inputs {
51
keep[relayURL] = struct{}{}
52
}
53
63
- seen := make(map[string]struct{}, len(filtered))
54
for key, state := range s.relays {
65
- if state.Equal(s.self) {
66
- delete(s.relays, key)
67
- continue
68
- }
69
-
55
_, bootstrap := keep[key]
56
state.Bootstrap = bootstrap
57
if !state.Bootstrap &&
@@ -78,13 +63,10 @@ func (s *RelaySet) SetBootstrapRelayURLs(inputs []string) error {
63
}
64
65
s.relays[key] = state
81
- if bootstrap {
82
- seen[key] = struct{}{}
83
- }
66
}
67
86
- for _, relayURL := range filtered {
87
- if _, ok := seen[relayURL]; ok {
68
+ for _, relayURL := range inputs {
69
+ if _, ok := s.relays[relayURL]; ok {
70
continue
71
}
72
@@ -170,10 +152,6 @@ func (s *RelaySet) BanRelayURL(relayURL string) {
152
if !ok {
153
state = newRelayStateFromURL(relayURL)
154
}
173
- if state.Equal(s.self) {
174
- delete(s.relays, relayURL)
175
- return
176
- }
155
state = s.policy.OnBanned(state)
156
s.relays[relayURL] = state
157
}
@@ -252,9 +230,6 @@ func (s *RelaySet) ApplyRelayDiscoveryResponse(targetIdentity types.Identity, ta
230
if err != nil {
231
return false, err
232
}
255
- if selfState.Equal(s.self) {
256
- return false, nil
257
- }
233
if strings.TrimSpace(targetIdentity.Name) == "" && strings.TrimSpace(targetIdentity.Address) == "" {
234
return false, errors.New("target relay identity is required")
235
}
@@ -287,9 +262,6 @@ func (s *RelaySet) ApplyRelayDiscoveryResponse(targetIdentity types.Identity, ta
262
if err != nil {
263
continue
264
}
290
- if relayState.Equal(s.self) {
291
- continue
292
- }
265
relayKey := relayState.Descriptor.Key()
266
if _, ok := seen[relayKey]; ok {
267
continue
portal/discovery/relayset_test.go
+2
-2
@@ -8,7 +8,7 @@ import (
8
)
9
10
func TestApplyRelayDiscoveryResponsePreservesBootstrapFlag(t *testing.T) {
11
- set, err := NewRelaySet(types.Identity{}, "", []string{"https://relay-a.example"})
11
+ set, err := NewRelaySet([]string{"https://relay-a.example"})
12
if err != nil {
13
t.Fatalf("NewRelaySet() error = %v", err)
14
}
@@ -31,7 +31,7 @@ func TestApplyRelayDiscoveryResponsePreservesBootstrapFlag(t *testing.T) {
31
}
32
33
func TestApplyRelayDiscoveryResponseAllowsURLChangeForSameIdentity(t *testing.T) {
34
- set, err := NewRelaySet(types.Identity{}, "", nil)
34
+ set, err := NewRelaySet(nil)
35
if err != nil {
36
t.Fatalf("NewRelaySet() error = %v", err)
37
}
portal/server.go
+6
-1
@@ -139,7 +139,12 @@ func NewServer(cfg ServerConfig) (*Server, error) {
139
}
140
var relaySet *discovery.RelaySet
141
if cfg.DiscoveryEnabled {
142
- relaySet, err = discovery.NewRelaySet(identity.Base(), cfg.PortalURL, cfg.Bootstraps)
142
+ cfg.Bootstraps, err = utils.ResolvePortalRelayURLs(context.Background(), cfg.Bootstraps, true)
143
+ if err != nil {
144
+ return nil, fmt.Errorf("resolve discovery bootstraps: %w", err)
145
+ }
146
+ cfg.Bootstraps = utils.RemoveRelayURL(cfg.Bootstraps, cfg.PortalURL)
147
+ relaySet, err = discovery.NewRelaySet(cfg.Bootstraps)
148
if err != nil {
149
return nil, err
150
}
portal/server_test.go
-87
@@ -436,93 +436,6 @@ func TestRegisterLeaseBuildsUDPEnabledRuntime(t *testing.T) {
436
}
437
}
438
439
-func TestServerSetBootstrapRelayURLsAllowsLoopbackButSkipsSelfRelay(t *testing.T) {
440
- t.Parallel()
441
-
442
- server, err := NewServer(ServerConfig{
443
- PortalURL: "https://relay-a.example.com",
444
- IdentityPath: tempIdentityPath(t),
445
- Bootstraps: []string{"https://bootstrap.example.com"},
446
- DiscoveryEnabled: true,
447
- })
448
- if err != nil {
449
- t.Fatalf("NewServer() error = %v", err)
450
- }
451
-
452
- if err := server.relaySet.SetBootstrapRelayURLs([]string{
453
- "https://bootstrap.example.com",
454
- "https://localhost:4017",
455
- "https://relay-a.example.com",
456
- "https://relay-b.example.com",
457
- }); err != nil {
458
- t.Fatalf("SetBootstrapRelayURLs() error = %v", err)
459
- }
460
- advertisedDescriptors := server.relaySet.ConfirmedDescriptors()
461
- knownURLs := make([]string, 0)
462
- for _, state := range server.relaySet.ActiveRelays() {
463
- knownURLs = append(knownURLs, state.Descriptor.APIHTTPSAddr)
464
- }
465
- sort.Strings(knownURLs)
466
- if !reflect.DeepEqual(knownURLs, []string{
467
- "https://bootstrap.example.com",
468
- "https://localhost:4017",
469
- "https://relay-b.example.com",
470
- }) {
471
- t.Fatalf("ActiveRelayURLs() = %v, want loopback kept and self filtered", knownURLs)
472
- }
473
- if len(advertisedDescriptors) != 0 {
474
- t.Fatalf("advertised count = %d, want 0 before direct confirmation", len(advertisedDescriptors))
475
- }
476
-}
477
-
478
-func TestServerDiscoverySkipsSelfRelayHint(t *testing.T) {
479
- t.Parallel()
480
-
481
- server, err := NewServer(ServerConfig{
482
- PortalURL: "https://portal.example.com",
483
- IdentityPath: tempIdentityPath(t),
484
- Bootstraps: []string{"https://bootstrap.example.com"},
485
- DiscoveryEnabled: true,
486
- })
487
- if err != nil {
488
- t.Fatalf("NewServer() error = %v", err)
489
- }
490
-
491
- now := time.Now().UTC()
492
- bootstrapDesc := mustRelayDescriptor(t, "https://bootstrap.example.com")
493
- selfHint, err := utils.NormalizeDescriptor(types.RelayDescriptor{
494
- Identity: server.identity.Base(),
495
- RelayID: "https://self-mirror.example.com",
496
- Version: 1,
497
- IssuedAt: now,
498
- ExpiresAt: now.Add(time.Hour),
499
- APIHTTPSAddr: "https://self-mirror.example.com",
500
- })
501
- if err != nil {
502
- t.Fatalf("NormalizeDescriptor() self hint error = %v", err)
503
- }
504
-
505
- if err := applyRelay(
506
- t,
507
- server.relaySet,
508
- bootstrapDesc.Identity,
509
- bootstrapDesc.APIHTTPSAddr,
510
- types.DiscoveryResponse{ProtocolVersion: types.ProtocolVersion, Self: bootstrapDesc, Relays: []types.RelayDescriptor{selfHint}},
511
- now,
512
- ); err != nil {
513
- t.Fatalf("ApplyRelayDiscoveryResponse() error = %v", err)
514
- }
515
-
516
- knownURLs := make([]string, 0)
517
- for _, state := range server.relaySet.ActiveRelays() {
518
- knownURLs = append(knownURLs, state.Descriptor.APIHTTPSAddr)
519
- }
520
- sort.Strings(knownURLs)
521
- if !reflect.DeepEqual(knownURLs, []string{"https://bootstrap.example.com"}) {
522
- t.Fatalf("ActiveRelayURLs() = %v, want self hint excluded", knownURLs)
523
- }
524
-}
525
-
439
func TestServerRecordVerifiedDiscoveryPeerRequiresDirectConfirmation(t *testing.T) {
440
t.Parallel()
441
sdk/expose.go
+1
-1
@@ -105,7 +105,7 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
105
return nil, fmt.Errorf("invalid --udp-addr value %q: %w", cfg.UDPAddr, err)
106
}
107
}
108
- relaySet, err := discovery.NewRelaySet(types.Identity{}, "", relayURLs)
108
+ relaySet, err := discovery.NewRelaySet(relayURLs)
109
if err != nil {
110
return nil, err
111
}
sdk/expose_test.go
+1
-2
@@ -5,13 +5,12 @@ import (
5
"testing"
6
7
"github.com/gosuda/portal-tunnel/v2/portal/discovery"
8
- "github.com/gosuda/portal-tunnel/v2/types"
8
)
9
10
func mustRelaySet(t *testing.T, relayURLs ...string) *discovery.RelaySet {
11
t.Helper()
12
14
- set, err := discovery.NewRelaySet(types.Identity{}, "", relayURLs)
13
+ set, err := discovery.NewRelaySet(relayURLs)
14
if err != nil {
15
t.Fatalf("NewRelaySet() error = %v", err)
16
}