refactor: update lease handling methods and improve public lease functionality
Kim committed
Apr 17, 2026 at 16:16 UTC
94daa6074a42562d1b96fc14b29d00bf24ab3d7e
8 files changed
+361
-289
cmd/relay-server/admin.go
+1
-1
@@ -172,7 +172,7 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
172
if !utils.RequireMethod(w, r, http.MethodGet) {
173
return
174
}
175
- leases := f.server.AdminLeaseSnapshots()
175
+ leases := f.server.AdminLeases()
176
f.attachAutomaticAdminThumbnails(leases)
177
utils.WriteAPIData(w, http.StatusOK, types.AdminSnapshotResponse{
178
ApprovalMode: string(runtime.Approver().Mode()),
cmd/relay-server/frontend.go
+36
-23
@@ -207,12 +207,12 @@ func (f *Frontend) servePortalHTMLWithSSR(w http.ResponseWriter) {
207
}
208
209
func (f *Frontend) injectServerData(htmlContent string) string {
210
- var snapshots []types.Lease
210
+ var leases []types.Lease
211
if f.server != nil {
212
- snapshots = f.server.LeaseSnapshots()
213
- f.attachAutomaticThumbnails(snapshots)
212
+ leases = f.server.PublicLeases()
213
+ f.attachAutomaticThumbnails(leases)
214
}
215
- jsonData, err := json.Marshal(snapshots)
215
+ jsonData, err := json.Marshal(leases)
216
if err != nil {
217
jsonData = []byte("[]")
218
}
@@ -234,10 +234,10 @@ func (f *Frontend) serveTunnelStatus(w http.ResponseWriter, r *http.Request) {
234
resp := types.TunnelStatusResponse{
235
Hostname: hostname,
236
}
237
- if snapshot, ok := f.server.LeaseSnapshotByHostname(hostname); ok {
238
- resp.Hostname = snapshot.Hostname
237
+ if lease, ok := f.publicLeaseByHostname(hostname); ok {
238
+ resp.Hostname = lease.Hostname
239
resp.Registered = true
240
- resp.ServiceAlive = snapshot.Ready > 0
240
+ resp.ServiceAlive = lease.Ready > 0
241
}
242
utils.WriteAPIData(w, http.StatusOK, resp)
243
}
@@ -254,8 +254,8 @@ func (f *Frontend) serveThumbnail(w http.ResponseWriter, r *http.Request) {
254
return
255
}
256
257
- snapshot, ok := f.server.LeaseSnapshotByHostname(hostname)
258
- if !ok || snapshot.Metadata.Thumbnail != "" {
257
+ lease, ok := f.publicLeaseByHostname(hostname)
258
+ if !ok || lease.Metadata.Thumbnail != "" {
259
f.thumbnails.remove(hostname)
260
http.NotFound(w, r)
261
return
@@ -277,30 +277,43 @@ func (f *Frontend) serveThumbnail(w http.ResponseWriter, r *http.Request) {
277
_, _ = w.Write(data)
278
}
279
280
-func (f *Frontend) attachAutomaticThumbnails(leases []types.Lease) {
281
- if f == nil || f.thumbnails == nil {
282
- return
280
+func (f *Frontend) publicLeaseByHostname(hostname string) (types.Lease, bool) {
281
+ if f == nil || f.server == nil {
282
+ return types.Lease{}, false
283
}
284
- for i := range leases {
285
- if leases[i].Hostname == "" || leases[i].Metadata.Thumbnail != "" {
286
- continue
284
+ hostname = utils.NormalizeHostname(hostname)
285
+ if hostname == "" {
286
+ return types.Lease{}, false
287
+ }
288
+ for _, lease := range f.server.PublicLeases() {
289
+ if utils.HostnameMatchesPattern(lease.Hostname, hostname) {
290
+ return lease, true
291
}
288
- leases[i].Metadata.Thumbnail = types.PathThumbnailPrefix + leases[i].Hostname
289
- f.thumbnails.triggerAsync(leases[i].Hostname)
292
+ }
293
+ return types.Lease{}, false
294
+}
295
+
296
+func (f *Frontend) attachAutomaticThumbnails(leases []types.Lease) {
297
+ for i := range leases {
298
+ f.attachAutomaticThumbnail(leases[i].Hostname, &leases[i].Metadata)
299
}
300
}
301
302
func (f *Frontend) attachAutomaticAdminThumbnails(leases []types.AdminLease) {
303
+ for i := range leases {
304
+ f.attachAutomaticThumbnail(leases[i].Hostname, &leases[i].Metadata)
305
+ }
306
+}
307
+
308
+func (f *Frontend) attachAutomaticThumbnail(hostname string, metadata *types.LeaseMetadata) {
309
if f == nil || f.thumbnails == nil {
310
return
311
}
297
- for i := range leases {
298
- if leases[i].Hostname == "" || leases[i].Metadata.Thumbnail != "" {
299
- continue
300
- }
301
- leases[i].Metadata.Thumbnail = types.PathThumbnailPrefix + leases[i].Hostname
302
- f.thumbnails.triggerAsync(leases[i].Hostname)
312
+ if hostname == "" || metadata == nil || metadata.Thumbnail != "" {
313
+ return
314
}
315
+ metadata.Thumbnail = types.PathThumbnailPrefix + hostname
316
+ f.thumbnails.triggerAsync(hostname)
317
}
318
319
func (f *Frontend) injectOGMetadata(htmlContent, title, description string) string {
portal/api_server.go
+10
-12
@@ -417,12 +417,12 @@ func (s *Server) handleRenew(w http.ResponseWriter, r *http.Request) {
417
if req.TTL > 0 {
418
ttl = time.Duration(req.TTL) * time.Second
419
}
420
- record, err := s.registry.Renew(claims.Identity, ttl, clientIP, utils.SanitizeReportedIP(req.ReportedIP))
420
+ record, err := s.registry.Renew(claims.Identity.Key(), ttl, clientIP, utils.SanitizeReportedIP(req.ReportedIP))
421
if err != nil {
422
writeAPIErrorResponse(w, err)
423
return
424
}
425
- nextAccessToken, _, err := auth.IssueLeaseAccessToken(s.identity.PrivateKey, s.identity.Address, s.cfg.PortalURL, record.Copy(), ttl)
425
+ nextAccessToken, _, err := auth.IssueLeaseAccessToken(s.identity.PrivateKey, s.identity.Address, s.cfg.PortalURL, record.Identity, ttl)
426
if err != nil {
427
utils.WriteAPIError(w, http.StatusInternalServerError, types.APIErrorCodeInternal, err.Error())
428
return
@@ -449,7 +449,7 @@ func (s *Server) handleUnregister(w http.ResponseWriter, r *http.Request) {
449
return
450
}
451
452
- record, err := s.registry.Unregister(claims.Identity)
452
+ record, err := s.registry.Unregister(claims.Identity.Key())
453
if err != nil {
454
writeAPIErrorResponse(w, err)
455
return
@@ -616,7 +616,7 @@ func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) {
616
return
617
}
618
619
- s.registry.Touch(lease.Copy(), clientIP, time.Now())
619
+ s.registry.Touch(lease.Key(), clientIP, time.Now())
620
log.Info().
621
Str("address", lease.Address).
622
Str("lease_name", lease.Name).
@@ -670,7 +670,7 @@ func (s *Server) handleQUICTunnelConn(conn *quic.Conn) {
670
}
671
672
_ = json.NewEncoder(stream).Encode(types.QUICControlResponse{OK: true})
673
- s.registry.Touch(lease.Copy(), conn.RemoteAddr().String(), time.Now())
673
+ s.registry.Touch(lease.Key(), conn.RemoteAddr().String(), time.Now())
674
log.Info().
675
Str("component", "quic-tunnel-listener").
676
Str("address", lease.Address).
@@ -752,14 +752,12 @@ func (s *Server) registerLease(req types.RegisterChallengeRequest, clientIP, rep
752
record := &leaseRecord{
753
Identity: identity,
754
Hostname: hostname,
755
- Metadata: req.Metadata.Copy(),
755
+ Metadata: req.Metadata,
756
ExpiresAt: expiresAt,
757
FirstSeenAt: issuedAt,
758
LastSeenAt: issuedAt,
759
ClientIP: clientIP,
760
ReportedIP: utils.SanitizeReportedIP(reportedIP),
761
- UDPEnabled: req.UDPEnabled,
762
- TCPEnabled: req.TCPEnabled,
761
hopToken: req.HopToken,
762
stream: stream,
763
}
@@ -804,19 +802,19 @@ func (s *Server) registerLease(req types.RegisterChallengeRequest, clientIP, rep
802
syncCtx, cancel := context.WithTimeout(context.Background(), defaultClaimTimeout)
803
defer cancel()
804
if err := s.acmeManager.SyncENSGaslessHostname(syncCtx, record.Hostname, record.Address); err != nil {
807
- _, _ = s.registry.Unregister(record.Copy())
805
+ _, _ = s.registry.Unregister(record.Key())
806
record.Close()
807
return types.RegisterResponse{}, err
808
}
809
}
810
811
resp := types.RegisterResponse{
814
- Identity: record.Copy(),
812
+ Identity: record.Identity,
813
Hostname: hostname,
814
ExpiresAt: expiresAt,
815
AccessToken: accessToken,
818
- UDPEnabled: record.UDPEnabled,
819
- TCPEnabled: record.TCPEnabled,
816
+ UDPEnabled: record.datagram != nil,
817
+ TCPEnabled: record.tcpPort != nil,
818
}
819
if record.datagram != nil {
820
resp.SNIPort = s.cfg.SNIPort
portal/lease.go
+241
-221
@@ -18,12 +18,9 @@ import (
18
const defaultRegisterChallengeTTL = 2 * time.Minute
19
20
type leaseRegistry struct {
21
- leasesByKey map[string]*leaseRecord
22
- recordsByHostname map[string]*leaseRecord
23
- recordsByHopToken map[string]*leaseRecord
24
- registerChallenges map[string]*auth.RegisterChallenge
25
- policy *policy.Runtime
26
- mu sync.RWMutex
21
+ records []*leaseRecord
22
+ policy *policy.Runtime
23
+ mu sync.RWMutex
24
}
25
26
func newLeaseRegistry(udpEnabled, tcpPortEnabled bool, trustProxyHeaders bool, rawTrustedProxyCIDRs string) (*leaseRegistry, error) {
@@ -33,11 +30,8 @@ func newLeaseRegistry(udpEnabled, tcpPortEnabled bool, trustProxyHeaders bool, r
30
}
31
32
return &leaseRegistry{
36
- leasesByKey: make(map[string]*leaseRecord),
37
- recordsByHostname: make(map[string]*leaseRecord),
38
- recordsByHopToken: make(map[string]*leaseRecord),
39
- registerChallenges: make(map[string]*auth.RegisterChallenge),
40
- policy: runtime,
33
+ records: make([]*leaseRecord, 0),
34
+ policy: runtime,
35
}, nil
36
}
37
@@ -45,22 +39,13 @@ func (r *leaseRegistry) CloseAll() []*leaseRecord {
39
r.mu.Lock()
40
defer r.mu.Unlock()
41
48
- out := make([]*leaseRecord, 0, len(r.leasesByKey))
49
- seen := make(map[*leaseRecord]struct{}, len(r.leasesByKey))
50
- for _, record := range r.leasesByKey {
51
- out = append(out, record)
52
- seen[record] = struct{}{}
53
- r.policy.ForgetIdentity(record.Key())
54
- }
55
- for _, record := range r.recordsByHostname {
56
- if _, ok := seen[record]; record != nil && !ok && record.isPublicEntry() {
57
- out = append(out, record)
42
+ out := r.records
43
+ for _, record := range out {
44
+ if record != nil && record.stream != nil {
45
+ r.policy.ForgetIdentity(record.Key())
46
}
47
}
60
- r.leasesByKey = make(map[string]*leaseRecord)
61
- r.recordsByHostname = make(map[string]*leaseRecord)
62
- r.recordsByHopToken = make(map[string]*leaseRecord)
63
- r.registerChallenges = make(map[string]*auth.RegisterChallenge)
48
+ r.records = nil
49
return out
50
}
51
@@ -73,20 +58,48 @@ func (r *leaseRegistry) Lookup(host string) (*leaseRecord, bool) {
58
r.mu.RLock()
59
defer r.mu.RUnlock()
60
76
- record := r.lookupLocked(host)
77
- return record, record != nil
61
+ now := time.Now()
62
+ for _, record := range r.records {
63
+ if record == nil || !record.isPublicEntry() || record.isExpired(now) {
64
+ continue
65
+ }
66
+ if record.Hostname == host {
67
+ return record, true
68
+ }
69
+ }
70
+ for _, record := range r.records {
71
+ if record == nil || !record.isPublicEntry() || record.isExpired(now) {
72
+ continue
73
+ }
74
+ if record.Hostname != host && utils.HostnameMatchesPattern(record.Hostname, host) {
75
+ return record, true
76
+ }
77
+ }
78
+ return nil, false
79
}
80
80
-func (r *leaseRegistry) lookupLocked(host string) *leaseRecord {
81
- record := r.recordsByHostname[host]
82
- if record == nil {
83
- parts := strings.Split(host, ".")
84
- if len(parts) < 3 {
85
- return nil
81
+func (r *leaseRegistry) recordByKey(key string, now time.Time) *leaseRecord {
82
+ for _, record := range r.records {
83
+ if record == nil || record.stream == nil || record.isExpired(now) {
84
+ continue
85
+ }
86
+ if record.Key() == key {
87
+ return record
88
}
87
- record = r.recordsByHostname["*."+strings.Join(parts[1:], ".")]
89
}
89
- return record
90
+ return nil
91
+}
92
+
93
+func (r *leaseRegistry) recordByHopToken(token string, now time.Time) *leaseRecord {
94
+ for _, record := range r.records {
95
+ if record == nil || record.isExpired(now) {
96
+ continue
97
+ }
98
+ if (record.isHopMiddle() || record.isHopExit()) && record.hopToken == token {
99
+ return record
100
+ }
101
+ }
102
+ return nil
103
}
104
105
func (r *leaseRegistry) Register(record *leaseRecord) error {
@@ -108,31 +121,46 @@ func (r *leaseRegistry) Register(record *leaseRecord) error {
121
122
now := time.Now()
123
if record.isPublicEntry() {
111
- if existing := r.recordsByHostname[hostname]; existing != nil && existing.Key() != key && now.Before(existing.ExpiresAt) {
112
- r.mu.Unlock()
113
- return errHostnameConflict
124
+ for _, existing := range r.records {
125
+ if existing == nil || !existing.isPublicEntry() || existing.isExpired(now) {
126
+ continue
127
+ }
128
+ if existing.Hostname == hostname && existing.Key() != key {
129
+ r.mu.Unlock()
130
+ return errHostnameConflict
131
+ }
132
}
133
}
134
if record.isHopExit() {
117
- if existing := r.recordsByHopToken[record.hopToken]; existing != nil && existing.Key() != key && now.Before(existing.ExpiresAt) {
135
+ if existing := r.recordByHopToken(record.hopToken, now); existing != nil && existing.Key() != key {
136
r.mu.Unlock()
137
return errors.New("hop token conflict")
138
}
139
}
140
141
var replaced *leaseRecord
124
- if existing, ok := r.leasesByKey[key]; ok && existing != nil {
125
- replaced = existing
126
- r.deleteIndexesLocked(existing)
127
- r.policy.ForgetIdentity(existing.Key())
142
+ replacedIndex := -1
143
+ for i := 0; i < len(r.records); i++ {
144
+ existing := r.records[i]
145
+ if existing != nil && existing.stream == nil && existing.isPublicEntry() &&
146
+ existing.Hostname == hostname && existing.Key() == key {
147
+ r.deleteRecord(i)
148
+ i--
149
+ }
150
}
129
- record.Hostname = hostname
130
- r.leasesByKey[key] = record
131
- if record.isPublicEntry() {
132
- r.recordsByHostname[hostname] = record
151
+ for i, existing := range r.records {
152
+ if existing != nil && existing.stream != nil && existing.Key() == key {
153
+ replaced = existing
154
+ replacedIndex = i
155
+ r.policy.ForgetIdentity(existing.Key())
156
+ break
157
+ }
158
}
134
- if record.isHopExit() {
135
- r.recordsByHopToken[record.hopToken] = record
159
+ record.Hostname = hostname
160
+ if replacedIndex >= 0 {
161
+ r.records[replacedIndex] = record
162
+ } else {
163
+ r.records = append(r.records, record)
164
}
165
r.policy.IPFilter().RegisterIdentityIP(key, record.ClientIP)
166
r.mu.Unlock()
@@ -143,12 +171,12 @@ func (r *leaseRegistry) Register(record *leaseRecord) error {
171
return nil
172
}
173
146
-func (r *leaseRegistry) Renew(identity types.Identity, ttl time.Duration, clientIP, reportedIP string) (*leaseRecord, error) {
174
+func (r *leaseRegistry) Renew(key string, ttl time.Duration, clientIP, reportedIP string) (*leaseRecord, error) {
175
r.mu.Lock()
176
defer r.mu.Unlock()
177
150
- record, ok := r.leasesByKey[identity.Key()]
151
- if !ok {
178
+ record := r.recordByKey(key, time.Time{})
179
+ if record == nil {
180
return nil, errLeaseNotFound
181
}
182
@@ -166,20 +194,20 @@ func (r *leaseRegistry) Renew(identity types.Identity, ttl time.Duration, client
194
return record, nil
195
}
196
169
-func (r *leaseRegistry) Unregister(identity types.Identity) (*leaseRecord, error) {
197
+func (r *leaseRegistry) Unregister(key string) (*leaseRecord, error) {
198
r.mu.Lock()
199
defer r.mu.Unlock()
200
173
- key := identity.Key()
174
- record, ok := r.leasesByKey[key]
175
- if !ok {
176
- return nil, errLeaseNotFound
201
+ key = strings.TrimSpace(key)
202
+ for i, record := range r.records {
203
+ if record == nil || record.stream == nil || record.Key() != key {
204
+ continue
205
+ }
206
+ r.deleteRecord(i)
207
+ r.policy.ForgetIdentity(key)
208
+ return record, nil
209
}
178
-
179
- delete(r.leasesByKey, key)
180
- r.deleteIndexesLocked(record)
181
- r.policy.ForgetIdentity(key)
182
- return record, nil
210
+ return nil, errLeaseNotFound
211
}
212
213
func (r *leaseRegistry) RecordByKey(key string, now time.Time) (*leaseRecord, bool) {
@@ -191,8 +219,8 @@ func (r *leaseRegistry) RecordByKey(key string, now time.Time) (*leaseRecord, bo
219
r.mu.RLock()
220
defer r.mu.RUnlock()
221
194
- record := r.leasesByKey[key]
195
- if record == nil || now.After(record.ExpiresAt) {
222
+ record := r.recordByKey(key, now)
223
+ if record == nil {
224
return nil, false
225
}
226
return record, true
@@ -207,8 +235,8 @@ func (r *leaseRegistry) RecordByHopToken(token string, now time.Time) (*leaseRec
235
r.mu.RLock()
236
defer r.mu.RUnlock()
237
210
- record := r.recordsByHopToken[token]
211
- if record != nil && now.Before(record.ExpiresAt) {
238
+ record := r.recordByHopToken(token, now)
239
+ if record != nil {
240
return record, true
241
}
242
return nil, false
@@ -242,37 +270,63 @@ func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) (
270
case forwardToken == "":
271
return nil, errors.New("forward token is required")
272
}
273
+ name := matchHostname
274
+ if label, _, ok := strings.Cut(matchHostname, "."); ok {
275
+ name = label
276
+ }
277
278
r.mu.Lock()
279
defer r.mu.Unlock()
280
281
record := &leaseRecord{
282
Identity: types.Identity{
283
+ Name: name,
284
Address: ownerKey,
285
},
286
Hostname: matchHostname,
287
ExpiresAt: expiresAt,
255
- hopOwnerKey: ownerKey,
288
hopToken: matchToken,
289
hopNextOverlayIPv4: overlayIPv4,
290
hopNextToken: forwardToken,
291
}
292
switch {
293
case record.isPublicEntry():
262
- if existing := r.lookupLocked(record.Hostname); existing != nil && now.Before(existing.ExpiresAt) {
263
- if !existing.isPublicEntry() || !sameHopRouteOwner(existing, record) {
294
+ for _, existing := range r.records {
295
+ if existing == nil || !existing.isPublicEntry() || existing.isExpired(now) {
296
+ continue
297
+ }
298
+ if existing.Hostname != record.Hostname {
299
+ continue
300
+ }
301
+ if existing.stream != nil || !strings.EqualFold(existing.Address, record.Address) {
302
return nil, errHostnameConflict
303
}
304
}
267
- r.recordsByHostname[record.Hostname] = record
305
+ for i, existing := range r.records {
306
+ if existing != nil && existing.stream == nil && existing.isPublicEntry() &&
307
+ existing.Hostname == record.Hostname &&
308
+ strings.EqualFold(existing.Address, record.Address) {
309
+ r.records[i] = record
310
+ return record, nil
311
+ }
312
+ }
313
+ r.records = append(r.records, record)
314
return record, nil
315
case record.isHopMiddle():
270
- if existing := r.recordsByHopToken[record.hopToken]; existing != nil && now.Before(existing.ExpiresAt) {
271
- if !existing.isHopMiddle() || !sameHopRouteOwner(existing, record) {
316
+ if existing := r.recordByHopToken(record.hopToken, now); existing != nil {
317
+ if !existing.isHopMiddle() || !strings.EqualFold(existing.Address, record.Address) {
318
return nil, errors.New("hop token conflict")
319
}
320
}
275
- r.recordsByHopToken[record.hopToken] = record
321
+ for i, existing := range r.records {
322
+ if existing != nil && existing.isHopMiddle() &&
323
+ existing.hopToken == record.hopToken &&
324
+ strings.EqualFold(existing.Address, record.Address) {
325
+ r.records[i] = record
326
+ return record, nil
327
+ }
328
+ }
329
+ r.records = append(r.records, record)
330
return record, nil
331
default:
332
return nil, errors.New("invalid hop route")
@@ -289,23 +343,29 @@ func (r *leaseRegistry) DeleteHopRoute(route *types.HopRoute) *leaseRecord {
343
}
344
hostname := utils.NormalizeHostname(route.MatchHostname)
345
token := strings.TrimSpace(route.MatchToken)
292
- candidate := &leaseRecord{
293
- hopOwnerKey: ownerKey,
294
- hopNextToken: strings.TrimSpace(route.ForwardToken),
295
- }
346
347
var deleted *leaseRecord
348
r.mu.Lock()
299
- if hostname != "" {
300
- if record := r.recordsByHostname[hostname]; record != nil && record.isPublicEntry() && sameHopRouteOwner(record, candidate) {
301
- delete(r.recordsByHostname, hostname)
302
- deleted = record
349
+ for i := 0; i < len(r.records); i++ {
350
+ record := r.records[i]
351
+ if record == nil || record.stream != nil {
352
+ continue
353
}
304
- }
305
- if token != "" {
306
- if record := r.recordsByHopToken[token]; record != nil && record.isHopMiddle() && sameHopRouteOwner(record, candidate) {
307
- delete(r.recordsByHopToken, token)
354
+ deleteRecord := false
355
+ if hostname != "" {
356
+ deleteRecord = record.isPublicEntry() &&
357
+ record.Hostname == hostname &&
358
+ strings.EqualFold(record.Address, ownerKey)
359
+ }
360
+ if token != "" {
361
+ deleteRecord = record.isHopMiddle() &&
362
+ record.hopToken == token &&
363
+ strings.EqualFold(record.Address, ownerKey)
364
+ }
365
+ if deleteRecord {
366
deleted = record
367
+ r.deleteRecord(i)
368
+ break
369
}
370
}
371
r.mu.Unlock()
@@ -340,7 +400,10 @@ func (r *leaseRegistry) issueRegisterChallenge(req types.RegisterChallengeReques
400
}
401
402
r.mu.Lock()
343
- r.registerChallenges[challenge.ChallengeID] = challenge
403
+ r.records = append(r.records, &leaseRecord{
404
+ ExpiresAt: challenge.ExpiresAt,
405
+ registerChallenge: challenge,
406
+ })
407
r.mu.Unlock()
408
409
return types.RegisterChallengeResponse{
@@ -360,28 +423,31 @@ func (r *leaseRegistry) consumeVerifiedRegisterChallenge(req types.RegisterReque
423
r.mu.Lock()
424
defer r.mu.Unlock()
425
363
- challenge := r.registerChallenges[challengeID]
364
- if challenge == nil {
365
- return nil, auth.ErrRegisterChallengeNotFound
366
- }
367
- if challenge.Expired(now) {
368
- delete(r.registerChallenges, challengeID)
369
- return nil, auth.ErrRegisterChallengeExpired
370
- }
371
- if err := challenge.Verify(req, now); err != nil {
372
- return nil, err
373
- }
426
+ for i, record := range r.records {
427
+ if record == nil || record.registerChallenge == nil || record.registerChallenge.ChallengeID != challengeID {
428
+ continue
429
+ }
430
+ challenge := record.registerChallenge
431
+ if challenge.Expired(now) {
432
+ r.deleteRecord(i)
433
+ return nil, auth.ErrRegisterChallengeExpired
434
+ }
435
+ if err := challenge.Verify(req, now); err != nil {
436
+ return nil, err
437
+ }
438
375
- delete(r.registerChallenges, challengeID)
376
- return challenge, nil
439
+ r.deleteRecord(i)
440
+ return challenge, nil
441
+ }
442
+ return nil, auth.ErrRegisterChallengeNotFound
443
}
444
379
-func (r *leaseRegistry) Touch(identity types.Identity, clientIP string, now time.Time) {
445
+func (r *leaseRegistry) Touch(key, clientIP string, now time.Time) {
446
r.mu.Lock()
447
defer r.mu.Unlock()
448
383
- record, ok := r.leasesByKey[identity.Key()]
384
- if !ok {
449
+ record := r.recordByKey(key, now)
450
+ if record == nil {
451
return
452
}
453
record.LastSeenAt = now
@@ -395,32 +461,18 @@ func (r *leaseRegistry) cleanupExpired(now time.Time) []*leaseRecord {
461
r.mu.Lock()
462
defer r.mu.Unlock()
463
398
- expired := make([]*leaseRecord, 0)
399
- for key, record := range r.leasesByKey {
400
- if now.After(record.ExpiresAt) {
464
+ var expired []*leaseRecord
465
+ for i := 0; i < len(r.records); {
466
+ record := r.records[i]
467
+ if record != nil && record.isExpired(now) {
468
expired = append(expired, record)
402
- delete(r.leasesByKey, key)
403
- r.deleteIndexesLocked(record)
404
- r.policy.ForgetIdentity(key)
405
- }
406
- }
407
- for challengeID, challenge := range r.registerChallenges {
408
- if challenge.Expired(now) {
409
- delete(r.registerChallenges, challengeID)
410
- }
411
- }
412
- for hostname, record := range r.recordsByHostname {
413
- if record != nil && !now.Before(record.ExpiresAt) {
414
- delete(r.recordsByHostname, hostname)
415
- if record.isPublicEntry() {
416
- expired = append(expired, record)
469
+ if record.stream != nil {
470
+ r.policy.ForgetIdentity(record.Key())
471
}
472
+ r.deleteRecord(i)
473
+ continue
474
}
419
- }
420
- for token, record := range r.recordsByHopToken {
421
- if record != nil && !now.Before(record.ExpiresAt) {
422
- delete(r.recordsByHopToken, token)
423
- }
475
+ i++
476
}
477
return expired
478
}
@@ -431,8 +483,8 @@ func (r *leaseRegistry) countDatagramLeases() int {
483
484
now := time.Now()
485
count := 0
434
- for _, record := range r.leasesByKey {
435
- if now.Before(record.ExpiresAt) && record.datagram != nil {
486
+ for _, record := range r.records {
487
+ if record != nil && !record.isExpired(now) && record.datagram != nil {
488
count++
489
}
490
}
@@ -445,82 +497,93 @@ func (r *leaseRegistry) countTCPPortLeases() int {
497
498
now := time.Now()
499
count := 0
448
- for _, record := range r.leasesByKey {
449
- if now.Before(record.ExpiresAt) && record.tcpPort != nil {
500
+ for _, record := range r.records {
501
+ if record != nil && !record.isExpired(now) && record.tcpPort != nil {
502
count++
503
}
504
}
505
return count
506
}
507
456
-func (r *leaseRegistry) LeaseSnapshots(now time.Time) []types.Lease {
508
+func (r *leaseRegistry) PublicLeases(now time.Time) []types.Lease {
509
r.mu.RLock()
510
defer r.mu.RUnlock()
511
460
- snapshots := make([]types.Lease, 0, len(r.leasesByKey)+len(r.recordsByHostname))
461
- seenHostnames := make(map[string]struct{}, len(r.leasesByKey))
462
- for _, record := range r.leasesByKey {
463
- if record == nil || !record.isPublicEntry() || now.After(record.ExpiresAt) {
464
- continue
465
- }
466
- adminSnapshot := r.AdminSnapshot(record)
467
- since := time.Duration(0)
468
- if !adminSnapshot.LastSeenAt.IsZero() {
469
- since = max(now.Sub(adminSnapshot.LastSeenAt), 0)
470
- }
471
- if adminSnapshot.IsBanned || adminSnapshot.IsDenied || !adminSnapshot.IsApproved || adminSnapshot.Metadata.Hide {
472
- continue
473
- }
474
- if adminSnapshot.Ready == 0 && since >= 3*time.Minute {
475
- continue
476
- }
477
- snapshots = append(snapshots, adminSnapshot.Lease)
478
- seenHostnames[adminSnapshot.Hostname] = struct{}{}
479
- }
480
- for hostname, record := range r.recordsByHostname {
481
- if record == nil || !record.isPublicEntry() || hostname == "" || now.After(record.ExpiresAt) {
512
+ leases := make([]types.Lease, 0, len(r.records))
513
+ for _, record := range r.records {
514
+ if record == nil || !record.isPublicEntry() || record.isExpired(now) {
515
continue
516
}
484
- if _, ok := seenHostnames[record.Hostname]; ok {
485
- continue
517
+ if record.stream != nil {
518
+ identityKey := record.Key()
519
+ if r.policy.IsIdentityBanned(identityKey) || r.policy.IsIdentityDenied(identityKey) || !r.policy.EffectiveApproval(identityKey) || record.Metadata.Hide {
520
+ continue
521
+ }
522
+ since := time.Duration(0)
523
+ if !record.LastSeenAt.IsZero() {
524
+ since = max(now.Sub(record.LastSeenAt), 0)
525
+ }
526
+ if record.stream.ReadyCount() == 0 && since >= 3*time.Minute {
527
+ continue
528
+ }
529
}
487
- snapshots = append(snapshots, r.Snapshot(record))
530
+ leases = append(leases, r.publicLease(record))
531
}
489
- return snapshots
532
+ return leases
533
}
534
492
-func (r *leaseRegistry) AdminLeaseSnapshots(now time.Time) []types.AdminLease {
535
+func (r *leaseRegistry) AdminLeases(now time.Time) []types.AdminLease {
536
r.mu.RLock()
537
defer r.mu.RUnlock()
538
496
- snapshots := make([]types.AdminLease, 0, len(r.leasesByKey))
497
- for _, record := range r.leasesByKey {
498
- if now.After(record.ExpiresAt) {
539
+ leases := make([]types.AdminLease, 0, len(r.records))
540
+ for _, record := range r.records {
541
+ if record == nil || record.stream == nil || record.isExpired(now) {
542
continue
543
}
501
- snapshots = append(snapshots, r.AdminSnapshot(record))
502
- }
503
- return snapshots
544
+ clientIP := record.ClientIP
545
+ identityKey := record.Key()
546
+ leases = append(leases, types.AdminLease{
547
+ Lease: r.publicLease(record),
548
+ IdentityKey: identityKey,
549
+ Address: record.Address,
550
+ BPS: r.policy.BPSManager().IdentityBPS(identityKey),
551
+ ClientIP: clientIP,
552
+ ReportedIP: record.ReportedIP,
553
+ IsApproved: r.policy.EffectiveApproval(identityKey),
554
+ IsBanned: r.policy.IsIdentityBanned(identityKey),
555
+ IsDenied: r.policy.IsIdentityDenied(identityKey),
556
+ IsIPBanned: r.policy.IPFilter().IsIPBanned(clientIP),
557
+ })
558
+ }
559
+ return leases
560
+}
561
+
562
+func (r *leaseRegistry) deleteRecord(i int) {
563
+ last := len(r.records) - 1
564
+ r.records[i] = r.records[last]
565
+ r.records[last] = nil
566
+ r.records = r.records[:last]
567
}
568
506
-func (r *leaseRegistry) Snapshot(record *leaseRecord) types.Lease {
507
- snapshot := types.Lease{
569
+func (r *leaseRegistry) publicLease(record *leaseRecord) types.Lease {
570
+ lease := types.Lease{
571
Name: record.Name,
572
ExpiresAt: record.ExpiresAt,
573
FirstSeenAt: record.FirstSeenAt,
574
LastSeenAt: record.LastSeenAt,
575
Hostname: record.Hostname,
513
- UDPEnabled: record.UDPEnabled,
514
- TCPEnabled: record.TCPEnabled,
576
+ UDPEnabled: record.datagram != nil,
577
+ TCPEnabled: record.tcpPort != nil,
578
Metadata: record.Metadata.Copy(),
579
}
580
if record.tcpPort != nil {
518
- snapshot.TCPAddr = fmt.Sprintf("%s:%d", record.Hostname, record.tcpPort.TCPPort())
581
+ lease.TCPAddr = fmt.Sprintf("%s:%d", record.Hostname, record.tcpPort.TCPPort())
582
}
583
if record.stream != nil {
521
- snapshot.Ready = record.stream.ReadyCount()
584
+ lease.Ready = record.stream.ReadyCount()
585
}
523
- return snapshot
586
+ return lease
587
}
588
589
type leaseRecord struct {
@@ -531,14 +594,12 @@ type leaseRecord struct {
594
ClientIP string
595
ReportedIP string
596
Hostname string
534
- UDPEnabled bool
535
- TCPEnabled bool
597
Metadata types.LeaseMetadata
598
599
hopToken string
539
- hopOwnerKey string
600
hopNextOverlayIPv4 string
601
hopNextToken string
602
+ registerChallenge *auth.RegisterChallenge
603
604
datagram *transport.RelayDatagram
605
udpPorts *transport.PortAllocator
@@ -550,71 +611,30 @@ type leaseRecord struct {
611
}
612
613
func (r *leaseRecord) isPublicEntry() bool {
553
- return r != nil && strings.TrimSpace(r.Hostname) != "" && strings.TrimSpace(r.hopToken) == ""
614
+ return r != nil && r.Hostname != "" && r.hopToken == ""
615
}
616
617
func (r *leaseRecord) isHopMiddle() bool {
618
_, _, hasNextHop := r.nextHop()
558
- return r != nil && strings.TrimSpace(r.hopToken) != "" && hasNextHop
619
+ return r != nil && r.Hostname == "" && r.hopToken != "" && hasNextHop
620
}
621
622
func (r *leaseRecord) isHopExit() bool {
623
_, _, hasNextHop := r.nextHop()
563
- return r != nil && strings.TrimSpace(r.hopToken) != "" && !hasNextHop
624
+ return r != nil && r.Hostname != "" && r.hopToken != "" && !hasNextHop
625
}
626
627
func (r *leaseRecord) nextHop() (string, string, bool) {
628
if r == nil {
629
return "", "", false
630
}
570
- overlayIPv4 := strings.TrimSpace(r.hopNextOverlayIPv4)
571
- forwardToken := strings.TrimSpace(r.hopNextToken)
631
+ overlayIPv4 := r.hopNextOverlayIPv4
632
+ forwardToken := r.hopNextToken
633
return overlayIPv4, forwardToken, overlayIPv4 != "" || forwardToken != ""
634
}
635
575
-func sameHopRouteOwner(existing, next *leaseRecord) bool {
576
- if existing == nil || next == nil {
577
- return false
578
- }
579
- if strings.TrimSpace(existing.hopOwnerKey) == strings.TrimSpace(next.hopOwnerKey) {
580
- return true
581
- }
582
- return strings.TrimSpace(existing.hopNextToken) != "" && strings.TrimSpace(existing.hopNextToken) == strings.TrimSpace(next.hopNextToken)
583
-}
584
-
585
-func (r *leaseRegistry) deleteIndexesLocked(record *leaseRecord) {
586
- if record == nil {
587
- return
588
- }
589
- hostname := utils.NormalizeHostname(record.Hostname)
590
- if hostname != "" {
591
- if r.recordsByHostname[hostname] == record {
592
- delete(r.recordsByHostname, hostname)
593
- }
594
- }
595
- token := strings.TrimSpace(record.hopToken)
596
- if token != "" {
597
- if r.recordsByHopToken[token] == record {
598
- delete(r.recordsByHopToken, token)
599
- }
600
- }
601
-}
602
-
603
-func (r *leaseRegistry) AdminSnapshot(record *leaseRecord) types.AdminLease {
604
- clientIP := record.ClientIP
605
- identityKey := record.Key()
606
- return types.AdminLease{
607
- Lease: r.Snapshot(record),
608
- IdentityKey: identityKey,
609
- Address: record.Address,
610
- BPS: r.policy.BPSManager().IdentityBPS(identityKey),
611
- ClientIP: clientIP,
612
- ReportedIP: record.ReportedIP,
613
- IsApproved: r.policy.EffectiveApproval(identityKey),
614
- IsBanned: r.policy.IsIdentityBanned(identityKey),
615
- IsDenied: r.policy.IsIdentityDenied(identityKey),
616
- IsIPBanned: r.policy.IPFilter().IsIPBanned(clientIP),
617
- }
636
+func (r *leaseRecord) isExpired(now time.Time) bool {
637
+ return r != nil && !now.IsZero() && !now.Before(r.ExpiresAt)
638
}
639
640
func (r *leaseRecord) Start() error {
portal/lease_test.go
+43
-9
@@ -45,7 +45,7 @@ func TestLeaseRegistryLifecycle(t *testing.T) {
45
t.Fatalf("Lookup() = %v, %v, want registered lease", lookedUp, ok)
46
}
47
48
- renewed, err := registry.Renew(record.Copy(), time.Minute, "203.0.113.10", "")
48
+ renewed, err := registry.Renew(record.Key(), time.Minute, "203.0.113.10", "")
49
if err != nil {
50
t.Fatalf("Renew() error = %v", err)
51
}
@@ -56,7 +56,7 @@ func TestLeaseRegistryLifecycle(t *testing.T) {
56
t.Fatalf("Renew() did not register client IP for lease")
57
}
58
59
- removed, err := registry.Unregister(record.Copy())
59
+ removed, err := registry.Unregister(record.Key())
60
if err != nil {
61
t.Fatalf("Unregister() error = %v", err)
62
}
@@ -111,7 +111,7 @@ func TestLeaseRegistryWildcardAndConflict(t *testing.T) {
111
}
112
}
113
114
-func TestLeaseRegistrySnapshotAndRoutableUsePolicy(t *testing.T) {
114
+func TestLeaseRegistryAdminLeasesAndRoutableUsePolicy(t *testing.T) {
115
t.Parallel()
116
117
registry := newTestRegistry(t)
@@ -137,9 +137,12 @@ func TestLeaseRegistrySnapshotAndRoutableUsePolicy(t *testing.T) {
137
t.Fatal("policy.IsIdentityRoutable() = true, want false before approval")
138
}
139
140
- snapshot := registry.AdminSnapshot(record)
141
- if snapshot.IsApproved {
142
- t.Fatal("AdminSnapshot().IsApproved = true, want false before approval")
140
+ leases := registry.AdminLeases(time.Now())
141
+ if len(leases) != 1 {
142
+ t.Fatalf("AdminLeases() length = %d, want 1", len(leases))
143
+ }
144
+ if leases[0].IsApproved {
145
+ t.Fatal("AdminLeases()[0].IsApproved = true, want false before approval")
146
}
147
if got := runtime.IPFilter().IdentityIP(record.Key()); got != "203.0.113.20" {
148
t.Fatalf("Register() lease IP = %q, want %q", got, "203.0.113.20")
@@ -150,9 +153,40 @@ func TestLeaseRegistrySnapshotAndRoutableUsePolicy(t *testing.T) {
153
t.Fatal("policy.IsIdentityRoutable() = false, want true after approval")
154
}
155
153
- snapshot = registry.AdminSnapshot(record)
154
- if !snapshot.IsApproved {
155
- t.Fatal("AdminSnapshot().IsApproved = false, want true after approval")
156
+ leases = registry.AdminLeases(time.Now())
157
+ if len(leases) != 1 {
158
+ t.Fatalf("AdminLeases() length = %d, want 1", len(leases))
159
+ }
160
+ if !leases[0].IsApproved {
161
+ t.Fatal("AdminLeases()[0].IsApproved = false, want true after approval")
162
+ }
163
+}
164
+
165
+func TestLeaseRegistryPublicLeasesIncludesIngressRouteInManualApproval(t *testing.T) {
166
+ t.Parallel()
167
+
168
+ registry := newTestRegistry(t)
169
+ if err := registry.policy.Approver().SetMode(policy.ModeManual); err != nil {
170
+ t.Fatalf("SetMode() error = %v", err)
171
+ }
172
+ route := &leaseRecord{
173
+ Identity: types.Identity{
174
+ Name: "demo",
175
+ Address: "addr-ingress",
176
+ },
177
+ Hostname: "demo.example.com",
178
+ ExpiresAt: time.Now().Add(30 * time.Second),
179
+ }
180
+ if err := registry.Register(route); err != nil {
181
+ t.Fatalf("Register() error = %v", err)
182
+ }
183
+
184
+ leases := registry.PublicLeases(time.Now())
185
+ if len(leases) != 1 {
186
+ t.Fatalf("PublicLeases() length = %d, want 1", len(leases))
187
+ }
188
+ if leases[0].Hostname != route.Hostname {
189
+ t.Fatalf("PublicLeases()[0].Hostname = %q, want %q", leases[0].Hostname, route.Hostname)
190
}
191
}
192
portal/server.go
+5
-18
@@ -384,30 +384,18 @@ func (s *Server) PortalURL() string {
384
return s.cfg.PortalURL
385
}
386
387
-func (s *Server) LeaseSnapshots() []types.Lease {
387
+func (s *Server) PublicLeases() []types.Lease {
388
if s == nil || s.registry == nil {
389
return nil
390
}
391
- return s.registry.LeaseSnapshots(time.Now())
391
+ return s.registry.PublicLeases(time.Now())
392
}
393
394
-func (s *Server) AdminLeaseSnapshots() []types.AdminLease {
394
+func (s *Server) AdminLeases() []types.AdminLease {
395
if s == nil || s.registry == nil {
396
return nil
397
}
398
- return s.registry.AdminLeaseSnapshots(time.Now())
399
-}
400
-
401
-func (s *Server) LeaseSnapshotByHostname(hostname string) (types.Lease, bool) {
402
- if s == nil || s.registry == nil {
403
- return types.Lease{}, false
404
- }
405
-
406
- record, ok := s.registry.Lookup(hostname)
407
- if !ok || record == nil || time.Now().After(record.ExpiresAt) {
408
- return types.Lease{}, false
409
- }
410
- return s.registry.Snapshot(record), true
398
+ return s.registry.AdminLeases(time.Now())
399
}
400
401
func (s *Server) prepareAPITLS(ctx context.Context) (keyless.TLSMaterialConfig, *acme.Manager, error) {
@@ -530,8 +518,7 @@ func (s *Server) runHopMux(ctx context.Context) error {
518
_ = stream.Conn.Close()
519
return
520
}
533
- _, _, hasNextHop := record.nextHop()
534
- log.Info().Str("remote_addr", stream.RemoteAddr).Bool("forward", hasNextHop).Msg("hop stream received")
521
+ log.Info().Str("remote_addr", stream.RemoteAddr).Bool("forward", record.isHopMiddle()).Msg("hop stream received")
522
if err := s.bridgeLeaseConn(groupCtx, stream.Conn, record); err != nil {
523
log.Warn().Err(err).Str("remote_addr", stream.RemoteAddr).Msg("hop stream bridge failed")
524
_ = stream.Conn.Close()
portal/server_test.go
+5
-5
@@ -345,12 +345,12 @@ func TestRegisterLeaseDerivesFixedHostnameFromName(t *testing.T) {
345
if !ok {
346
t.Fatal("registry.RecordByKey() = false, want registered lease")
347
}
348
- snapshot := server.registry.Snapshot(record)
349
- if snapshot.Name != "demo-app" {
350
- t.Fatalf("Snapshot().Name = %q, want %q", snapshot.Name, "demo-app")
348
+ lease := server.registry.publicLease(record)
349
+ if lease.Name != "demo-app" {
350
+ t.Fatalf("publicLease().Name = %q, want %q", lease.Name, "demo-app")
351
}
352
- if snapshot.Hostname != wantHostname {
353
- t.Fatalf("Snapshot().Hostname = %q, want %q", snapshot.Hostname, wantHostname)
352
+ if lease.Hostname != wantHostname {
353
+ t.Fatalf("publicLease().Hostname = %q, want %q", lease.Hostname, wantHostname)
354
}
355
}
356
utils/utils.go
+20
@@ -208,6 +208,26 @@ func HostnameMatchesBaseDomain(hostname, baseDomain string) bool {
208
return hostname == baseDomain || strings.HasSuffix(hostname, "."+baseDomain)
209
}
210
211
+func HostnameMatchesPattern(pattern, hostname string) bool {
212
+ pattern = NormalizeHostname(pattern)
213
+ hostname = NormalizeHostname(hostname)
214
+ if pattern == "" || hostname == "" {
215
+ return false
216
+ }
217
+ if pattern == hostname {
218
+ return true
219
+ }
220
+ if !strings.HasPrefix(pattern, "*.") {
221
+ return false
222
+ }
223
+ suffix := strings.TrimPrefix(pattern, "*.")
224
+ if !strings.Contains(suffix, ".") {
225
+ return false
226
+ }
227
+ _, rest, ok := strings.Cut(hostname, ".")
228
+ return ok && rest == suffix
229
+}
230
+
231
func NormalizeChildHostnames(inputs []string, baseDomain string) []string {
232
if len(inputs) == 0 {
233
return nil