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