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 }