main
go 964 lines 27.6 KB
Raw
1 package portal
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "net"
9 "net/http"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/rs/zerolog/log"
15
16 "github.com/gosuda/portal-tunnel/v2/portal/acme"
17 "github.com/gosuda/portal-tunnel/v2/portal/auth"
18 "github.com/gosuda/portal-tunnel/v2/portal/identity"
19 "github.com/gosuda/portal-tunnel/v2/portal/keyless"
20 "github.com/gosuda/portal-tunnel/v2/portal/policy"
21 "github.com/gosuda/portal-tunnel/v2/portal/transport"
22 "github.com/gosuda/portal-tunnel/v2/types"
23 "github.com/gosuda/portal-tunnel/v2/utils"
24 )
25
26 const (
27 defaultLeaseTTL = 2 * time.Minute
28 defaultRegisterChallengeTTL = 2 * time.Minute
29 defaultRegisterChallengeOutstandingPerIP = 32
30 defaultPortReservationGrace = 5 * time.Minute
31 defaultIdleKeepalive = 15 * time.Second
32 defaultReadyQueueLimit = 8
33 )
34
35 type leaseRegistry struct {
36 records []*leaseRecord
37 rootHostname string
38 sniPort int
39 tokenAuthority identity.Authority
40 tokenIssuer string
41 policy *policy.Runtime
42 udpPorts *transport.PortAllocator
43 tcpPorts *transport.PortAllocator
44 proxy *proxy
45 mu sync.RWMutex
46 }
47
48 func newLeaseRegistry(udpEnabled, tcpPortEnabled bool, minPort, maxPort int, rootHostname string, sniPort int, tokenAuthority identity.Authority, tokenIssuer string, trustProxyHeaders bool, rawTrustedProxyCIDRs string) (*leaseRegistry, error) {
49 if tokenAuthority == nil {
50 return nil, errors.New("lease token authority is required")
51 }
52 tokenIdentity := tokenAuthority.Identity()
53 if strings.TrimSpace(tokenIdentity.PublicKey) == "" {
54 return nil, errors.New("lease token authority public key is required")
55 }
56 runtime, err := policy.NewRuntime(udpEnabled, tcpPortEnabled, trustProxyHeaders, rawTrustedProxyCIDRs)
57 if err != nil {
58 return nil, err
59 }
60
61 return &leaseRegistry{
62 records: make([]*leaseRecord, 0),
63 rootHostname: utils.NormalizeHostname(rootHostname),
64 sniPort: sniPort,
65 tokenAuthority: tokenAuthority,
66 tokenIssuer: tokenIssuer,
67 policy: runtime,
68 udpPorts: transport.NewPortAllocator(minPort, maxPort, defaultPortReservationGrace),
69 tcpPorts: transport.NewPortAllocator(minPort, maxPort, defaultPortReservationGrace),
70 proxy: &proxy{},
71 }, nil
72 }
73
74 func (r *leaseRegistry) CloseAll() []*leaseRecord {
75 r.mu.Lock()
76 out := r.records
77 for _, record := range out {
78 if record != nil && record.stream != nil {
79 r.policy.ForgetIdentity(record.Key())
80 }
81 }
82 r.records = nil
83 r.mu.Unlock()
84
85 for _, record := range out {
86 record.Close()
87 }
88 return out
89 }
90
91 func (r *leaseRegistry) Lookup(host string) (*leaseRecord, bool) {
92 host = utils.NormalizeHostname(host)
93 if host == "" {
94 return nil, false
95 }
96
97 r.mu.RLock()
98 defer r.mu.RUnlock()
99
100 now := time.Now()
101 for _, record := range r.records {
102 if record == nil || !record.isPublicEntry() || record.isExpired(now) {
103 continue
104 }
105 if record.Hostname == host {
106 return record, true
107 }
108 }
109 hostHash := utils.HostnameHash(host)
110 for _, record := range r.records {
111 if record == nil || !record.isPublicEntry() || record.isExpired(now) {
112 continue
113 }
114 if record.HostnameHash != "" && record.HostnameHash == hostHash {
115 return record, true
116 }
117 }
118 for _, record := range r.records {
119 if record == nil || !record.isPublicEntry() || record.isExpired(now) {
120 continue
121 }
122 if record.Hostname != host && utils.HostnameMatchesPattern(record.Hostname, host) {
123 return record, true
124 }
125 }
126 return nil, false
127 }
128
129 func (r *leaseRegistry) recordByKey(key string, now time.Time) *leaseRecord {
130 for _, record := range r.records {
131 if record == nil || record.stream == nil || record.isExpired(now) {
132 continue
133 }
134 if record.Key() == key {
135 return record
136 }
137 }
138 return nil
139 }
140
141 func (r *leaseRegistry) recordByHopToken(token string, now time.Time) *leaseRecord {
142 for _, record := range r.records {
143 if record == nil || record.isExpired(now) {
144 continue
145 }
146 if (record.isHopMiddle() || record.isHopExit()) && record.hopToken == token {
147 return record
148 }
149 }
150 return nil
151 }
152
153 func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, reportedIP string) (*leaseRecord, types.RegisterResponse, error) {
154 if r == nil {
155 return nil, types.RegisterResponse{}, errFeatureUnavailable
156 }
157 leaseIdentity, err := identity.NormalizeIdentity(req.Identity)
158 if err != nil {
159 return nil, types.RegisterResponse{}, err
160 }
161 if r.policy.IPFilter().IsIPBanned(clientIP) {
162 return nil, types.RegisterResponse{}, errIPBanned
163 }
164
165 ttl := defaultLeaseTTL
166 if req.TTL > 0 {
167 ttl = time.Duration(req.TTL) * time.Second
168 }
169
170 identityKey := leaseIdentity.Key()
171 hopToken := strings.TrimSpace(req.HopToken)
172 routeHostname := utils.NormalizeHostname(req.RouteHostname)
173 hostnameHash := strings.TrimSpace(req.HostnameHash)
174 echConfigList := bytes.Clone(req.ECHConfigList)
175 if hopToken != "" && (req.UDPEnabled || req.TCPEnabled) {
176 return nil, types.RegisterResponse{}, errTransportMismatch
177 }
178 if (routeHostname != "" || hostnameHash != "") && (hopToken != "" || req.UDPEnabled || req.TCPEnabled) {
179 return nil, types.RegisterResponse{}, errTransportMismatch
180 }
181 if hostnameHash != "" && routeHostname == "" {
182 return nil, types.RegisterResponse{}, errors.New("hostname hash requires route hostname")
183 }
184 if len(echConfigList) > 0 && routeHostname == "" {
185 return nil, types.RegisterResponse{}, errors.New("ech config list requires route hostname")
186 }
187 publicHostname := ""
188 if routeHostname != "" {
189 routeLabel, routeBase, ok := strings.Cut(routeHostname, ".")
190 normalizedRouteLabel, labelErr := utils.NormalizeDNSLabel(routeLabel)
191 if !ok || labelErr != nil || normalizedRouteLabel != routeLabel || routeBase != r.rootHostname {
192 return nil, types.RegisterResponse{}, errors.New("route hostname must be a child of relay root hostname")
193 }
194
195 publicHostname, err = utils.LeaseHostname(leaseIdentity.Name, r.rootHostname)
196 if err != nil {
197 return nil, types.RegisterResponse{}, err
198 }
199 expectedHostnameHash := utils.HostnameHash(publicHostname)
200 if hostnameHash != "" && hostnameHash != expectedHostnameHash {
201 return nil, types.RegisterResponse{}, errors.New("hostname hash does not match public hostname")
202 }
203 hostnameHash = expectedHostnameHash
204 }
205 if len(echConfigList) > 0 {
206 echConfigList, err = keyless.NormalizeEncryptedClientHelloConfigList(echConfigList)
207 if err != nil {
208 return nil, types.RegisterResponse{}, err
209 }
210 }
211 echDNSHostname := ""
212 if len(echConfigList) > 0 {
213 echDNSHostname = publicHostname
214 }
215 if req.UDPEnabled && !r.policy.IsUDPEnabled() {
216 return nil, types.RegisterResponse{}, errUDPDisabled
217 }
218 if req.TCPEnabled {
219 if !r.policy.IsTCPPortEnabled() {
220 return nil, types.RegisterResponse{}, errTCPPortDisabled
221 }
222 if r.proxy == nil {
223 return nil, types.RegisterResponse{}, errors.New("tcp proxy is not available")
224 }
225 }
226
227 hostname := routeHostname
228 if hostname == "" && hopToken == "" {
229 hostname, err = utils.LeaseHostname(leaseIdentity.Name, r.rootHostname)
230 if err != nil {
231 return nil, types.RegisterResponse{}, err
232 }
233 }
234
235 accessToken, claims, err := auth.IssueLeaseAccessToken(r.tokenAuthority, r.tokenIssuer, leaseIdentity, ttl)
236 if err != nil {
237 return nil, types.RegisterResponse{}, err
238 }
239 issuedAt := claims.IssuedAt.Time().UTC()
240 expiresAt := claims.Expiry.Time().UTC()
241
242 stream := transport.NewRelayStream(identityKey, defaultIdleKeepalive, defaultReadyQueueLimit)
243 record := &leaseRecord{
244 Identity: leaseIdentity,
245 Hostname: hostname,
246 HostnameHash: hostnameHash,
247 ECHConfigList: echConfigList,
248 ECHDNSHostname: echDNSHostname,
249 Metadata: req.Metadata.Copy(),
250 ExpiresAt: expiresAt,
251 FirstSeenAt: issuedAt,
252 LastSeenAt: issuedAt,
253 ClientIP: clientIP,
254 ReportedIP: utils.SanitizeReportedIP(reportedIP),
255 hopToken: hopToken,
256 stream: stream,
257 }
258
259 if req.UDPEnabled {
260 if r.udpPorts == nil {
261 return nil, types.RegisterResponse{}, errors.New("udp port allocation not available")
262 }
263 port, err := r.udpPorts.Allocate(leaseIdentity.Name)
264 if err != nil {
265 if errors.Is(err, transport.ErrPortExhausted) {
266 return nil, types.RegisterResponse{}, errUDPPortExhausted
267 }
268 return nil, types.RegisterResponse{}, err
269 }
270 record.datagram = transport.NewRelayDatagram(identityKey, port)
271 record.udpPorts = r.udpPorts
272 }
273
274 if req.TCPEnabled {
275 if r.tcpPorts == nil {
276 record.Close()
277 return nil, types.RegisterResponse{}, errors.New("tcp port allocation not available")
278 }
279 port, err := r.tcpPorts.Allocate(leaseIdentity.Name)
280 if err != nil {
281 record.Close()
282 if errors.Is(err, transport.ErrPortExhausted) {
283 return nil, types.RegisterResponse{}, errTCPPortExhausted
284 }
285 return nil, types.RegisterResponse{}, err
286 }
287 record.tcpPort = transport.NewRelayTCPPort(identityKey, port, stream, func(left, right net.Conn) {
288 r.proxy.bridge(left, right, identityKey, r.policy.BPSManager())
289 })
290 record.tcpPorts = r.tcpPorts
291 }
292
293 if err := record.Start(); err != nil {
294 record.Close()
295 return nil, types.RegisterResponse{}, err
296 }
297
298 var replaced *leaseRecord
299 replacedIndex := -1
300 r.mu.Lock()
301 now := time.Now()
302 udpLeases := 0
303 tcpLeases := 0
304 for i, existing := range r.records {
305 if existing == nil {
306 continue
307 }
308 existingKey := existing.Key()
309 if replacedIndex < 0 && existing.stream != nil && existingKey == identityKey {
310 replaced = existing
311 replacedIndex = i
312 }
313 if existing.isExpired(now) {
314 continue
315 }
316 if existingKey != identityKey {
317 if existing.datagram != nil {
318 udpLeases++
319 }
320 if existing.tcpPort != nil {
321 tcpLeases++
322 }
323 }
324 if existing.isPublicEntry() && existingKey != identityKey && existing.routesOverlap(record) {
325 r.mu.Unlock()
326 record.Close()
327 return nil, types.RegisterResponse{}, errHostnameConflict
328 }
329 if hopToken != "" && (existing.isHopMiddle() || existing.isHopExit()) && existing.hopToken == hopToken && existingKey != identityKey {
330 r.mu.Unlock()
331 record.Close()
332 return nil, types.RegisterResponse{}, errors.New("hop token conflict")
333 }
334 }
335 if record.datagram != nil {
336 if max := r.policy.UDPMaxLeases(); max > 0 && udpLeases >= max {
337 r.mu.Unlock()
338 record.Close()
339 return nil, types.RegisterResponse{}, errUDPCapacityExceeded
340 }
341 }
342 if record.tcpPort != nil {
343 if max := r.policy.TCPPortMaxLeases(); max > 0 && tcpLeases >= max {
344 r.mu.Unlock()
345 record.Close()
346 return nil, types.RegisterResponse{}, errTCPPortCapacityExceeded
347 }
348 }
349 for i := 0; i < len(r.records); i++ {
350 existing := r.records[i]
351 if existing == nil || existing.stream != nil || !existing.isPublicEntry() || existing.Key() != identityKey {
352 continue
353 }
354 if existing.routesOverlap(record) {
355 r.deleteRecord(i)
356 i--
357 }
358 }
359 if replacedIndex >= 0 {
360 r.policy.ForgetIdentity(identityKey)
361 r.records[replacedIndex] = record
362 } else {
363 r.records = append(r.records, record)
364 }
365 r.policy.IPFilter().RegisterIdentityIP(identityKey, record.ClientIP)
366 r.mu.Unlock()
367
368 if replaced != nil {
369 replaced.Close()
370 }
371
372 resp := types.RegisterResponse{
373 Identity: record.Identity,
374 ExpiresAt: record.ExpiresAt,
375 AccessToken: accessToken,
376 SNIPort: r.sniPort,
377 UDPEnabled: record.datagram != nil,
378 TCPEnabled: record.tcpPort != nil,
379 }
380 if record.datagram != nil {
381 resp.UDPAddr = fmt.Sprintf("%s:%d", r.rootHostname, record.datagram.UDPPort())
382 }
383 if record.tcpPort != nil {
384 resp.TCPAddr = fmt.Sprintf("%s:%d", r.rootHostname, record.tcpPort.TCPPort())
385 }
386 return record, resp, nil
387 }
388
389 func (r *leaseRegistry) admitLeaseByToken(token string, requireDatagram bool) (*leaseRecord, error) {
390 if r == nil {
391 return nil, errFeatureUnavailable
392 }
393 now := time.Now().UTC()
394 claims, err := auth.VerifyLeaseAccessToken(token, r.tokenAuthority.Identity().PublicKey, r.tokenIssuer, now)
395 if err != nil {
396 return nil, errUnauthorized
397 }
398 r.mu.RLock()
399 record := r.recordByKey(claims.Identity.Key(), now)
400 r.mu.RUnlock()
401 if record == nil {
402 return nil, errLeaseNotFound
403 }
404 if !r.policy.IsIdentityRoutable(record.Key()) {
405 return nil, errLeaseRejected
406 }
407 if record.stream == nil || (requireDatagram && record.datagram == nil) {
408 return nil, errTransportMismatch
409 }
410 return record, nil
411 }
412
413 func (r *leaseRegistry) Renew(req types.RenewRequest, clientIP string) (types.RenewResponse, error) {
414 if r == nil {
415 return types.RenewResponse{}, errFeatureUnavailable
416 }
417 claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, r.tokenAuthority.Identity().PublicKey, r.tokenIssuer, time.Now().UTC())
418 if err != nil {
419 return types.RenewResponse{}, errUnauthorized
420 }
421 ttl := defaultLeaseTTL
422 if req.TTL > 0 {
423 ttl = time.Duration(req.TTL) * time.Second
424 }
425
426 leaseKey := claims.Identity.Key()
427 reportedIP := utils.SanitizeReportedIP(req.ReportedIP)
428 r.mu.Lock()
429 record := r.recordByKey(leaseKey, time.Time{})
430 if record == nil {
431 r.mu.Unlock()
432 return types.RenewResponse{}, errLeaseNotFound
433 }
434
435 now := time.Now()
436 expiresAt := now.Add(ttl)
437 record.ExpiresAt = expiresAt
438 record.LastSeenAt = now
439 if strings.TrimSpace(clientIP) != "" {
440 record.ClientIP = clientIP
441 }
442 if strings.TrimSpace(reportedIP) != "" {
443 record.ReportedIP = reportedIP
444 }
445 record.Metadata = req.Metadata.Copy()
446 r.policy.IPFilter().RegisterIdentityIP(leaseKey, clientIP)
447 recordIdentity := record.Identity
448 r.mu.Unlock()
449
450 nextAccessToken, _, err := auth.IssueLeaseAccessToken(r.tokenAuthority, r.tokenIssuer, recordIdentity, ttl)
451 if err != nil {
452 return types.RenewResponse{}, &apiError{types.APIErrorCodeInternal, err.Error(), http.StatusInternalServerError}
453 }
454
455 return types.RenewResponse{
456 ExpiresAt: expiresAt,
457 AccessToken: nextAccessToken,
458 }, nil
459 }
460
461 func (r *leaseRegistry) Unregister(req types.UnregisterRequest) (*leaseRecord, error) {
462 if r == nil {
463 return nil, errFeatureUnavailable
464 }
465 claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, r.tokenAuthority.Identity().PublicKey, r.tokenIssuer, time.Now().UTC())
466 if err != nil {
467 return nil, errUnauthorized
468 }
469 r.mu.Lock()
470
471 key := strings.TrimSpace(claims.Identity.Key())
472 for i, record := range r.records {
473 if record == nil || record.stream == nil || record.Key() != key {
474 continue
475 }
476 r.deleteRecord(i)
477 r.policy.ForgetIdentity(key)
478 r.mu.Unlock()
479 record.Close()
480 return record, nil
481 }
482 r.mu.Unlock()
483 return nil, errLeaseNotFound
484 }
485
486 func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) (*leaseRecord, error) {
487 if route == nil {
488 return nil, errors.New("hop route is required")
489 }
490 ownerKey, err := identity.AddressFromCompressedPublicKeyHex(route.OwnerPublicKey)
491 if err != nil {
492 return nil, err
493 }
494 routeHostname := route.RouteHostname
495 hostnameHash := route.HostnameHash
496 echConfigList := bytes.Clone(route.ECHConfigList)
497 publicHostname := utils.NormalizeHostname(route.PublicHostname)
498 matchToken := route.MatchToken
499 overlayIPv4, overlayErr := identity.DeriveWireGuardOverlayIPv4(route.ForwardRelay.WireGuardPublicKey)
500 forwardToken := route.ForwardToken
501 expiresAt := route.ExpiresAt.UTC()
502 hasPublicMatcher := routeHostname != "" || hostnameHash != ""
503
504 switch {
505 case r == nil:
506 return nil, errFeatureUnavailable
507 case !expiresAt.After(now):
508 return nil, errors.New("route expiry must be in the future")
509 case matchToken != "" && hasPublicMatcher:
510 return nil, errors.New("route and token matchers are mutually exclusive")
511 case matchToken == "" && routeHostname == "":
512 return nil, errors.New("route hostname or token matcher is required")
513 case overlayErr != nil:
514 return nil, fmt.Errorf("forward relay overlay ipv4: %w", overlayErr)
515 case forwardToken == "":
516 return nil, errors.New("forward token is required")
517 }
518 if routeHostname != "" {
519 routeLabel, routeBase, ok := strings.Cut(routeHostname, ".")
520 normalizedRouteLabel, labelErr := utils.NormalizeDNSLabel(routeLabel)
521 if !ok || labelErr != nil || normalizedRouteLabel != routeLabel || routeBase != r.rootHostname {
522 return nil, errors.New("route hostname must be a child of relay root hostname")
523 }
524 }
525 if hostnameHash != "" {
526 if publicHostname == "" {
527 return nil, errors.New("hostname hash requires public hostname")
528 }
529 if !utils.HostnameMatchesBaseDomain(publicHostname, r.rootHostname) {
530 return nil, errors.New("public hostname must be a child of relay root hostname")
531 }
532 if utils.HostnameHash(publicHostname) != hostnameHash {
533 return nil, errors.New("hostname hash does not match public hostname")
534 }
535 }
536 if len(echConfigList) > 0 {
537 if publicHostname == "" || routeHostname == "" || hostnameHash == "" {
538 return nil, errors.New("ech config list requires public hostname, route hostname, and hostname hash")
539 }
540 echConfigList, err = keyless.NormalizeEncryptedClientHelloConfigList(echConfigList)
541 if err != nil {
542 return nil, err
543 }
544 }
545 name := routeHostname
546 if label, _, ok := strings.Cut(name, "."); ok {
547 name = label
548 }
549
550 r.mu.Lock()
551 defer r.mu.Unlock()
552
553 record := &leaseRecord{
554 Identity: types.Identity{
555 Name: name,
556 Address: ownerKey,
557 },
558 Hostname: routeHostname,
559 HostnameHash: hostnameHash,
560 ECHConfigList: echConfigList,
561 ECHDNSHostname: publicHostname,
562 Metadata: route.Metadata.Copy(),
563 FirstSeenAt: route.FirstSeenAt.UTC(),
564 ExpiresAt: expiresAt,
565 hopToken: matchToken,
566 hopNextOverlayIPv4: overlayIPv4,
567 hopNextToken: forwardToken,
568 }
569 switch {
570 case record.isPublicEntry():
571 for _, existing := range r.records {
572 if existing == nil || !existing.isPublicEntry() || existing.isExpired(now) {
573 continue
574 }
575 if !existing.routesOverlap(record) {
576 continue
577 }
578 if existing.stream != nil || !strings.EqualFold(existing.Address, record.Address) {
579 return nil, errHostnameConflict
580 }
581 }
582 for i, existing := range r.records {
583 if existing == nil || existing.stream != nil || !existing.isPublicEntry() || !strings.EqualFold(existing.Address, record.Address) {
584 continue
585 }
586 if existing.routesOverlap(record) {
587 r.records[i] = record
588 return record, nil
589 }
590 }
591 r.records = append(r.records, record)
592 return record, nil
593 case record.isHopMiddle():
594 if existing := r.recordByHopToken(record.hopToken, now); existing != nil {
595 if !existing.isHopMiddle() || !strings.EqualFold(existing.Address, record.Address) {
596 return nil, errors.New("hop token conflict")
597 }
598 }
599 for i, existing := range r.records {
600 if existing != nil && existing.isHopMiddle() &&
601 existing.hopToken == record.hopToken &&
602 strings.EqualFold(existing.Address, record.Address) {
603 r.records[i] = record
604 return record, nil
605 }
606 }
607 r.records = append(r.records, record)
608 return record, nil
609 default:
610 return nil, errors.New("invalid hop route")
611 }
612 }
613
614 func (r *leaseRegistry) DeleteHopRoute(route *types.HopRoute) *leaseRecord {
615 if r == nil || route == nil {
616 return nil
617 }
618 ownerKey, err := identity.AddressFromCompressedPublicKeyHex(route.OwnerPublicKey)
619 if err != nil {
620 return nil
621 }
622 routeHostname := route.RouteHostname
623 hostnameHash := route.HostnameHash
624 token := route.MatchToken
625
626 var deleted *leaseRecord
627 r.mu.Lock()
628 for i := 0; i < len(r.records); i++ {
629 record := r.records[i]
630 if record == nil || record.stream != nil {
631 continue
632 }
633 deleteRecord := false
634 if routeHostname != "" || hostnameHash != "" {
635 deleteRecord = record.isPublicEntry() && strings.EqualFold(record.Address, ownerKey)
636 if routeHostname != "" {
637 deleteRecord = deleteRecord && record.Hostname == routeHostname
638 }
639 if hostnameHash != "" {
640 deleteRecord = deleteRecord && record.HostnameHash == hostnameHash
641 }
642 }
643 if token != "" {
644 deleteRecord = deleteRecord || record.isHopMiddle() &&
645 record.hopToken == token &&
646 strings.EqualFold(record.Address, ownerKey)
647 }
648 if deleteRecord {
649 deleted = record
650 r.deleteRecord(i)
651 break
652 }
653 }
654 r.mu.Unlock()
655 deleted.Close()
656 return deleted
657 }
658
659 func (r *leaseRegistry) promoteECHDNS(record *leaseRecord, manager *acme.Manager, sniPort int) {
660 if !record.hasECHDNSRecord() {
661 return
662 }
663
664 go func() {
665 active := false
666 now := time.Now()
667 r.mu.RLock()
668 for _, existing := range r.records {
669 if existing == record && !existing.isExpired(now) {
670 active = true
671 break
672 }
673 }
674 r.mu.RUnlock()
675 if !active {
676 return
677 }
678
679 ctx, cancel := context.WithTimeout(context.Background(), defaultClaimTimeout)
680 err := record.syncECHDNS(ctx, manager, sniPort)
681 cancel()
682
683 if err != nil {
684 log.Warn().
685 Err(err).
686 Str("hostname", record.ECHDNSHostname).
687 Str("route_hostname", record.Hostname).
688 Str("address", record.Address).
689 Msg("promote ech dns record")
690 }
691
692 hostnameActive := false
693 now = time.Now()
694 r.mu.RLock()
695 for _, existing := range r.records {
696 if existing != nil && !existing.isExpired(now) && existing.hasECHDNSRecord() && existing.ECHDNSHostname == record.ECHDNSHostname {
697 hostnameActive = true
698 break
699 }
700 }
701 r.mu.RUnlock()
702 if !hostnameActive {
703 cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), defaultClaimTimeout)
704 record.deleteECHDNS(cleanupCtx, manager)
705 cleanupCancel()
706 }
707 }()
708 }
709
710 func (r *leaseRegistry) issueRegisterChallenge(req types.RegisterChallengeRequest, domain, uri, clientIP string) (types.RegisterChallengeResponse, error) {
711 if r == nil {
712 return types.RegisterChallengeResponse{}, errFeatureUnavailable
713 }
714 if len(req.ECHConfigList) > 0 {
715 echConfigList, err := keyless.NormalizeEncryptedClientHelloConfigList(req.ECHConfigList)
716 if err != nil {
717 return types.RegisterChallengeResponse{}, err
718 }
719 req.ECHConfigList = echConfigList
720 }
721
722 now := time.Now().UTC()
723 challenge, err := auth.NewRegisterChallenge(req, domain, uri, now, defaultRegisterChallengeTTL)
724 if err != nil {
725 return types.RegisterChallengeResponse{}, err
726 }
727 clientIP = strings.ToLower(strings.TrimSpace(clientIP))
728 if clientIP == "" {
729 clientIP = "<unknown>"
730 }
731
732 r.mu.Lock()
733 defer r.mu.Unlock()
734
735 pending := 0
736 for i := 0; i < len(r.records); {
737 record := r.records[i]
738 if record != nil && record.registerChallenge != nil {
739 if record.isExpired(now) {
740 r.deleteRecord(i)
741 continue
742 }
743 if record.ClientIP == clientIP {
744 pending++
745 }
746 }
747 i++
748 }
749 if pending >= defaultRegisterChallengeOutstandingPerIP {
750 return types.RegisterChallengeResponse{}, errRegisterChallengePending
751 }
752 r.records = append(r.records, &leaseRecord{
753 ExpiresAt: challenge.ExpiresAt,
754 ClientIP: clientIP,
755 registerChallenge: challenge,
756 })
757
758 return types.RegisterChallengeResponse{
759 ChallengeID: challenge.ChallengeID,
760 ExpiresAt: challenge.ExpiresAt,
761 SIWEMessage: challenge.SIWEMessage,
762 }, nil
763 }
764
765 func (r *leaseRegistry) consumeVerifiedRegisterChallenge(req types.RegisterRequest) (*auth.RegisterChallenge, error) {
766 challengeID := strings.TrimSpace(req.ChallengeID)
767 if challengeID == "" {
768 return nil, auth.ErrRegisterChallengeNotFound
769 }
770
771 now := time.Now().UTC()
772 r.mu.Lock()
773 defer r.mu.Unlock()
774
775 for i, record := range r.records {
776 if record == nil || record.registerChallenge == nil || record.registerChallenge.ChallengeID != challengeID {
777 continue
778 }
779 challenge := record.registerChallenge
780 if challenge.Expired(now) {
781 r.deleteRecord(i)
782 return nil, auth.ErrRegisterChallengeExpired
783 }
784 if err := challenge.Verify(req, now); err != nil {
785 return nil, err
786 }
787
788 r.deleteRecord(i)
789 return challenge, nil
790 }
791 return nil, auth.ErrRegisterChallengeNotFound
792 }
793
794 func (r *leaseRegistry) issueLeaseAccessToken(record *leaseRecord, now time.Time) (string, error) {
795 token, _, err := auth.IssueLeaseAccessToken(r.tokenAuthority, r.tokenIssuer, record.Identity, record.ExpiresAt.Sub(now))
796 return token, err
797 }
798
799 func (r *leaseRegistry) verifySigningAccessToken(token string) error {
800 now := time.Now().UTC()
801 claims, err := auth.VerifyLeaseAccessToken(token, r.tokenAuthority.Identity().PublicKey, r.tokenIssuer, now)
802 if err != nil {
803 return errUnauthorized
804 }
805
806 r.mu.RLock()
807 defer r.mu.RUnlock()
808
809 for _, record := range r.records {
810 if record == nil || record.isExpired(now) || record.Key() != claims.Identity.Key() {
811 continue
812 }
813 if record.stream != nil && record.isPublicEntry() {
814 if !r.policy.IsIdentityRoutable(record.Key()) {
815 return errLeaseRejected
816 }
817 return nil
818 }
819 _, _, hasNextHop := record.nextHop()
820 if record.stream == nil && record.isPublicEntry() && hasNextHop {
821 return nil
822 }
823 }
824 return errUnauthorized
825 }
826
827 func (r *leaseRegistry) Touch(key, clientIP string, now time.Time) {
828 r.mu.Lock()
829 defer r.mu.Unlock()
830
831 record := r.recordByKey(key, now)
832 if record == nil {
833 return
834 }
835 record.LastSeenAt = now
836 if strings.TrimSpace(clientIP) != "" {
837 record.ClientIP = clientIP
838 }
839 r.policy.IPFilter().RegisterIdentityIP(record.Key(), clientIP)
840 }
841
842 func (r *leaseRegistry) cleanupExpired(now time.Time) []*leaseRecord {
843 r.mu.Lock()
844
845 var expired []*leaseRecord
846 for i := 0; i < len(r.records); {
847 record := r.records[i]
848 if record != nil && record.isExpired(now) {
849 expired = append(expired, record)
850 if record.stream != nil {
851 r.policy.ForgetIdentity(record.Key())
852 }
853 r.deleteRecord(i)
854 continue
855 }
856 i++
857 }
858 r.mu.Unlock()
859
860 for _, record := range expired {
861 record.Close()
862 }
863 return expired
864 }
865
866 func (r *leaseRegistry) PublicLeases(now time.Time) []types.Lease {
867 r.mu.RLock()
868 defer r.mu.RUnlock()
869
870 leases := make([]types.Lease, 0, len(r.records))
871 for _, record := range r.records {
872 if record == nil || !record.isPublicEntry() || record.isExpired(now) {
873 continue
874 }
875 if record.Metadata.Hide {
876 continue
877 }
878 if record.stream != nil {
879 identityKey := record.Key()
880 if r.policy.IsIdentityBanned(identityKey) || r.policy.IsIdentityDenied(identityKey) || !r.policy.EffectiveApproval(identityKey) {
881 continue
882 }
883 since := time.Duration(0)
884 if !record.LastSeenAt.IsZero() {
885 since = max(now.Sub(record.LastSeenAt), 0)
886 }
887 if record.stream.ReadyCount() == 0 && since >= 3*time.Minute {
888 continue
889 }
890 }
891 leases = append(leases, r.publicLease(record))
892 }
893 return leases
894 }
895
896 func (r *leaseRegistry) PolicyLeases(now time.Time) []types.PolicyLease {
897 r.mu.RLock()
898 defer r.mu.RUnlock()
899
900 leases := make([]types.PolicyLease, 0, len(r.records))
901 for _, record := range r.records {
902 if record == nil || record.stream == nil || record.isExpired(now) {
903 continue
904 }
905 clientIP := record.ClientIP
906 identityKey := record.Key()
907 leases = append(leases, types.PolicyLease{
908 Lease: r.publicLease(record),
909 IdentityKey: identityKey,
910 Address: record.Address,
911 BPS: r.policy.BPSManager().IdentityBPS(identityKey),
912 ClientIP: clientIP,
913 ReportedIP: record.ReportedIP,
914 IsApproved: r.policy.EffectiveApproval(identityKey),
915 IsBanned: r.policy.IsIdentityBanned(identityKey),
916 IsDenied: r.policy.IsIdentityDenied(identityKey),
917 IsIPBanned: r.policy.IPFilter().IsIPBanned(clientIP),
918 })
919 }
920 return leases
921 }
922
923 func (r *leaseRegistry) deleteRecord(i int) {
924 last := len(r.records) - 1
925 r.records[i] = r.records[last]
926 r.records[last] = nil
927 r.records = r.records[:last]
928 }
929
930 func (r *leaseRegistry) publicLease(record *leaseRecord) types.Lease {
931 name := record.Name
932 hostname := record.Hostname
933 if record.stream != nil && record.HostnameHash != "" {
934 if publicHostname, err := utils.LeaseHostname(record.Name, r.rootHostname); err == nil && utils.HostnameHash(publicHostname) == record.HostnameHash {
935 hostname = publicHostname
936 }
937 } else if record.HostnameHash != "" && record.Hostname != "" {
938 label, _, _ := strings.Cut(record.Hostname, ".")
939 name = label
940 }
941 lease := types.Lease{
942 Name: name,
943 ExpiresAt: record.ExpiresAt,
944 FirstSeenAt: record.FirstSeenAt,
945 LastSeenAt: record.LastSeenAt,
946 Hostname: hostname,
947 UDPEnabled: record.datagram != nil,
948 TCPEnabled: record.tcpPort != nil,
949 Metadata: record.Metadata.Copy(),
950 }
951 if record.tcpPort != nil {
952 lease.TCPAddr = fmt.Sprintf("%s:%d", record.Hostname, record.tcpPort.TCPPort())
953 }
954 if record.stream != nil {
955 lease.Ready = record.stream.ReadyCount()
956 } else if record.isPublicEntry() {
957 _, _, hasNextHop := record.nextHop()
958 if !hasNextHop {
959 return lease
960 }
961 lease.Ready = 1
962 }
963 return lease
964 }