feat: Refactor ECH DNS handling and introduce leaseRecord structure for improved management
Kim committed
May 7, 2026 at 17:10 UTC
0ca35a80251ea2b592da3651f6b470e65577dc99
9 files changed
+292
-403
portal/acme/acme.go
+4
-4
@@ -513,10 +513,6 @@ func (m *Manager) DeleteECHConfig(ctx context.Context, hostname string) error {
513
return nil
514
}
515
516
- m.echMu.Lock()
517
- delete(m.echRecords, hostname)
518
- m.echMu.Unlock()
519
-
516
if err := m.dns.DeleteHTTPSRecord(ctx, hostname); err != nil {
517
return err
518
}
@@ -525,6 +521,10 @@ func (m *Manager) DeleteECHConfig(ctx context.Context, hostname string) error {
521
return fmt.Errorf("delete ECH A record for %s: %w", hostname, err)
522
}
523
}
524
+
525
+ m.echMu.Lock()
526
+ delete(m.echRecords, hostname)
527
+ m.echMu.Unlock()
528
return nil
529
}
530
portal/api_server.go
+33
-154
@@ -1,6 +1,7 @@
1
package portal
2
3
import (
4
+ "context"
5
"crypto/tls"
6
"errors"
7
"fmt"
@@ -282,62 +283,20 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
283
writeAPIErrorResponse(w, err)
284
return
285
}
285
- if manager := s.acmeManager; manager != nil {
286
- if record.hasENSGaslessDNSRecord() {
287
- err := manager.SyncENSGaslessHostname(r.Context(), record.Hostname, record.Address)
288
- if err != nil {
289
- removed, _ := s.registry.Unregister(types.UnregisterRequest{AccessToken: resp.AccessToken})
290
- if removed == nil {
291
- record.Close()
292
- removed = record
293
- }
294
- if removed.hasENSGaslessDNSRecord() {
295
- deleteErr := manager.DeleteENSGaslessHostname(r.Context(), removed.Hostname)
296
- if deleteErr != nil {
297
- log.Warn().
298
- Err(deleteErr).
299
- Str("hostname", removed.Hostname).
300
- Str("address", removed.Address).
301
- Msg("delete lease ens gasless hostname after sync failure")
302
- }
303
- }
304
- writeAPIErrorResponse(w, err)
305
- return
306
- }
307
- }
308
- if record.hasECHDNSRecord() {
309
- err := manager.SyncECHConfig(r.Context(), record.ECHDNSHostname, record.ECHConfigList, s.cfg.SNIPort)
310
- if err != nil {
311
- removed, _ := s.registry.Unregister(types.UnregisterRequest{AccessToken: resp.AccessToken})
312
- if removed == nil {
313
- record.Close()
314
- removed = record
315
- }
316
- if removed.hasENSGaslessDNSRecord() {
317
- deleteErr := manager.DeleteENSGaslessHostname(r.Context(), removed.Hostname)
318
- if deleteErr != nil {
319
- log.Warn().
320
- Err(deleteErr).
321
- Str("hostname", removed.Hostname).
322
- Str("address", removed.Address).
323
- Msg("delete lease ens gasless hostname after ech sync failure")
324
- }
325
- }
326
- if removed.hasECHDNSRecord() {
327
- deleteErr := manager.DeleteECHConfig(r.Context(), removed.ECHDNSHostname)
328
- if deleteErr != nil {
329
- log.Warn().
330
- Err(deleteErr).
331
- Str("hostname", removed.ECHDNSHostname).
332
- Str("route_hostname", removed.Hostname).
333
- Str("address", removed.Address).
334
- Msg("delete lease ech dns record after sync failure")
335
- }
336
- }
337
- writeAPIErrorResponse(w, err)
338
- return
339
- }
286
+ dnsCtx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), defaultClaimTimeout)
287
+ deleteECHOnRollback, err := record.syncDNS(dnsCtx, s.acmeManager, s.cfg.SNIPort)
288
+ cancel()
289
+ if err != nil {
290
+ removed, _ := s.registry.Unregister(types.UnregisterRequest{AccessToken: resp.AccessToken})
291
+ if removed == nil {
292
+ record.Close()
293
+ removed = record
294
}
295
+ cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(r.Context()), defaultClaimTimeout)
296
+ removed.deleteDNS(cleanupCtx, s.acmeManager, deleteECHOnRollback)
297
+ cleanupCancel()
298
+ writeAPIErrorResponse(w, err)
299
+ return
300
}
301
302
utils.WriteAPIData(w, http.StatusCreated, resp)
@@ -432,29 +391,9 @@ func (s *Server) handleUnregister(w http.ResponseWriter, r *http.Request) {
391
writeAPIErrorResponse(w, err)
392
return
393
}
435
- if manager := s.acmeManager; manager != nil {
436
- if record.hasENSGaslessDNSRecord() {
437
- err := manager.DeleteENSGaslessHostname(r.Context(), record.Hostname)
438
- if err != nil {
439
- log.Warn().
440
- Err(err).
441
- Str("hostname", record.Hostname).
442
- Str("address", record.Address).
443
- Msg("delete lease ens gasless hostname")
444
- }
445
- }
446
- if record.hasECHDNSRecord() {
447
- err := manager.DeleteECHConfig(r.Context(), record.ECHDNSHostname)
448
- if err != nil {
449
- log.Warn().
450
- Err(err).
451
- Str("hostname", record.ECHDNSHostname).
452
- Str("route_hostname", record.Hostname).
453
- Str("address", record.Address).
454
- Msg("delete lease ech dns record")
455
- }
456
- }
457
- }
394
+ dnsCtx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), defaultClaimTimeout)
395
+ record.deleteDNS(dnsCtx, s.acmeManager, true)
396
+ cancel()
397
398
utils.WriteAPIData(w, http.StatusOK, map[string]any{})
399
}
@@ -493,29 +432,9 @@ func (s *Server) handleHop(w http.ResponseWriter, r *http.Request) {
432
}
433
if r.Method == http.MethodDelete {
434
record := s.registry.DeleteHopRoute(&route)
496
- if manager := s.acmeManager; manager != nil {
497
- if record.hasENSGaslessDNSRecord() {
498
- err := manager.DeleteENSGaslessHostname(r.Context(), record.Hostname)
499
- if err != nil {
500
- log.Warn().
501
- Err(err).
502
- Str("hostname", record.Hostname).
503
- Str("address", record.Address).
504
- Msg("delete hop route ens gasless hostname")
505
- }
506
- }
507
- if record.hasECHDNSRecord() {
508
- err := manager.DeleteECHConfig(r.Context(), record.ECHDNSHostname)
509
- if err != nil {
510
- log.Warn().
511
- Err(err).
512
- Str("hostname", record.ECHDNSHostname).
513
- Str("route_hostname", record.Hostname).
514
- Str("address", record.Address).
515
- Msg("delete hop route ech dns record")
516
- }
517
- }
518
- }
435
+ dnsCtx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), defaultClaimTimeout)
436
+ record.deleteDNS(dnsCtx, s.acmeManager, true)
437
+ cancel()
438
utils.WriteAPIData(w, http.StatusOK, map[string]any{})
439
return
440
}
@@ -548,60 +467,19 @@ func (s *Server) handleHop(w http.ResponseWriter, r *http.Request) {
467
writeAPIErrorResponse(w, err)
468
return
469
}
551
- if manager := s.acmeManager; manager != nil {
552
- if record.hasENSGaslessDNSRecord() {
553
- err := manager.SyncENSGaslessHostname(r.Context(), record.Hostname, record.Address)
554
- if err != nil {
555
- removed := s.registry.DeleteHopRoute(&route)
556
- if removed == nil {
557
- removed = record
558
- }
559
- if removed.hasENSGaslessDNSRecord() {
560
- deleteErr := manager.DeleteENSGaslessHostname(r.Context(), removed.Hostname)
561
- if deleteErr != nil {
562
- log.Warn().
563
- Err(deleteErr).
564
- Str("hostname", removed.Hostname).
565
- Str("address", removed.Address).
566
- Msg("delete hop route ens gasless hostname after sync failure")
567
- }
568
- }
569
- writeAPIErrorResponse(w, err)
570
- return
571
- }
572
- }
573
- if record.hasECHDNSRecord() {
574
- err := manager.SyncECHConfig(r.Context(), record.ECHDNSHostname, record.ECHConfigList, s.cfg.SNIPort)
575
- if err != nil {
576
- removed := s.registry.DeleteHopRoute(&route)
577
- if removed == nil {
578
- removed = record
579
- }
580
- if removed.hasENSGaslessDNSRecord() {
581
- deleteErr := manager.DeleteENSGaslessHostname(r.Context(), removed.Hostname)
582
- if deleteErr != nil {
583
- log.Warn().
584
- Err(deleteErr).
585
- Str("hostname", removed.Hostname).
586
- Str("address", removed.Address).
587
- Msg("delete hop route ens gasless hostname after ech sync failure")
588
- }
589
- }
590
- if removed.hasECHDNSRecord() {
591
- deleteErr := manager.DeleteECHConfig(r.Context(), removed.ECHDNSHostname)
592
- if deleteErr != nil {
593
- log.Warn().
594
- Err(deleteErr).
595
- Str("hostname", removed.ECHDNSHostname).
596
- Str("route_hostname", removed.Hostname).
597
- Str("address", removed.Address).
598
- Msg("delete hop route ech dns record after sync failure")
599
- }
600
- }
601
- writeAPIErrorResponse(w, err)
602
- return
603
- }
470
+ dnsCtx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), defaultClaimTimeout)
471
+ deleteECHOnRollback, err := record.syncDNS(dnsCtx, s.acmeManager, s.cfg.SNIPort)
472
+ cancel()
473
+ if err != nil {
474
+ removed := s.registry.DeleteHopRoute(&route)
475
+ if removed == nil {
476
+ removed = record
477
}
478
+ cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(r.Context()), defaultClaimTimeout)
479
+ removed.deleteDNS(cleanupCtx, s.acmeManager, deleteECHOnRollback)
480
+ cleanupCancel()
481
+ writeAPIErrorResponse(w, err)
482
+ return
483
}
484
var accessToken string
485
if record.isPublicEntry() {
@@ -613,6 +491,7 @@ func (s *Server) handleHop(w http.ResponseWriter, r *http.Request) {
491
}
492
utils.WriteAPIData(w, http.StatusOK, types.HopRouteResponse{
493
AccessToken: accessToken,
494
+ SNIPort: s.cfg.SNIPort,
495
})
496
}
497
portal/lease.go
+12
-117
@@ -2,7 +2,6 @@ package portal
2
3
import (
4
"bytes"
5
- "context"
5
"errors"
6
"fmt"
7
"net"
@@ -389,18 +388,18 @@ func (r *leaseRegistry) admitLeaseByToken(token string, requireDatagram bool) (*
388
return nil, errUnauthorized
389
}
390
r.mu.RLock()
392
- lease := r.recordByKey(claims.Identity.Key(), now)
391
+ record := r.recordByKey(claims.Identity.Key(), now)
392
r.mu.RUnlock()
394
- if lease == nil {
393
+ if record == nil {
394
return nil, errLeaseNotFound
395
}
397
- if !r.policy.IsIdentityRoutable(lease.Key()) {
396
+ if !r.policy.IsIdentityRoutable(record.Key()) {
397
return nil, errLeaseRejected
398
}
400
- if lease.stream == nil || (requireDatagram && lease.datagram == nil) {
399
+ if record.stream == nil || (requireDatagram && record.datagram == nil) {
400
return nil, errTransportMismatch
401
}
403
- return lease, nil
402
+ return record, nil
403
}
404
405
func (r *leaseRegistry) Renew(req types.RenewRequest, clientIP string) (types.RenewResponse, error) {
@@ -652,6 +651,13 @@ func (r *leaseRegistry) issueRegisterChallenge(req types.RegisterChallengeReques
651
if r == nil {
652
return types.RegisterChallengeResponse{}, errFeatureUnavailable
653
}
654
+ if len(req.ECHConfigList) > 0 {
655
+ echConfigList, err := keyless.NormalizeEncryptedClientHelloConfigList(req.ECHConfigList)
656
+ if err != nil {
657
+ return types.RegisterChallengeResponse{}, err
658
+ }
659
+ req.ECHConfigList = echConfigList
660
+ }
661
662
now := time.Now().UTC()
663
challenge, err := auth.NewRegisterChallenge(req, domain, uri, now, defaultRegisterChallengeTTL)
@@ -896,114 +902,3 @@ func (r *leaseRegistry) publicLease(record *leaseRecord) types.Lease {
902
}
903
return lease
904
}
899
-
900
-type leaseRecord struct {
901
- types.Identity
902
- ExpiresAt time.Time
903
- FirstSeenAt time.Time
904
- LastSeenAt time.Time
905
- ClientIP string
906
- ReportedIP string
907
- Hostname string
908
- HostnameHash string
909
- ECHConfigList []byte
910
- ECHDNSHostname string
911
- Metadata types.LeaseMetadata
912
-
913
- hopToken string
914
- hopNextOverlayIPv4 string
915
- hopNextToken string
916
- registerChallenge *auth.RegisterChallenge
917
-
918
- datagram *transport.RelayDatagram
919
- udpPorts *transport.PortAllocator
920
- tcpPort *transport.RelayTCPPort
921
- tcpPorts *transport.PortAllocator
922
- stream *transport.RelayStream
923
-}
924
-
925
-func (r *leaseRecord) isPublicEntry() bool {
926
- return r != nil && r.hopToken == "" && r.Hostname != ""
927
-}
928
-
929
-func (r *leaseRecord) hasENSGaslessDNSRecord() bool {
930
- return r.isPublicEntry() && r.HostnameHash == ""
931
-}
932
-
933
-func (r *leaseRecord) hasECHDNSRecord() bool {
934
- return r.isPublicEntry() && len(r.ECHConfigList) > 0 && r.ECHDNSHostname != ""
935
-}
936
-
937
-func (r *leaseRecord) isHopMiddle() bool {
938
- _, _, hasNextHop := r.nextHop()
939
- return r != nil && r.Hostname == "" && r.hopToken != "" && hasNextHop
940
-}
941
-
942
-func (r *leaseRecord) isHopExit() bool {
943
- _, _, hasNextHop := r.nextHop()
944
- return r != nil && r.hopToken != "" && !hasNextHop
945
-}
946
-
947
-func (r *leaseRecord) routesOverlap(other *leaseRecord) bool {
948
- if r == nil || other == nil {
949
- return false
950
- }
951
- if r.Hostname != "" && other.Hostname != "" && r.Hostname == other.Hostname {
952
- return true
953
- }
954
- if r.HostnameHash != "" && other.HostnameHash != "" && r.HostnameHash == other.HostnameHash {
955
- return true
956
- }
957
- if r.Hostname != "" && other.HostnameHash != "" && utils.HostnameHash(r.Hostname) == other.HostnameHash {
958
- return true
959
- }
960
- return other.Hostname != "" && r.HostnameHash != "" && utils.HostnameHash(other.Hostname) == r.HostnameHash
961
-}
962
-
963
-func (r *leaseRecord) nextHop() (string, string, bool) {
964
- if r == nil {
965
- return "", "", false
966
- }
967
- overlayIPv4 := r.hopNextOverlayIPv4
968
- forwardToken := r.hopNextToken
969
- return overlayIPv4, forwardToken, overlayIPv4 != "" || forwardToken != ""
970
-}
971
-
972
-func (r *leaseRecord) isExpired(now time.Time) bool {
973
- return r != nil && !now.IsZero() && !now.Before(r.ExpiresAt)
974
-}
975
-
976
-func (r *leaseRecord) Start() error {
977
- if r.datagram != nil {
978
- if err := r.datagram.Start(context.Background()); err != nil {
979
- return err
980
- }
981
- }
982
- if r.tcpPort != nil {
983
- return r.tcpPort.Start(context.Background())
984
- }
985
- return nil
986
-}
987
-
988
-func (r *leaseRecord) Close() {
989
- if r == nil {
990
- return
991
- }
992
- if r.stream != nil {
993
- r.stream.Close()
994
- }
995
- if r.datagram != nil {
996
- port := r.datagram.UDPPort()
997
- r.datagram.Close()
998
- if port > 0 && r.udpPorts != nil {
999
- r.udpPorts.Release(port)
1000
- }
1001
- }
1002
- if r.tcpPort != nil {
1003
- port := r.tcpPort.TCPPort()
1004
- r.tcpPort.Close()
1005
- if port > 0 && r.tcpPorts != nil {
1006
- r.tcpPorts.Release(port)
1007
- }
1008
- }
1009
-}
portal/record.go
new
+177
@@ -0,0 +1,177 @@
1
+package portal
2
+
3
+import (
4
+ "context"
5
+ "time"
6
+
7
+ "github.com/gosuda/portal-tunnel/v2/portal/acme"
8
+ "github.com/gosuda/portal-tunnel/v2/portal/auth"
9
+ "github.com/gosuda/portal-tunnel/v2/portal/transport"
10
+ "github.com/gosuda/portal-tunnel/v2/types"
11
+ "github.com/gosuda/portal-tunnel/v2/utils"
12
+ "github.com/rs/zerolog/log"
13
+)
14
+
15
+type leaseRecord struct {
16
+ types.Identity
17
+ ExpiresAt time.Time
18
+ FirstSeenAt time.Time
19
+ LastSeenAt time.Time
20
+ ClientIP string
21
+ ReportedIP string
22
+ Hostname string
23
+ HostnameHash string
24
+ ECHConfigList []byte
25
+ ECHDNSHostname string
26
+ Metadata types.LeaseMetadata
27
+
28
+ hopToken string
29
+ hopNextOverlayIPv4 string
30
+ hopNextToken string
31
+ registerChallenge *auth.RegisterChallenge
32
+
33
+ datagram *transport.RelayDatagram
34
+ udpPorts *transport.PortAllocator
35
+ tcpPort *transport.RelayTCPPort
36
+ tcpPorts *transport.PortAllocator
37
+ stream *transport.RelayStream
38
+}
39
+
40
+func (r *leaseRecord) isPublicEntry() bool {
41
+ return r != nil && r.hopToken == "" && r.Hostname != ""
42
+}
43
+
44
+func (r *leaseRecord) ensGaslessDNSHostname() string {
45
+ if !r.isPublicEntry() {
46
+ return ""
47
+ }
48
+ if len(r.ECHConfigList) > 0 && r.ECHDNSHostname != "" {
49
+ return r.ECHDNSHostname
50
+ }
51
+ if r.HostnameHash == "" {
52
+ return r.Hostname
53
+ }
54
+ return ""
55
+}
56
+
57
+func (r *leaseRecord) hasECHDNSRecord() bool {
58
+ return r.isPublicEntry() && len(r.ECHConfigList) > 0 && r.ECHDNSHostname != ""
59
+}
60
+
61
+func (r *leaseRecord) isHopMiddle() bool {
62
+ _, _, hasNextHop := r.nextHop()
63
+ return r != nil && r.Hostname == "" && r.hopToken != "" && hasNextHop
64
+}
65
+
66
+func (r *leaseRecord) isHopExit() bool {
67
+ _, _, hasNextHop := r.nextHop()
68
+ return r != nil && r.hopToken != "" && !hasNextHop
69
+}
70
+
71
+func (r *leaseRecord) routesOverlap(other *leaseRecord) bool {
72
+ if r == nil || other == nil {
73
+ return false
74
+ }
75
+ if r.Hostname != "" && other.Hostname != "" && r.Hostname == other.Hostname {
76
+ return true
77
+ }
78
+ if r.HostnameHash != "" && other.HostnameHash != "" && r.HostnameHash == other.HostnameHash {
79
+ return true
80
+ }
81
+ if r.Hostname != "" && other.HostnameHash != "" && utils.HostnameHash(r.Hostname) == other.HostnameHash {
82
+ return true
83
+ }
84
+ return other.Hostname != "" && r.HostnameHash != "" && utils.HostnameHash(other.Hostname) == r.HostnameHash
85
+}
86
+
87
+func (r *leaseRecord) nextHop() (string, string, bool) {
88
+ if r == nil {
89
+ return "", "", false
90
+ }
91
+ overlayIPv4 := r.hopNextOverlayIPv4
92
+ forwardToken := r.hopNextToken
93
+ return overlayIPv4, forwardToken, overlayIPv4 != "" || forwardToken != ""
94
+}
95
+
96
+func (r *leaseRecord) isExpired(now time.Time) bool {
97
+ return r != nil && !now.IsZero() && !now.Before(r.ExpiresAt)
98
+}
99
+
100
+func (r *leaseRecord) Start() error {
101
+ if r.datagram != nil {
102
+ if err := r.datagram.Start(context.Background()); err != nil {
103
+ return err
104
+ }
105
+ }
106
+ if r.tcpPort != nil {
107
+ return r.tcpPort.Start(context.Background())
108
+ }
109
+ return nil
110
+}
111
+
112
+func (r *leaseRecord) Close() {
113
+ if r == nil {
114
+ return
115
+ }
116
+ if r.stream != nil {
117
+ r.stream.Close()
118
+ }
119
+ if r.datagram != nil {
120
+ port := r.datagram.UDPPort()
121
+ r.datagram.Close()
122
+ if port > 0 && r.udpPorts != nil {
123
+ r.udpPorts.Release(port)
124
+ }
125
+ }
126
+ if r.tcpPort != nil {
127
+ port := r.tcpPort.TCPPort()
128
+ r.tcpPort.Close()
129
+ if port > 0 && r.tcpPorts != nil {
130
+ r.tcpPorts.Release(port)
131
+ }
132
+ }
133
+}
134
+
135
+func (r *leaseRecord) syncDNS(ctx context.Context, manager *acme.Manager, sniPort int) (bool, error) {
136
+ if r == nil || manager == nil {
137
+ return false, nil
138
+ }
139
+ if ensHostname := r.ensGaslessDNSHostname(); ensHostname != "" {
140
+ if err := manager.SyncENSGaslessHostname(ctx, ensHostname, r.Address); err != nil {
141
+ return false, err
142
+ }
143
+ }
144
+ if r.hasECHDNSRecord() {
145
+ if err := manager.SyncECHConfig(ctx, r.ECHDNSHostname, r.ECHConfigList, sniPort); err != nil {
146
+ return true, err
147
+ }
148
+ }
149
+ return false, nil
150
+}
151
+
152
+func (r *leaseRecord) deleteDNS(ctx context.Context, manager *acme.Manager, includeECH bool) {
153
+ if r == nil || manager == nil {
154
+ return
155
+ }
156
+ if ensHostname := r.ensGaslessDNSHostname(); ensHostname != "" {
157
+ err := manager.DeleteENSGaslessHostname(ctx, ensHostname)
158
+ if err != nil {
159
+ log.Warn().
160
+ Err(err).
161
+ Str("hostname", ensHostname).
162
+ Str("address", r.Address).
163
+ Msg("delete ens gasless hostname")
164
+ }
165
+ }
166
+ if includeECH && r.hasECHDNSRecord() {
167
+ err := manager.DeleteECHConfig(ctx, r.ECHDNSHostname)
168
+ if err != nil {
169
+ log.Warn().
170
+ Err(err).
171
+ Str("hostname", r.ECHDNSHostname).
172
+ Str("route_hostname", r.Hostname).
173
+ Str("address", r.Address).
174
+ Msg("delete ech dns record")
175
+ }
176
+ }
177
+}
portal/server.go
+6
-50
@@ -387,31 +387,9 @@ func (s *Server) Shutdown(ctx context.Context) error {
387
s.cancel()
388
}
389
390
- leases := s.registry.CloseAll()
391
- if manager := s.acmeManager; manager != nil {
392
- for _, lease := range leases {
393
- if lease.hasENSGaslessDNSRecord() {
394
- err := manager.DeleteENSGaslessHostname(ctx, lease.Hostname)
395
- if err != nil {
396
- log.Warn().
397
- Err(err).
398
- Str("hostname", lease.Hostname).
399
- Str("address", lease.Address).
400
- Msg("delete lease ens gasless hostname during shutdown")
401
- }
402
- }
403
- if lease.hasECHDNSRecord() {
404
- err := manager.DeleteECHConfig(ctx, lease.ECHDNSHostname)
405
- if err != nil {
406
- log.Warn().
407
- Err(err).
408
- Str("hostname", lease.ECHDNSHostname).
409
- Str("route_hostname", lease.Hostname).
410
- Str("address", lease.Address).
411
- Msg("delete lease ech dns record during shutdown")
412
- }
413
- }
414
- }
390
+ records := s.registry.CloseAll()
391
+ for _, record := range records {
392
+ record.deleteDNS(ctx, s.acmeManager, true)
393
}
394
395
if s.quicBackhaul != nil {
@@ -678,31 +656,9 @@ func (s *Server) runRegistryJanitor(ctx context.Context, interval time.Duration)
656
case <-ctx.Done():
657
return nil
658
case <-ticker.C:
681
- leases := s.registry.cleanupExpired(time.Now())
682
- if manager := s.acmeManager; manager != nil {
683
- for _, lease := range leases {
684
- if lease.hasENSGaslessDNSRecord() {
685
- err := manager.DeleteENSGaslessHostname(ctx, lease.Hostname)
686
- if err != nil {
687
- log.Warn().
688
- Err(err).
689
- Str("hostname", lease.Hostname).
690
- Str("address", lease.Address).
691
- Msg("delete expired lease ens gasless hostname")
692
- }
693
- }
694
- if lease.hasECHDNSRecord() {
695
- err := manager.DeleteECHConfig(ctx, lease.ECHDNSHostname)
696
- if err != nil {
697
- log.Warn().
698
- Err(err).
699
- Str("hostname", lease.ECHDNSHostname).
700
- Str("route_hostname", lease.Hostname).
701
- Str("address", lease.Address).
702
- Msg("delete expired lease ech dns record")
703
- }
704
- }
705
- }
659
+ records := s.registry.cleanupExpired(time.Now())
660
+ for _, record := range records {
661
+ record.deleteDNS(ctx, s.acmeManager, true)
662
}
663
}
664
}
sdk/api_client.go
+42
-67
@@ -10,7 +10,6 @@ import (
10
"net"
11
"net/http"
12
"net/url"
13
- "slices"
13
"strconv"
14
"strings"
15
"time"
@@ -37,12 +36,11 @@ var errRelayIncompatible = errors.New("relay is incompatible")
36
// API call creates fresh TCP connections. Call this after detecting a system
37
// sleep/wake cycle where pooled connections are almost certainly dead.
38
func (l *listener) resetTransport() {
40
- if l.httpClient != nil {
41
- if transport, ok := l.httpClient.Transport.(*http.Transport); ok {
42
- transport.CloseIdleConnections()
43
- }
39
+ if l.httpTransport != nil {
40
+ l.httpTransport.CloseIdleConnections()
41
}
42
l.httpClient = nil
43
+ l.httpTransport = nil
44
l.tlsConfig = nil
45
}
46
@@ -54,16 +52,14 @@ func (l *listener) initHTTPTransport(ctx context.Context) error {
52
bootstrapCtx, cancel := context.WithTimeout(ctx, defaultDialTimeout+defaultHandshakeTimeout)
53
defer cancel()
54
57
- tlsConfig, httpClient, err := utils.NewHTTPTLSClient(bootstrapCtx, l.relayURL, l.requestTimeout)
55
+ tlsConfig, httpClient, httpTransport, err := utils.NewHTTPTLSClient(bootstrapCtx, l.relayURL, l.requestTimeout)
56
if err != nil {
57
return err
58
}
59
60
var domainResp types.DomainResponse
61
if err := utils.HTTPDoAPIPath(ctx, httpClient, l.relayURL, http.MethodGet, types.PathSDKDomain, nil, nil, &domainResp); err != nil {
64
- if transport, ok := httpClient.Transport.(*http.Transport); ok {
65
- transport.CloseIdleConnections()
66
- }
62
+ httpTransport.CloseIdleConnections()
63
err = fmt.Errorf("check relay compatibility: %w", err)
64
var netErr net.Error
65
var apiErr *types.APIRequestError
@@ -77,13 +73,12 @@ func (l *listener) initHTTPTransport(ctx context.Context) error {
73
}
74
protocolVersion := strings.TrimSpace(domainResp.ProtocolVersion)
75
if protocolVersion != types.SDKVersion {
80
- if transport, ok := httpClient.Transport.(*http.Transport); ok {
81
- transport.CloseIdleConnections()
82
- }
76
+ httpTransport.CloseIdleConnections()
77
return fmt.Errorf("%w: relay sdk protocol version mismatch: relay=%q client=%q", errRelayIncompatible, protocolVersion, types.SDKVersion)
78
}
79
80
l.httpClient = httpClient
81
+ l.httpTransport = httpTransport
82
l.tlsConfig = tlsConfig
83
return nil
84
}
@@ -243,108 +238,88 @@ func (l *listener) renewRegisteredLease(ctx context.Context, ttl time.Duration,
238
}
239
240
func (l *listener) unregisterLease(ctx context.Context, accessToken string, hopRoutes []types.HopRoute) error {
246
- var unregisterErr error
247
- if err := l.unregisterHopRoutes(ctx, hopRoutes); err != nil {
248
- unregisterErr = errors.Join(unregisterErr, err)
249
- }
241
+ hopErr := l.unregisterHopRoutes(ctx, hopRoutes)
242
err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKUnregister, types.UnregisterRequest{
243
AccessToken: accessToken,
244
}, nil, nil)
253
- return errors.Join(unregisterErr, err)
245
+ return errors.Join(hopErr, err)
246
}
247
256
-func (l *listener) registerHopRoutes(ctx context.Context, expiresAt time.Time, routes []types.HopRoute) (multihopAccessToken string, err error) {
257
- if len(routes) == 0 {
258
- return "", nil
259
- }
248
+func (l *listener) registerHopRoutes(ctx context.Context, expiresAt time.Time, routes []types.HopRoute) (string, int, error) {
249
if l.relaySet == nil {
261
- return "", errors.New("multi-hop relay set is unavailable")
250
+ return "", 0, errors.New("multi-hop relay set is unavailable")
251
}
252
264
- orderedRoutes := append([]types.HopRoute(nil), routes...)
253
now := time.Now().UTC()
266
- for i := range orderedRoutes {
267
- desc, ok := l.relaySet.OverlayRelayDescriptor(orderedRoutes[i].ForwardRelay.APIHTTPSAddr, now)
254
+ for i := len(routes) - 1; i >= 0; i-- {
255
+ route := routes[i]
256
+ desc, ok := l.relaySet.OverlayRelayDescriptor(route.ForwardRelay.APIHTTPSAddr, now)
257
if !ok {
269
- return "", fmt.Errorf("multi-hop forward relay %d descriptor is unavailable", i)
258
+ return "", 0, fmt.Errorf("multi-hop forward relay %d descriptor is unavailable", i)
259
}
271
- orderedRoutes[i].ForwardRelay = desc
272
- }
273
- slices.Reverse(orderedRoutes)
274
-
275
- for _, unsignedRoute := range orderedRoutes {
276
- unsignedRoute.FirstSeenAt = expiresAt.Add(-30 * time.Second)
277
- route, err := auth.SignHopRoute(http.MethodPost, unsignedRoute, l.identity, expiresAt)
260
+ route.ForwardRelay = desc
261
+ route.FirstSeenAt = expiresAt.Add(-30 * time.Second)
262
+ route, err := auth.SignHopRoute(http.MethodPost, route, l.identity, expiresAt)
263
if err != nil {
279
- return "", err
264
+ return "", 0, err
265
}
266
relayURL, err := url.Parse(route.RelayURL)
267
if err != nil {
283
- return "", fmt.Errorf("parse hop route relay url: %w", err)
268
+ return "", 0, fmt.Errorf("parse hop route relay url: %w", err)
269
}
270
271
bootstrapCtx, cancel := context.WithTimeout(ctx, defaultDialTimeout+defaultHandshakeTimeout)
287
- _, client, err := utils.NewHTTPTLSClient(bootstrapCtx, relayURL, l.requestTimeout)
272
+ _, client, transport, err := utils.NewHTTPTLSClient(bootstrapCtx, relayURL, l.requestTimeout)
273
cancel()
274
if err != nil {
290
- return "", err
275
+ return "", 0, err
276
}
292
- transport, _ := client.Transport.(*http.Transport)
293
- entryRoute := route.MatchToken == "" && route.RouteHostname != ""
277
var hopResp types.HopRouteResponse
295
- var out any
296
- if entryRoute {
297
- out = &hopResp
298
- }
299
- err = utils.HTTPDoAPIPath(ctx, client, relayURL, http.MethodPost, types.PathSDKHop, route, nil, out)
300
- if transport != nil {
301
- transport.CloseIdleConnections()
302
- }
278
+ err = utils.HTTPDoAPIPath(ctx, client, relayURL, http.MethodPost, types.PathSDKHop, route, nil, &hopResp)
279
+ transport.CloseIdleConnections()
280
if err != nil {
304
- return "", err
281
+ return "", 0, err
282
}
306
- if !entryRoute {
283
+ if route.MatchToken != "" || route.RouteHostname == "" {
284
continue
285
}
286
if hopResp.AccessToken == "" {
310
- return "", errors.New("entry relay did not return access token")
287
+ return "", 0, errors.New("entry relay did not return access token")
288
+ }
289
+ if hopResp.SNIPort <= 0 {
290
+ return "", 0, errors.New("entry relay did not return sni port")
291
}
312
- multihopAccessToken = hopResp.AccessToken
313
- return multihopAccessToken, nil
292
+ return hopResp.AccessToken, hopResp.SNIPort, nil
293
}
315
- return "", errors.New("entry hop route did not return access token")
294
+ return "", 0, errors.New("entry hop route did not return access token")
295
}
296
297
func (l *listener) unregisterHopRoutes(ctx context.Context, routes []types.HopRoute) error {
319
- var syncErr error
320
- for _, unsignedRoute := range routes {
321
- route, err := auth.SignHopRoute(http.MethodDelete, unsignedRoute, l.identity, time.Time{})
298
+ var unregisterErr error
299
+ for _, route := range routes {
300
+ route, err := auth.SignHopRoute(http.MethodDelete, route, l.identity, time.Time{})
301
if err != nil {
323
- syncErr = errors.Join(syncErr, err)
302
+ unregisterErr = errors.Join(unregisterErr, err)
303
continue
304
}
305
relayURL, err := url.Parse(route.RelayURL)
306
if err != nil {
328
- syncErr = errors.Join(syncErr, fmt.Errorf("parse hop route relay url: %w", err))
307
+ unregisterErr = errors.Join(unregisterErr, fmt.Errorf("parse hop route relay url: %w", err))
308
continue
309
}
310
311
bootstrapCtx, cancel := context.WithTimeout(ctx, defaultDialTimeout+defaultHandshakeTimeout)
333
- _, client, err := utils.NewHTTPTLSClient(bootstrapCtx, relayURL, l.requestTimeout)
312
+ _, client, transport, err := utils.NewHTTPTLSClient(bootstrapCtx, relayURL, l.requestTimeout)
313
cancel()
314
if err != nil {
336
- syncErr = errors.Join(syncErr, err)
315
+ unregisterErr = errors.Join(unregisterErr, err)
316
continue
317
}
339
- transport, _ := client.Transport.(*http.Transport)
318
err = utils.HTTPDoAPIPath(ctx, client, relayURL, http.MethodDelete, types.PathSDKHop, route, nil, nil)
341
- if transport != nil {
342
- transport.CloseIdleConnections()
343
- }
319
+ transport.CloseIdleConnections()
320
if err != nil {
345
- syncErr = errors.Join(syncErr, err)
346
- continue
321
+ unregisterErr = errors.Join(unregisterErr, err)
322
}
323
}
349
- return syncErr
324
+ return unregisterErr
325
}
sdk/listener.go
+11
-5
@@ -70,8 +70,9 @@ type listener struct {
70
datagram *transport.ClientDatagram
71
mitmManager *mitmManager
72
73
- httpClient *http.Client
74
- tlsConfig *tls.Config
73
+ httpClient *http.Client
74
+ httpTransport *http.Transport
75
+ tlsConfig *tls.Config
76
77
leaseMu sync.RWMutex
78
lease *listenerSnapshot
@@ -687,8 +688,9 @@ func (l *listener) renewLease(ctx context.Context) error {
688
return errors.New("relay did not return renewed access token")
689
}
690
multihopAccessToken := resp.AccessToken
691
+ var entrySNIPort int
692
if len(lease.hopRoutes) > 0 {
691
- multihopAccessToken, err = l.registerHopRoutes(requestCtx, resp.ExpiresAt, lease.hopRoutes)
693
+ multihopAccessToken, entrySNIPort, err = l.registerHopRoutes(requestCtx, resp.ExpiresAt, lease.hopRoutes)
694
if err != nil {
695
return err
696
}
@@ -702,6 +704,9 @@ func (l *listener) renewLease(ctx context.Context) error {
704
next.accessToken = resp.AccessToken
705
next.expiresAt = resp.ExpiresAt
706
next.multihopAccessToken = multihopAccessToken
707
+ if entrySNIPort > 0 {
708
+ next.sniPort = entrySNIPort
709
+ }
710
l.lease = &next
711
l.leaseMu.Unlock()
712
return nil
@@ -732,8 +737,9 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
737
return errors.New("relay did not return sni port for udp transport")
738
}
739
multihopAccessToken := resp.AccessToken
740
+ sniPort := resp.SNIPort
741
if len(hopRoutes) > 0 {
736
- multihopAccessToken, err = l.registerHopRoutes(ctx, resp.ExpiresAt, hopRoutes)
742
+ multihopAccessToken, sniPort, err = l.registerHopRoutes(ctx, resp.ExpiresAt, hopRoutes)
743
if err != nil {
744
_ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
745
return err
@@ -786,7 +792,7 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
792
tcpAddr: resp.TCPAddr,
793
accessToken: resp.AccessToken,
794
expiresAt: resp.ExpiresAt,
789
- sniPort: resp.SNIPort,
795
+ sniPort: sniPort,
796
publicURLBase: publicURLBase,
797
tlsConfig: tlsConf,
798
tlsCloser: tenantTLSCloser,
types/api.go
+1
@@ -142,6 +142,7 @@ type HopRoute struct {
142
143
type HopRouteResponse struct {
144
AccessToken string `json:"access_token,omitempty"`
145
+ SNIPort int `json:"sni_port,omitempty"`
146
}
147
148
func HopRouteBytes(method string, route HopRoute) ([]byte, error) {
utils/tls.go
+6
-6
@@ -17,25 +17,25 @@ import (
17
"time"
18
)
19
20
-func NewHTTPTLSClient(ctx context.Context, relayURL *url.URL, timeout time.Duration) (*tls.Config, *http.Client, error) {
20
+func NewHTTPTLSClient(ctx context.Context, relayURL *url.URL, timeout time.Duration) (*tls.Config, *http.Client, *http.Transport, error) {
21
if relayURL == nil {
22
- return nil, nil, errors.New("relay url is required")
22
+ return nil, nil, nil, errors.New("relay url is required")
23
}
24
25
serverName := relayURL.Hostname()
26
if serverName == "" {
27
- return nil, nil, errors.New("relay hostname is required")
27
+ return nil, nil, nil, errors.New("relay hostname is required")
28
}
29
30
var rootCAs *x509.CertPool
31
if IsLocalRelayHost(serverName) {
32
rootCAPEM, err := FetchEndpointCertificateChain(ctx, relayURL.String(), serverName)
33
if err != nil {
34
- return nil, nil, fmt.Errorf("bootstrap localhost relay trust: %w", err)
34
+ return nil, nil, nil, fmt.Errorf("bootstrap localhost relay trust: %w", err)
35
}
36
rootCAs = x509.NewCertPool()
37
if !rootCAs.AppendCertsFromPEM(rootCAPEM) {
38
- return nil, nil, errors.New("failed to parse relay root ca")
38
+ return nil, nil, nil, errors.New("failed to parse relay root ca")
39
}
40
}
41
@@ -50,7 +50,7 @@ func NewHTTPTLSClient(ctx context.Context, relayURL *url.URL, timeout time.Durat
50
WithoutHTTP2(),
51
WithHTTPTimeout(timeout),
52
)
53
- return rawTLSConfig, httpClient, nil
53
+ return rawTLSConfig, httpClient, mustTransportOf(httpClient), nil
54
}
55
56
func FetchEndpointCertificateChain(ctx context.Context, endpoint, serverName string) ([]byte, error) {