feat: restore sdk discovery
Kim committed
Apr 14, 2026 at 17:28 UTC
cc447d8805adff75706f84b2a3236a97bd885614
8 files changed
+68
-54
cmd/demo-app/main.go
+4
-10
@@ -129,12 +129,9 @@ func runUDPCommand(args []string) error {
129
}
130
131
func runTCPDemo(ctx context.Context, cfg demoConfig) error {
132
- relayURLs, err := utils.ResolvePortalRelayURLs(ctx, utils.SplitCSV(cfg.relayURLs), cfg.discovery)
133
- if err != nil {
134
- return fmt.Errorf("resolve relay urls: %w", err)
135
- }
132
exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
137
- RelayURLs: relayURLs,
133
+ RelayURLs: utils.SplitCSV(cfg.relayURLs),
134
+ Discovery: cfg.discovery,
135
IdentityPath: cfg.identityPath,
136
IdentityJSON: cfg.identityJSON,
137
Name: cfg.name,
@@ -175,12 +172,9 @@ func runTCPDemo(ctx context.Context, cfg demoConfig) error {
172
}
173
174
func runUDPDemo(ctx context.Context, cfg demoConfig) error {
178
- relayURLs, err := utils.ResolvePortalRelayURLs(ctx, utils.SplitCSV(cfg.relayURLs), cfg.discovery)
179
- if err != nil {
180
- return fmt.Errorf("resolve relay urls: %w", err)
181
- }
175
exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
183
- RelayURLs: relayURLs,
176
+ RelayURLs: utils.SplitCSV(cfg.relayURLs),
177
+ Discovery: cfg.discovery,
178
IdentityPath: cfg.identityPath,
179
IdentityJSON: cfg.identityJSON,
180
Name: cfg.name,
cmd/portal-tunnel/main.go
+2
-6
@@ -100,13 +100,9 @@ func runExposeCommand(args []string) error {
100
ctx, stop := utils.SignalContext()
101
defer stop()
102
103
- relayURLs, err := utils.ResolvePortalRelayURLs(ctx, utils.SplitCSV(flags.relayCSV), flags.discovery)
104
- if err != nil {
105
- return fmt.Errorf("resolve relay urls: %w", err)
106
- }
107
-
103
exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
109
- RelayURLs: relayURLs,
104
+ RelayURLs: utils.SplitCSV(flags.relayCSV),
105
+ Discovery: flags.discovery,
106
IdentityPath: flags.identityPath,
107
IdentityJSON: flags.identityJSON,
108
Name: flags.name,
portal/discovery/refresher.go
+20
-12
@@ -49,7 +49,7 @@ func NewRefresher(relaySet *RelaySet, overlay OverlayRuntime) *Refresher {
49
}
50
}
51
52
-func (r *Refresher) Refresh(ctx context.Context, self types.RelayDescriptor) error {
52
+func (r *Refresher) Refresh(ctx context.Context, self *types.RelayDescriptor) error {
53
if r.overlay != nil {
54
if err := r.refreshOverlay(ctx); err != nil && ctx.Err() == nil {
55
log.Warn().
@@ -63,7 +63,12 @@ func (r *Refresher) Refresh(ctx context.Context, self types.RelayDescriptor) err
63
if err := r.refreshHTTPS(ctx); err != nil {
64
return err
65
}
66
- return r.announceSelf(ctx, self)
66
+ if self != nil {
67
+ if err := r.announceSelf(ctx, *self); err != nil {
68
+ return err
69
+ }
70
+ }
71
+ return nil
72
}
73
74
func (r *Refresher) announceSelf(ctx context.Context, descriptor types.RelayDescriptor) error {
@@ -71,6 +76,18 @@ func (r *Refresher) announceSelf(ctx context.Context, descriptor types.RelayDesc
76
ProtocolVersion: types.DiscoveryVersion,
77
Descriptor: descriptor,
78
}
79
+ httpClient := &http.Client{
80
+ Transport: &http.Transport{
81
+ TLSClientConfig: &tls.Config{
82
+ MinVersion: tls.VersionTLS12,
83
+ NextProtos: []string{"http/1.1"},
84
+ },
85
+ ForceAttemptHTTP2: false,
86
+ },
87
+ Timeout: defaultRequestTimeout,
88
+ }
89
+ defer httpClient.CloseIdleConnections()
90
+
91
for _, relayURL := range r.relaySet.BootstrapRelayURLs() {
92
if relayURL == descriptor.APIHTTPSAddr {
93
continue
@@ -83,11 +100,8 @@ func (r *Refresher) announceSelf(ctx context.Context, descriptor types.RelayDesc
100
Msg("relay discovery announce target skipped")
101
continue
102
}
86
- if utils.IsLocalRelayHost(baseURL.Hostname()) {
87
- continue
88
- }
103
90
- if err := utils.HTTPDoAPIPath(ctx, r.httpClient, baseURL, http.MethodPost, types.PathDiscoveryAnnounce, req, nil, nil); err != nil {
104
+ if err := utils.HTTPDoAPIPath(ctx, httpClient, baseURL, http.MethodPost, types.PathDiscoveryAnnounce, req, nil, nil); err != nil {
105
if ctx.Err() != nil {
106
return ctx.Err()
107
}
@@ -143,12 +157,6 @@ func (r *Refresher) refreshHTTPS(ctx context.Context) error {
157
}
158
continue
159
}
146
- if utils.IsLocalRelayHost(baseURL.Hostname()) {
147
- log.Info().
148
- Str("relay", relayURL).
149
- Msg("skip loopback relay as discovery source")
150
- continue
151
- }
160
161
startedAt := time.Now()
162
var resp types.DiscoveryResponse
portal/server.go
+1
-1
@@ -605,7 +605,7 @@ func (s *Server) runRelayDiscoveryLoop(ctx context.Context) error {
605
if err != nil {
606
return fmt.Errorf("build relay discovery descriptor: %w", err)
607
}
608
- if err := refresher.Refresh(ctx, self); err != nil {
608
+ if err := refresher.Refresh(ctx, &self); err != nil {
609
if ctx.Err() != nil {
610
return nil
611
}
sdk/api_client.go
+1
-3
@@ -42,7 +42,6 @@ type apiClient struct {
42
rawTLSConfig *tls.Config
43
dialTimeout time.Duration
44
requestTimeout time.Duration
45
- rootCAPEM []byte
45
identity types.Identity
46
accessToken string
47
expiresAt time.Time
@@ -69,7 +68,6 @@ func newApiClient(relayURL string, cfg ListenerConfig) (*apiClient, error) {
68
baseURL: baseURL,
69
dialTimeout: dialTimeout,
70
requestTimeout: requestTimeout,
72
- rootCAPEM: append([]byte(nil), cfg.RootCAPEM...),
71
identity: cfg.Identity.Copy(),
72
metadata: cfg.Metadata.Copy(),
73
}, nil
@@ -186,7 +184,7 @@ func (a *apiClient) ensureHTTPClient(ctx context.Context) error {
184
bootstrapCtx, cancel := context.WithTimeout(ctx, defaultDialTimeout+defaultHandshakeTimeout)
185
defer cancel()
186
189
- rawTLSConfig, httpClient, err := utils.NewHTTPTLSClient(bootstrapCtx, a.baseURL, a.rootCAPEM, a.requestTimeout)
187
+ rawTLSConfig, httpClient, err := utils.NewHTTPTLSClient(bootstrapCtx, a.baseURL, a.requestTimeout)
188
if err != nil {
189
return err
190
}
sdk/expose.go
+35
-11
@@ -20,12 +20,11 @@ import (
20
)
21
22
// Exposure owns the lifecycle of one or more relay listeners and accepts
23
-// traffic from all of them through one net.Listener. The SDK is a pure
24
-// relay client: it uses only the explicit relay URLs passed in by the
25
-// caller, never gossips its own descriptor into the discovery mesh, and
26
-// never serves the /discovery endpoint. Relay bootstrap expansion (e.g.
27
-// loading a public registry) is the caller's responsibility — resolve the
28
-// final URL list before invoking Expose.
23
+// traffic from all of them through one net.Listener. The SDK is a pure relay
24
+// client: it never gossips its own descriptor into the discovery mesh and
25
+// never serves the /discovery endpoint. When SDK discovery is enabled, the
26
+// exposure resolves registry seed relays and consumes discovery results from
27
+// attached relays.
28
type Exposure struct {
29
cancel context.CancelFunc
30
done <-chan struct{}
@@ -39,7 +38,6 @@ type Exposure struct {
38
banMITM bool
39
maxActiveRelays int
40
metadata types.LeaseMetadata
42
- rootCAPEM []byte
41
42
accepted chan net.Conn
43
datagrams chan types.DatagramFrame
@@ -54,6 +52,7 @@ type Exposure struct {
52
53
type ExposeConfig struct {
54
RelayURLs []string
55
+ Discovery bool
56
IdentityPath string
57
IdentityJSON string
58
Name string
@@ -64,7 +63,6 @@ type ExposeConfig struct {
63
BanMITM bool
64
MaxActiveRelays int
65
Metadata types.LeaseMetadata
67
- RootCAPEM []byte
66
}
67
68
// Expose creates relay listeners for the selected relay pool and exposes a
@@ -74,7 +72,10 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
72
if err != nil {
73
return nil, err
74
}
77
- relayURLs := explicitRelayURLs
75
+ relayURLs, err := utils.ResolvePortalRelayURLs(ctx, explicitRelayURLs, cfg.Discovery)
76
+ if err != nil {
77
+ return nil, err
78
+ }
79
80
identity, createdIdentity, err := utils.ResolveListenerIdentity(
81
types.Identity{Name: cfg.Name},
@@ -115,7 +116,6 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
116
banMITM: cfg.BanMITM,
117
maxActiveRelays: cfg.MaxActiveRelays,
118
metadata: cfg.Metadata.Copy(),
118
- rootCAPEM: append([]byte(nil), cfg.RootCAPEM...),
119
accepted: make(chan net.Conn, max(len(relayURLs)*defaultReadyTarget*2, 1)),
120
datagrams: make(chan types.DatagramFrame, max(len(relayURLs)*32, 1)),
121
relaySet: discovery.NewRelaySet(relayURLs),
@@ -129,6 +129,10 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
129
}
130
}
131
132
+ if cfg.Discovery {
133
+ go exposure.runDiscoveryLoop(exposureCtx)
134
+ }
135
+
136
go func() {
137
<-exposure.done
138
_ = exposure.Close()
@@ -339,6 +343,27 @@ func (e *Exposure) Close() error {
343
return closeErr
344
}
345
346
+func (e *Exposure) runDiscoveryLoop(ctx context.Context) {
347
+ refresher := discovery.NewRefresher(e.relaySet, nil)
348
+ ticker := time.NewTicker(discovery.DiscoveryPollInterval)
349
+ defer ticker.Stop()
350
+
351
+ for {
352
+ if err := refresher.Refresh(ctx, nil); err != nil {
353
+ return
354
+ }
355
+ if err := e.reconcileRelayListeners(false); err != nil {
356
+ return
357
+ }
358
+
359
+ select {
360
+ case <-ctx.Done():
361
+ return
362
+ case <-ticker.C:
363
+ }
364
+ }
365
+}
366
+
367
func (e *Exposure) reconcileRelayListeners(failOnError bool) error {
368
clientState := discovery.ClientState{
369
ActiveRelayURLs: e.ActiveRelayURLs(),
@@ -395,7 +420,6 @@ func (e *Exposure) reconcileRelayListeners(failOnError bool) error {
420
BanMITM: e.banMITM,
421
RetryCount: retryCount,
422
Metadata: e.metadata.Copy(),
398
- RootCAPEM: append([]byte(nil), e.rootCAPEM...),
423
relaySet: e.relaySet,
424
})
425
if err != nil {
sdk/listener.go
-1
@@ -26,7 +26,6 @@ type ListenerConfig struct {
26
TCPEnabled bool
27
BanMITM bool
28
Metadata types.LeaseMetadata
29
- RootCAPEM []byte
29
DialTimeout time.Duration
30
RequestTimeout time.Duration
31
HandshakeTimeout time.Duration
utils/tls.go
+5
-10
@@ -17,7 +17,7 @@ import (
17
"time"
18
)
19
20
-func NewHTTPTLSClient(ctx context.Context, relayURL *url.URL, rootCAPEM []byte, timeout time.Duration) (*tls.Config, *http.Client, error) {
20
+func NewHTTPTLSClient(ctx context.Context, relayURL *url.URL, timeout time.Duration) (*tls.Config, *http.Client, error) {
21
if relayURL == nil {
22
return nil, nil, errors.New("relay url is required")
23
}
@@ -27,19 +27,14 @@ func NewHTTPTLSClient(ctx context.Context, relayURL *url.URL, rootCAPEM []byte,
27
return nil, nil, errors.New("relay hostname is required")
28
}
29
30
- resolvedRootCAPEM := append([]byte(nil), rootCAPEM...)
31
- if len(resolvedRootCAPEM) == 0 && IsLocalRelayHost(serverName) {
32
- fetchedRootCAPEM, err := FetchEndpointCertificateChain(ctx, relayURL.String(), serverName)
30
+ var rootCAs *x509.CertPool
31
+ if IsLocalRelayHost(serverName) {
32
+ rootCAPEM, err := FetchEndpointCertificateChain(ctx, relayURL.String(), serverName)
33
if err != nil {
34
return nil, nil, fmt.Errorf("bootstrap localhost relay trust: %w", err)
35
}
36
- resolvedRootCAPEM = fetchedRootCAPEM
37
- }
38
-
39
- var rootCAs *x509.CertPool
40
- if len(resolvedRootCAPEM) > 0 {
36
rootCAs = x509.NewCertPool()
42
- if !rootCAs.AppendCertsFromPEM(resolvedRootCAPEM) {
37
+ if !rootCAs.AppendCertsFromPEM(rootCAPEM) {
38
return nil, nil, errors.New("failed to parse relay root ca")
39
}
40
}