feat(listener): refactor lease management using utils.Snapshot
Kim committed
May 27, 2026 at 15:46 UTC
3acd0bc180bae8dbea4b0b5554d0ace8acc5d398
4 files changed
+69
-30
cmd/portal-tunnel/main.go
+2
@@ -238,6 +238,8 @@ func (f exposeX402Flags) config() (*types.X402Config, error) {
238
return nil, nil
239
}
240
switch {
241
+ case strings.TrimSpace(cfg.FacilitatorURL) == "":
242
+ return nil, errors.New("--x402-facilitator-url is required when x402 is enabled")
243
case strings.TrimSpace(cfg.Network) == "":
244
return nil, errors.New("--x402-network is required when x402 is enabled")
245
case strings.TrimSpace(cfg.Price) == "":
portal/x402/x402.go
+5
-1
@@ -89,6 +89,10 @@ func NewHTTPRouteHandler(cfg HTTPRouteHandlerConfig) (http.Handler, error) {
89
if network == "" {
90
return nil, fmt.Errorf("http route %q x402 network is required", prefix)
91
}
92
+ facilitatorURL := strings.TrimSpace(cfg.X402.FacilitatorURL)
93
+ if facilitatorURL == "" {
94
+ return nil, fmt.Errorf("http route %q x402 facilitator_url is required", prefix)
95
+ }
96
priceValue := strings.TrimSpace(cfg.X402.Price)
97
if priceValue == "" && cfg.PriceResolver == nil {
98
return nil, fmt.Errorf("http route %q x402 price is required", prefix)
@@ -155,7 +159,7 @@ func NewHTTPRouteHandler(cfg HTTPRouteHandlerConfig) (http.Handler, error) {
159
},
160
},
161
Facilitator: x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{
158
- URL: strings.TrimSpace(cfg.X402.FacilitatorURL),
162
+ URL: facilitatorURL,
163
}),
164
Schemes: []x402nethttp.SchemeConfig{
165
{
sdk/listener.go
+48
-29
@@ -76,8 +76,7 @@ type listener struct {
76
77
releaseVersion string
78
79
- leaseMu sync.RWMutex
80
- lease *listenerSnapshot
79
+ lease *utils.Snapshot[listenerSnapshot]
80
}
81
82
// newListener creates one relay listener and its dedicated relay transport for one relay URL.
@@ -119,6 +118,7 @@ func newListener(ctx context.Context, route discovery.Route, cfg listenerConfig)
118
retryWait: retryWait,
119
leaseTTL: leaseTTL,
120
renewBefore: renewBefore,
121
+ lease: utils.NewSnapshot(listenerSnapshot{}, listenerSnapshot.snapshot),
122
}
123
l.mitmManager = newMITMManager(listenerCtx, l, cfg.BanMITM)
124
l.stream = transport.NewClientStream(readyTarget, handshakeTimeout)
@@ -266,11 +266,24 @@ type listenerSnapshot struct {
266
hopRoutes []types.HopRoute
267
}
268
269
+func (s listenerSnapshot) snapshot() listenerSnapshot {
270
+ s.echConfigList = bytes.Clone(s.echConfigList)
271
+ s.hopRoutes = append([]types.HopRoute(nil), s.hopRoutes...)
272
+ if s.publicURLBase != nil {
273
+ publicURLBase := *s.publicURLBase
274
+ s.publicURLBase = &publicURLBase
275
+ }
276
+ if s.tlsConfig != nil {
277
+ s.tlsConfig = s.tlsConfig.Clone()
278
+ }
279
+ return s
280
+}
281
+
282
func (l *listener) clearLease(reason string) *listenerSnapshot {
270
- l.leaseMu.Lock()
271
- lease := l.lease
272
- l.lease = nil
273
- l.leaseMu.Unlock()
283
+ if l == nil || l.lease == nil {
284
+ return nil
285
+ }
286
+ lease := l.lease.Swap(listenerSnapshot{})
287
288
if l.mitmManager != nil {
289
l.mitmManager.reset()
@@ -278,17 +291,21 @@ func (l *listener) clearLease(reason string) *listenerSnapshot {
291
if l.datagram != nil && reason != "" {
292
l.datagram.Clear(reason)
293
}
281
- return lease
294
+ if lease.accessToken == "" && lease.tlsCloser == nil {
295
+ return nil
296
+ }
297
+ return &lease
298
}
299
300
func (l *listener) leaseSnapshot() (listenerSnapshot, bool) {
285
- l.leaseMu.RLock()
286
- defer l.leaseMu.RUnlock()
287
-
288
- if l.lease == nil {
301
+ if l == nil || l.lease == nil {
302
+ return listenerSnapshot{}, false
303
+ }
304
+ lease := l.lease.Load()
305
+ if lease.accessToken == "" {
306
return listenerSnapshot{}, false
307
}
291
- return *l.lease, true
308
+ return lease, true
309
}
310
311
func (l *listener) Accept() (net.Conn, error) {
@@ -727,20 +744,25 @@ func (l *listener) renewLease(ctx context.Context) error {
744
return err
745
}
746
}
730
- l.leaseMu.Lock()
731
- if l.lease == nil || l.lease.accessToken != lease.accessToken {
732
- l.leaseMu.Unlock()
747
+ if l.lease == nil {
748
return errLeaseRefreshRequired
749
}
735
- next := *l.lease
736
- next.accessToken = resp.AccessToken
737
- next.expiresAt = resp.ExpiresAt
738
- next.multihopAccessToken = multihopAccessToken
739
- if entrySNIPort > 0 {
740
- next.sniPort = entrySNIPort
750
+ _, updated := l.lease.UpdateIf(func(current listenerSnapshot) (listenerSnapshot, bool) {
751
+ if current.accessToken != lease.accessToken {
752
+ return current, false
753
+ }
754
+ next := current
755
+ next.accessToken = resp.AccessToken
756
+ next.expiresAt = resp.ExpiresAt
757
+ next.multihopAccessToken = multihopAccessToken
758
+ if entrySNIPort > 0 {
759
+ next.sniPort = entrySNIPort
760
+ }
761
+ return next, true
762
+ })
763
+ if !updated {
764
+ return errLeaseRefreshRequired
765
}
742
- l.lease = &next
743
- l.leaseMu.Unlock()
766
return nil
767
}
768
@@ -817,7 +839,7 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
839
}
840
return ctx.Err()
841
}
820
- next := &listenerSnapshot{
842
+ next := listenerSnapshot{
843
hostname: publicHostname,
844
echConfigList: echConfigList,
845
udpAddr: resp.UDPAddr,
@@ -831,11 +853,8 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
853
multihopAccessToken: multihopAccessToken,
854
hopRoutes: hopRoutes,
855
}
834
- l.leaseMu.Lock()
835
- oldLease := l.lease
836
- l.lease = next
837
- l.leaseMu.Unlock()
838
- if oldLease != nil && oldLease.tlsCloser != nil {
856
+ oldLease := l.lease.Swap(next)
857
+ if oldLease.tlsCloser != nil {
858
_ = oldLease.tlsCloser.Close()
859
}
860
if l.udpEnabled && l.datagram != nil {
utils/snapshot.go
+14
@@ -44,6 +44,20 @@ func (s *Snapshot[T]) Store(value T) {
44
s.value.Store(&value)
45
}
46
47
+func (s *Snapshot[T]) Swap(value T) T {
48
+ if s == nil {
49
+ var zero T
50
+ return zero
51
+ }
52
+ value = s.snapshotValue(value)
53
+ previous := s.value.Swap(&value)
54
+ if previous == nil {
55
+ var zero T
56
+ return zero
57
+ }
58
+ return s.snapshotValue(*previous)
59
+}
60
+
61
func (s *Snapshot[T]) snapshotValue(value T) T {
62
if s != nil && s.snapshot != nil {
63
return s.snapshot(value)