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)