feat: Update hostname handling and refactor related structures for improved clarity and functionality
Kim committed
May 6, 2026 at 14:42 UTC
e0b6760094d222264dd28bb8619f81b88e896c98
11 files changed
+161
-239
portal/auth/hop_route.go
+1
-2
@@ -78,8 +78,7 @@ func normalizeHopRoute(route types.HopRoute, requireOwner bool) (types.HopRoute,
78
route.OwnerPublicKey = ownerPublicKey
79
route.RelayURL = relayURL
80
route.RouteHostname = utils.NormalizeHostname(route.RouteHostname)
81
- route.MatchHostname = utils.NormalizeHostname(route.MatchHostname)
82
- route.MatchHostnameHash = strings.TrimSpace(route.MatchHostnameHash)
81
+ route.HostnameHash = strings.TrimSpace(route.HostnameHash)
82
route.MatchToken = strings.TrimSpace(route.MatchToken)
83
route.Metadata = route.Metadata.Copy()
84
route.ForwardToken = strings.TrimSpace(route.ForwardToken)
portal/auth/register_challenge.go
+3
-11
@@ -48,21 +48,13 @@ func NewRegisterChallenge(req types.RegisterChallengeRequest, domain, uri string
48
return nil, fmt.Errorf("build siwe message: %w", err)
49
}
50
51
- normalizedRequest := types.RegisterChallengeRequest{
52
- Identity: normalizedIdentity,
53
- Metadata: req.Metadata.Copy(),
54
- TTL: req.TTL,
55
- UDPEnabled: req.UDPEnabled,
56
- TCPEnabled: req.TCPEnabled,
57
- HopToken: strings.TrimSpace(req.HopToken),
58
- RouteHostname: utils.NormalizeHostname(req.RouteHostname),
59
- FallbackHostnameHash: strings.TrimSpace(req.FallbackHostnameHash),
60
- }
51
+ req.Identity = normalizedIdentity
52
+ req.Metadata = req.Metadata.Copy()
53
54
return &RegisterChallenge{
55
ChallengeID: challengeID,
56
ExpiresAt: expiresAt,
65
- Request: normalizedRequest,
57
+ Request: req,
58
SIWEMessage: message.String(),
59
domain: strings.TrimSpace(domain),
60
nonce: nonce,
portal/keyless/client.go
+1
-5
@@ -50,15 +50,11 @@ func BuildClientTLSConfig(relayURL, hostname string, echKeys []tls.EncryptedClie
50
return nil, nil, fmt.Errorf("create keyless remote signer: %w", err)
51
}
52
53
- minVersion := uint16(tls.VersionTLS12)
54
- if len(echKeys) > 0 {
55
- minVersion = tls.VersionTLS13
56
- }
53
tlsConfig, err := keylesstls.NewServerTLSConfig(keylesstls.ServerTLSConfig{
54
CertPEM: certPEM,
55
Signer: remoteSigner,
56
NextProtos: []string{"http/1.1"},
61
- MinVersion: minVersion,
57
+ MinVersion: MinTLSVersion(len(echKeys) > 0),
58
EncryptedClientHelloKeys: echKeys,
59
})
60
if err != nil {
portal/keyless/tls.go
+12
-10
@@ -26,15 +26,20 @@ type RemoteSignerConfig struct {
26
RootCAPEM []byte
27
}
28
29
+// MinTLSVersion returns the minimum TLS version required by the keyless TLS mode.
30
+func MinTLSVersion(echEnabled bool) uint16 {
31
+ if echEnabled {
32
+ return tls.VersionTLS13
33
+ }
34
+ return tls.VersionTLS12
35
+}
36
+
37
func AttachToHTTPServer(server *http.Server, cfg TLSMaterialConfig) (io.Closer, error) {
38
if server == nil {
39
return nil, errors.New("http server is required")
40
}
41
if cfg.Keyless != nil {
34
- minVersion := tls.VersionTLS12
35
- if len(cfg.EncryptedClientHelloKeys) > 0 {
36
- minVersion = tls.VersionTLS13
37
- }
42
+ minVersion := MinTLSVersion(len(cfg.EncryptedClientHelloKeys) > 0)
43
remoteSigner, err := keylesstls.AttachToHTTPServer(server, keylesstls.HTTPServerAttachConfig{
44
CertPEM: cfg.CertPEM,
45
RemoteSigner: keylesstls.RemoteSignerConfig{
@@ -46,7 +51,7 @@ func AttachToHTTPServer(server *http.Server, cfg TLSMaterialConfig) (io.Closer,
51
RootCAPEM: cfg.Keyless.RootCAPEM,
52
},
53
NextProtos: []string{"http/1.1"},
49
- MinTLSVersion: uint16(minVersion),
54
+ MinTLSVersion: minVersion,
55
EncryptedClientHelloKeys: cfg.EncryptedClientHelloKeys,
56
})
57
if err != nil {
@@ -60,12 +65,9 @@ func AttachToHTTPServer(server *http.Server, cfg TLSMaterialConfig) (io.Closer,
65
return nil, fmt.Errorf("parse api tls key pair: %w", err)
66
}
67
63
- minVersion := tls.VersionTLS12
64
- if len(cfg.EncryptedClientHelloKeys) > 0 {
65
- minVersion = tls.VersionTLS13
66
- }
68
+ minVersion := MinTLSVersion(len(cfg.EncryptedClientHelloKeys) > 0)
69
server.TLSConfig = &tls.Config{
68
- MinVersion: uint16(minVersion),
70
+ MinVersion: minVersion,
71
NextProtos: []string{"http/1.1"},
72
Certificates: []tls.Certificate{cert},
73
EncryptedClientHelloKeys: cfg.EncryptedClientHelloKeys,
portal/lease.go
+65
-104
@@ -102,7 +102,7 @@ func (r *leaseRegistry) Lookup(host string) (*leaseRecord, bool) {
102
if record == nil || !record.isPublicEntry() || record.isExpired(now) {
103
continue
104
}
105
- if record.FallbackHostnameHash != "" && record.FallbackHostnameHash == hostHash {
105
+ if record.HostnameHash != "" && record.HostnameHash == hostHash {
106
return record, true
107
}
108
}
@@ -161,34 +161,25 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
161
identityKey := identity.Key()
162
hopToken := strings.TrimSpace(req.HopToken)
163
routeHostname := utils.NormalizeHostname(req.RouteHostname)
164
- fallbackHostnameHash := strings.TrimSpace(req.FallbackHostnameHash)
164
+ hostnameHash := strings.TrimSpace(req.HostnameHash)
165
if hopToken != "" && (req.UDPEnabled || req.TCPEnabled) {
166
return nil, types.RegisterResponse{}, errTransportMismatch
167
}
168
- if (routeHostname != "" || fallbackHostnameHash != "") && (hopToken != "" || req.UDPEnabled || req.TCPEnabled) {
168
+ if (routeHostname != "" || hostnameHash != "") && (hopToken != "" || req.UDPEnabled || req.TCPEnabled) {
169
return nil, types.RegisterResponse{}, errTransportMismatch
170
}
171
- if fallbackHostnameHash != "" && routeHostname == "" {
172
- return nil, types.RegisterResponse{}, errors.New("fallback hostname hash requires route hostname")
171
+ if hostnameHash != "" && routeHostname == "" {
172
+ return nil, types.RegisterResponse{}, errors.New("hostname hash requires route hostname")
173
}
174
if routeHostname != "" {
175
routeLabel, routeBase, ok := strings.Cut(routeHostname, ".")
176
normalizedRouteLabel, labelErr := utils.NormalizeDNSLabel(routeLabel)
177
- if !ok || labelErr != nil || normalizedRouteLabel != routeLabel || routeBase != utils.NormalizeHostname(r.rootHostname) {
177
+ if !ok || labelErr != nil || normalizedRouteLabel != routeLabel || routeBase != r.rootHostname {
178
return nil, types.RegisterResponse{}, errors.New("route hostname must be a child of relay root hostname")
179
}
180
}
181
- hostname := routeHostname
182
- if hostname == "" && hopToken == "" {
183
- hostname, err = utils.LeaseHostname(identity.Name, r.rootHostname)
184
- if err != nil {
185
- return nil, types.RegisterResponse{}, err
186
- }
187
- }
188
- if req.UDPEnabled {
189
- if !r.policy.IsUDPEnabled() {
190
- return nil, types.RegisterResponse{}, errUDPDisabled
191
- }
181
+ if req.UDPEnabled && !r.policy.IsUDPEnabled() {
182
+ return nil, types.RegisterResponse{}, errUDPDisabled
183
}
184
if req.TCPEnabled {
185
if !r.policy.IsTCPPortEnabled() {
@@ -199,6 +190,14 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
190
}
191
}
192
193
+ hostname := routeHostname
194
+ if hostname == "" && hopToken == "" {
195
+ hostname, err = utils.LeaseHostname(identity.Name, r.rootHostname)
196
+ if err != nil {
197
+ return nil, types.RegisterResponse{}, err
198
+ }
199
+ }
200
+
201
accessToken, claims, err := auth.IssueLeaseAccessToken(r.tokenPrivateKey, r.tokenKeyID, r.tokenIssuer, identity, ttl)
202
if err != nil {
203
return nil, types.RegisterResponse{}, err
@@ -208,17 +207,17 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
207
208
stream := transport.NewRelayStream(identityKey, defaultIdleKeepalive, defaultReadyQueueLimit)
209
record := &leaseRecord{
211
- Identity: identity,
212
- Hostname: hostname,
213
- FallbackHostnameHash: fallbackHostnameHash,
214
- Metadata: req.Metadata,
215
- ExpiresAt: expiresAt,
216
- FirstSeenAt: issuedAt,
217
- LastSeenAt: issuedAt,
218
- ClientIP: clientIP,
219
- ReportedIP: utils.SanitizeReportedIP(reportedIP),
220
- hopToken: hopToken,
221
- stream: stream,
210
+ Identity: identity,
211
+ Hostname: hostname,
212
+ HostnameHash: hostnameHash,
213
+ Metadata: req.Metadata.Copy(),
214
+ ExpiresAt: expiresAt,
215
+ FirstSeenAt: issuedAt,
216
+ LastSeenAt: issuedAt,
217
+ ClientIP: clientIP,
218
+ ReportedIP: utils.SanitizeReportedIP(reportedIP),
219
+ hopToken: hopToken,
220
+ stream: stream,
221
}
222
223
if req.UDPEnabled {
@@ -456,18 +455,13 @@ func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) (
455
if err != nil {
456
return nil, err
457
}
459
- matchHostname := utils.NormalizeHostname(route.MatchHostname)
460
- routeHostname := utils.NormalizeHostname(route.RouteHostname)
461
- matchHostnameHash := strings.TrimSpace(route.MatchHostnameHash)
462
- matchToken := strings.TrimSpace(route.MatchToken)
458
+ routeHostname := route.RouteHostname
459
+ hostnameHash := route.HostnameHash
460
+ matchToken := route.MatchToken
461
overlayIPv4, overlayErr := utils.DeriveWireGuardOverlayIPv4(route.ForwardRelay.WireGuardPublicKey)
464
- forwardToken := strings.TrimSpace(route.ForwardToken)
462
+ forwardToken := route.ForwardToken
463
expiresAt := route.ExpiresAt.UTC()
466
- hostname := routeHostname
467
- if hostname == "" {
468
- hostname = matchHostname
469
- }
470
- hasPublicMatcher := hostname != "" || matchHostnameHash != ""
464
+ hasPublicMatcher := routeHostname != "" || hostnameHash != ""
465
466
switch {
467
case r == nil:
@@ -476,10 +470,8 @@ func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) (
470
return nil, errors.New("route expiry must be in the future")
471
case matchToken != "" && hasPublicMatcher:
472
return nil, errors.New("hostname and token matchers are mutually exclusive")
479
- case matchToken == "" && hostname == "":
480
- return nil, errors.New("route hostname or hostname matcher is required")
481
- case routeHostname != "" && matchHostname != "":
482
- return nil, errors.New("route hostname and hostname matcher are mutually exclusive")
473
+ case matchToken == "" && routeHostname == "":
474
+ return nil, errors.New("route hostname or token matcher is required")
475
case overlayErr != nil:
476
return nil, fmt.Errorf("forward relay overlay ipv4: %w", overlayErr)
477
case forwardToken == "":
@@ -488,11 +480,11 @@ func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) (
480
if routeHostname != "" {
481
routeLabel, routeBase, ok := strings.Cut(routeHostname, ".")
482
normalizedRouteLabel, labelErr := utils.NormalizeDNSLabel(routeLabel)
491
- if !ok || labelErr != nil || normalizedRouteLabel != routeLabel || routeBase != utils.NormalizeHostname(r.rootHostname) {
483
+ if !ok || labelErr != nil || normalizedRouteLabel != routeLabel || routeBase != r.rootHostname {
484
return nil, errors.New("route hostname must be a child of relay root hostname")
485
}
486
}
495
- name := hostname
487
+ name := routeHostname
488
if label, _, ok := strings.Cut(name, "."); ok {
489
name = label
490
}
@@ -505,14 +497,14 @@ func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) (
497
Name: name,
498
Address: ownerKey,
499
},
508
- Hostname: hostname,
509
- FallbackHostnameHash: matchHostnameHash,
510
- Metadata: route.Metadata.Copy(),
511
- FirstSeenAt: route.FirstSeenAt.UTC(),
512
- ExpiresAt: expiresAt,
513
- hopToken: matchToken,
514
- hopNextOverlayIPv4: overlayIPv4,
515
- hopNextToken: forwardToken,
500
+ Hostname: routeHostname,
501
+ HostnameHash: hostnameHash,
502
+ Metadata: route.Metadata.Copy(),
503
+ FirstSeenAt: route.FirstSeenAt.UTC(),
504
+ ExpiresAt: expiresAt,
505
+ hopToken: matchToken,
506
+ hopNextOverlayIPv4: overlayIPv4,
507
+ hopNextToken: forwardToken,
508
}
509
switch {
510
case record.isPublicEntry():
@@ -567,14 +559,9 @@ func (r *leaseRegistry) DeleteHopRoute(route *types.HopRoute) *leaseRecord {
559
if err != nil {
560
return nil
561
}
570
- hostname := utils.NormalizeHostname(route.MatchHostname)
571
- routeHostname := utils.NormalizeHostname(route.RouteHostname)
572
- hostnameHash := strings.TrimSpace(route.MatchHostnameHash)
573
- token := strings.TrimSpace(route.MatchToken)
574
- routeKey := routeHostname
575
- if routeKey == "" {
576
- routeKey = hostname
577
- }
562
+ routeHostname := route.RouteHostname
563
+ hostnameHash := route.HostnameHash
564
+ token := route.MatchToken
565
566
var deleted *leaseRecord
567
r.mu.Lock()
@@ -584,13 +571,13 @@ func (r *leaseRegistry) DeleteHopRoute(route *types.HopRoute) *leaseRecord {
571
continue
572
}
573
deleteRecord := false
587
- if routeKey != "" || hostnameHash != "" {
574
+ if routeHostname != "" || hostnameHash != "" {
575
deleteRecord = record.isPublicEntry() && strings.EqualFold(record.Address, ownerKey)
589
- if routeKey != "" {
590
- deleteRecord = deleteRecord && record.Hostname == routeKey
576
+ if routeHostname != "" {
577
+ deleteRecord = deleteRecord && record.Hostname == routeHostname
578
}
579
if hostnameHash != "" {
593
- deleteRecord = deleteRecord && record.FallbackHostnameHash == hostnameHash
580
+ deleteRecord = deleteRecord && record.HostnameHash == hostnameHash
581
}
582
}
583
if token != "" {
@@ -610,34 +597,8 @@ func (r *leaseRegistry) DeleteHopRoute(route *types.HopRoute) *leaseRecord {
597
}
598
599
func (r *leaseRegistry) issueRegisterChallenge(req types.RegisterChallengeRequest, domain, uri, clientIP string) (types.RegisterChallengeResponse, error) {
613
- hopToken := strings.TrimSpace(req.HopToken)
614
- routeHostname := utils.NormalizeHostname(req.RouteHostname)
615
- fallbackHostnameHash := strings.TrimSpace(req.FallbackHostnameHash)
616
- if hopToken != "" && (req.UDPEnabled || req.TCPEnabled) {
617
- return types.RegisterChallengeResponse{}, errTransportMismatch
618
- }
619
- if (routeHostname != "" || fallbackHostnameHash != "") && (hopToken != "" || req.UDPEnabled || req.TCPEnabled) {
620
- return types.RegisterChallengeResponse{}, errTransportMismatch
621
- }
622
- if fallbackHostnameHash != "" && routeHostname == "" {
623
- return types.RegisterChallengeResponse{}, errors.New("fallback hostname hash requires route hostname")
624
- }
625
- if routeHostname != "" {
626
- routeLabel, routeBase, ok := strings.Cut(routeHostname, ".")
627
- normalizedRouteLabel, labelErr := utils.NormalizeDNSLabel(routeLabel)
628
- if !ok || labelErr != nil || normalizedRouteLabel != routeLabel || routeBase != utils.NormalizeHostname(r.rootHostname) {
629
- return types.RegisterChallengeResponse{}, errors.New("route hostname must be a child of relay root hostname")
630
- }
631
- }
632
- if req.UDPEnabled {
633
- if !r.policy.IsUDPEnabled() {
634
- return types.RegisterChallengeResponse{}, errUDPDisabled
635
- }
636
- }
637
- if req.TCPEnabled {
638
- if !r.policy.IsTCPPortEnabled() {
639
- return types.RegisterChallengeResponse{}, errTCPPortDisabled
640
- }
600
+ if r == nil {
601
+ return types.RegisterChallengeResponse{}, errFeatureUnavailable
602
}
603
604
now := time.Now().UTC()
@@ -818,7 +779,7 @@ func (r *leaseRegistry) deleteRecord(i int) {
779
func (r *leaseRegistry) publicLease(record *leaseRecord) types.Lease {
780
name := record.Name
781
hostname := record.Hostname
821
- if record.FallbackHostnameHash != "" && record.Hostname != "" {
782
+ if record.HostnameHash != "" && record.Hostname != "" {
783
label, _, _ := strings.Cut(record.Hostname, ".")
784
name = label
785
}
@@ -849,14 +810,14 @@ func (r *leaseRegistry) publicLease(record *leaseRecord) types.Lease {
810
811
type leaseRecord struct {
812
types.Identity
852
- ExpiresAt time.Time
853
- FirstSeenAt time.Time
854
- LastSeenAt time.Time
855
- ClientIP string
856
- ReportedIP string
857
- Hostname string
858
- FallbackHostnameHash string
859
- Metadata types.LeaseMetadata
813
+ ExpiresAt time.Time
814
+ FirstSeenAt time.Time
815
+ LastSeenAt time.Time
816
+ ClientIP string
817
+ ReportedIP string
818
+ Hostname string
819
+ HostnameHash string
820
+ Metadata types.LeaseMetadata
821
822
hopToken string
823
hopNextOverlayIPv4 string
@@ -891,13 +852,13 @@ func (r *leaseRecord) routesOverlap(other *leaseRecord) bool {
852
if r.Hostname != "" && other.Hostname != "" && r.Hostname == other.Hostname {
853
return true
854
}
894
- if r.FallbackHostnameHash != "" && other.FallbackHostnameHash != "" && r.FallbackHostnameHash == other.FallbackHostnameHash {
855
+ if r.HostnameHash != "" && other.HostnameHash != "" && r.HostnameHash == other.HostnameHash {
856
return true
857
}
897
- if r.Hostname != "" && other.FallbackHostnameHash != "" && utils.HostnameHash(r.Hostname) == other.FallbackHostnameHash {
858
+ if r.Hostname != "" && other.HostnameHash != "" && utils.HostnameHash(r.Hostname) == other.HostnameHash {
859
return true
860
}
900
- return other.Hostname != "" && r.FallbackHostnameHash != "" && utils.HostnameHash(other.Hostname) == r.FallbackHostnameHash
861
+ return other.Hostname != "" && r.HostnameHash != "" && utils.HostnameHash(other.Hostname) == r.HostnameHash
862
}
863
864
func (r *leaseRecord) nextHop() (string, string, bool) {
portal/lease_test.go
+7
-7
@@ -97,9 +97,9 @@ func TestLeaseRegistryAutomaticECHRouteFallsBackToPlainSNI(t *testing.T) {
97
routeHostname := "ech-auto-ech.example.com"
98
publicHostname := "auto-ech.example.com"
99
record, registered, err := registry.Register(types.RegisterChallengeRequest{
100
- Identity: newTestLeaseIdentity(t, "auto-ech"),
101
- RouteHostname: routeHostname,
102
- FallbackHostnameHash: utils.HostnameHash(publicHostname),
100
+ Identity: newTestLeaseIdentity(t, "auto-ech"),
101
+ RouteHostname: routeHostname,
102
+ HostnameHash: utils.HostnameHash(publicHostname),
103
}, "203.0.113.10", "")
104
if err != nil {
105
t.Fatalf("Register() error = %v", err)
@@ -135,8 +135,8 @@ func TestLeaseRegistryAutomaticECHRouteFallsBackToPlainSNI(t *testing.T) {
135
}
136
137
if _, _, err := registry.Register(types.RegisterChallengeRequest{
138
- Identity: newTestLeaseIdentity(t, "hash-only"),
139
- FallbackHostnameHash: utils.HostnameHash("hash-only.example.com"),
138
+ Identity: newTestLeaseIdentity(t, "hash-only"),
139
+ HostnameHash: utils.HostnameHash("hash-only.example.com"),
140
}, "203.0.113.10", ""); err == nil {
141
t.Fatal("Register(fallback hash only) error = nil, want error")
142
}
@@ -168,14 +168,14 @@ func TestLeaseRegistryHopRouteCanExposeECHAndPlainSNIFallback(t *testing.T) {
168
}
169
route := baseRoute
170
route.RouteHostname = "ech-demo.example.com"
171
- route.MatchHostnameHash = utils.HostnameHash("demo.example.com")
171
+ route.HostnameHash = utils.HostnameHash("demo.example.com")
172
route.Metadata.Hide = true
173
174
if _, err := registry.RegisterHopRoute(&route, now); err != nil {
175
t.Fatalf("RegisterHopRoute() error = %v", err)
176
}
177
hashOnlyRoute := baseRoute
178
- hashOnlyRoute.MatchHostnameHash = utils.HostnameHash("hash-only.example.com")
178
+ hashOnlyRoute.HostnameHash = utils.HostnameHash("hash-only.example.com")
179
if _, err := registry.RegisterHopRoute(&hashOnlyRoute, now); err == nil {
180
t.Fatal("RegisterHopRoute(hash only) error = nil, want error")
181
}
portal/server.go
+2
-2
@@ -807,7 +807,7 @@ func (s *Server) newSelfDescriptor(now time.Time) (types.RelayDescriptor, error)
807
}
808
809
func (s *Server) syncENSGaslessHostname(ctx context.Context, record *leaseRecord) error {
810
- if record == nil || !record.isPublicEntry() || record.FallbackHostnameHash != "" || s.acmeManager == nil {
810
+ if record == nil || !record.isPublicEntry() || record.HostnameHash != "" || s.acmeManager == nil {
811
return nil
812
}
813
syncCtx, cancel := context.WithTimeout(ctx, defaultClaimTimeout)
@@ -816,7 +816,7 @@ func (s *Server) syncENSGaslessHostname(ctx context.Context, record *leaseRecord
816
}
817
818
func (s *Server) deleteENSGaslessHostname(ctx context.Context, record *leaseRecord, logMessage string) {
819
- if record == nil || !record.isPublicEntry() || record.FallbackHostnameHash != "" || s.acmeManager == nil {
819
+ if record == nil || !record.isPublicEntry() || record.HostnameHash != "" || s.acmeManager == nil {
820
return
821
}
822
deleteCtx, cancel := context.WithTimeout(ctx, defaultClaimTimeout)
sdk/api_client.go
+34
-41
@@ -90,13 +90,13 @@ func (l *listener) initHTTPTransport(ctx context.Context) error {
90
func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnabled, tcpEnabled bool) (types.RegisterResponse, []types.HopRoute, string, error) {
91
var exitHopToken string
92
var publicHostname string
93
- var keylessURL string
93
var routeHostname string
95
- var registerRouteHostname string
96
- var registerFallbackHostnameHash string
94
+ var rootHostname string
95
var hopRoutes []types.HopRoute
96
+ var hopPath []types.RelayDescriptor
97
+ streamLease := !udpEnabled && !tcpEnabled
98
registerIdentity := l.identity
99
- if !udpEnabled && !tcpEnabled {
99
+ if streamLease {
100
token, err := l.identity.DeriveToken("opaque-lease-name", l.identity.Name)
101
if err != nil {
102
return types.RegisterResponse{}, nil, "", fmt.Errorf("derive opaque lease name: %w", err)
@@ -109,6 +109,9 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
109
}
110
}
111
if len(l.multiHop) > 0 {
112
+ if !streamLease {
113
+ return types.RegisterResponse{}, nil, "", errors.New("multi-hop requires stream lease")
114
+ }
115
if len(l.multiHop) < 2 {
116
return types.RegisterResponse{}, nil, "", errors.New("multi-hop requires at least entry and exit relay urls")
117
}
@@ -117,7 +120,7 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
120
}
121
122
now := time.Now().UTC()
120
- hopPath := make([]types.RelayDescriptor, 0, len(l.multiHop))
123
+ hopPath = make([]types.RelayDescriptor, 0, len(l.multiHop))
124
for i, relayURL := range l.multiHop {
125
desc, ok := l.relaySet.OverlayRelayDescriptor(relayURL, now)
126
if !ok {
@@ -126,24 +129,30 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
129
hopPath = append(hopPath, desc)
130
}
131
132
+ rootHostname = utils.PortalRootHost(hopPath[0].APIHTTPSAddr)
133
+ } else if streamLease {
134
+ rootHostname = utils.PortalRootHost(l.relayURL.String())
135
+ }
136
+
137
+ if streamLease {
138
var err error
130
- entryRootHostname := utils.PortalRootHost(hopPath[0].APIHTTPSAddr)
131
- publicHostname, err = utils.LeaseHostname(l.identity.Name, entryRootHostname)
139
+ publicHostname, err = utils.LeaseHostname(l.identity.Name, rootHostname)
140
if err != nil {
141
return types.RegisterResponse{}, nil, "", err
142
}
135
- routeToken, err := l.identity.DeriveToken("ech-route", publicHostname, entryRootHostname)
143
+ routeToken, err := l.identity.DeriveToken("ech-route", publicHostname, rootHostname)
144
if err != nil {
145
return types.RegisterResponse{}, nil, "", err
146
}
147
routeSum := sha256.Sum256([]byte(routeToken))
148
routeLabel := "ech-" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(routeSum[:20]))
141
- routeHostname, err = utils.LeaseHostname(routeLabel, entryRootHostname)
149
+ routeHostname, err = utils.LeaseHostname(routeLabel, rootHostname)
150
if err != nil {
151
return types.RegisterResponse{}, nil, "", err
152
}
145
- keylessURL = hopPath[0].APIHTTPSAddr
153
+ }
154
155
+ if len(l.multiHop) > 0 {
156
hopRoutes = make([]types.HopRoute, 0, len(hopPath)-1)
157
var previousHopToken string
158
for i := 0; i < len(hopPath)-1; i++ {
@@ -165,7 +174,7 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
174
}
175
if i == 0 {
176
route.RouteHostname = routeHostname
168
- route.MatchHostnameHash = utils.HostnameHash(publicHostname)
177
+ route.HostnameHash = utils.HostnameHash(publicHostname)
178
route.Metadata.Hide = true
179
hopRoutes = append(hopRoutes, route)
180
} else {
@@ -175,38 +184,23 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
184
previousHopToken = forwardToken
185
}
186
exitHopToken = previousHopToken
178
- } else if !udpEnabled && !tcpEnabled {
179
- rootHostname := utils.PortalRootHost(l.relayURL.String())
180
- var err error
181
- publicHostname, err = utils.LeaseHostname(l.identity.Name, rootHostname)
182
- if err != nil {
183
- return types.RegisterResponse{}, nil, "", err
184
- }
185
- routeToken, err := l.identity.DeriveToken("ech-route", publicHostname, rootHostname)
186
- if err != nil {
187
- return types.RegisterResponse{}, nil, "", err
188
- }
189
- routeSum := sha256.Sum256([]byte(routeToken))
190
- routeLabel := "ech-" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(routeSum[:20]))
191
- routeHostname, err = utils.LeaseHostname(routeLabel, rootHostname)
192
- if err != nil {
193
- return types.RegisterResponse{}, nil, "", err
194
- }
195
- registerRouteHostname = routeHostname
196
- registerFallbackHostnameHash = utils.HostnameHash(publicHostname)
187
+ }
188
+
189
+ registerReq := types.RegisterChallengeRequest{
190
+ Identity: registerIdentity,
191
+ Metadata: l.metadata,
192
+ TTL: int(ttl / time.Second),
193
+ UDPEnabled: udpEnabled,
194
+ TCPEnabled: tcpEnabled,
195
+ HopToken: exitHopToken,
196
+ }
197
+ if streamLease && len(l.multiHop) == 0 {
198
+ registerReq.RouteHostname = routeHostname
199
+ registerReq.HostnameHash = utils.HostnameHash(publicHostname)
200
}
201
202
var challenge types.RegisterChallengeResponse
200
- if err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKRegisterChallenge, types.RegisterChallengeRequest{
201
- Identity: registerIdentity,
202
- Metadata: l.metadata,
203
- TTL: int(ttl / time.Second),
204
- UDPEnabled: udpEnabled,
205
- TCPEnabled: tcpEnabled,
206
- HopToken: exitHopToken,
207
- RouteHostname: registerRouteHostname,
208
- FallbackHostnameHash: registerFallbackHostnameHash,
209
- }, nil, &challenge); err != nil {
203
+ if err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKRegisterChallenge, registerReq, nil, &challenge); err != nil {
204
return types.RegisterResponse{}, nil, "", err
205
}
206
@@ -238,7 +232,6 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
232
_ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
233
return types.RegisterResponse{}, nil, "", err
234
}
241
- resp.KeylessURL = keylessURL
235
}
236
if publicHostname != "" {
237
resp.Hostname = publicHostname
sdk/listener.go
+10
-17
@@ -171,21 +171,10 @@ func (l *listener) run(ctx context.Context) {
171
172
retries = 0
173
publicURL := ""
174
- routeHostname := ""
175
- echConfigList := ""
174
if lease, ok := l.leaseSnapshot(); ok {
175
publicURL = l.publicURLForLease(lease)
178
- routeHostname = lease.routeHostname
179
- if len(lease.echConfigList) > 0 {
180
- echConfigList = base64.StdEncoding.EncodeToString(lease.echConfigList)
181
- }
176
}
177
event := log.Info().Str("address", l.identity.Address)
184
- if echConfigList != "" {
185
- event = event.
186
- Str("route_hostname", routeHostname).
187
- Str("ech_config_list_base64", echConfigList)
188
- }
178
if publicURL != "" {
179
event.Msg("service ready at " + publicURL)
180
} else {
@@ -254,7 +243,6 @@ func (l *listener) Close() error {
243
244
type listenerLease struct {
245
hostname string
257
- routeHostname string
246
echConfigList []byte
247
udpAddr string
248
tcpAddr string
@@ -728,9 +716,9 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
716
_ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
717
return errors.New("relay did not return sni port for udp transport")
718
}
731
- keylessURL := strings.TrimSpace(resp.KeylessURL)
732
- if keylessURL == "" {
733
- keylessURL = l.relayURL.String()
719
+ keylessURL := l.relayURL.String()
720
+ if len(l.multiHop) > 0 {
721
+ keylessURL = l.multiHop[0]
722
}
723
publicURLBase := l.relayURL
724
if normalizedKeylessURL, err := utils.NormalizeRelayURL(keylessURL); err == nil {
@@ -740,7 +728,6 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
728
}
729
var echKeys []tls.EncryptedClientHelloKey
730
var echConfigList []byte
743
- routeHostname = utils.NormalizeHostname(routeHostname)
731
if routeHostname != "" {
732
echSeed, err := l.identity.DeriveToken("tenant-ech", resp.Hostname, routeHostname)
733
if err != nil {
@@ -773,7 +760,6 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
760
}
761
next := &listenerLease{
762
hostname: resp.Hostname,
776
- routeHostname: routeHostname,
763
echConfigList: echConfigList,
764
udpAddr: resp.UDPAddr,
765
tcpAddr: resp.TCPAddr,
@@ -801,6 +787,13 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
787
if l.relaySet != nil && relayURL != "" {
788
l.relaySet.ConfirmRelayURL(relayURL)
789
}
790
+ if len(echConfigList) > 0 {
791
+ log.Info().
792
+ Str("address", l.identity.Address).
793
+ Str("route_hostname", routeHostname).
794
+ Str("ech_config_list_base64", base64.StdEncoding.EncodeToString(echConfigList)).
795
+ Msg("tenant ech config ready")
796
+ }
797
return nil
798
}
799
sdk/mitm.go
+5
-15
@@ -17,6 +17,7 @@ import (
17
18
"github.com/rs/zerolog/log"
19
20
+ "github.com/gosuda/portal-tunnel/v2/portal/keyless"
21
"github.com/gosuda/portal-tunnel/v2/types"
22
"github.com/gosuda/portal-tunnel/v2/utils"
23
)
@@ -112,21 +113,10 @@ func (m *mitmManager) probeTLSPassthrough(ctx context.Context) (MITMProbeReport,
113
}
114
115
probeTLSConf := &tls.Config{
115
- ServerName: lease.hostname,
116
- InsecureSkipVerify: true,
117
- }
118
- if len(lease.echConfigList) > 0 {
119
- probeTLSConf.MinVersion = tls.VersionTLS13
120
- probeTLSConf.EncryptedClientHelloConfigList = append([]byte(nil), lease.echConfigList...)
121
- }
122
- if lease.tlsConfig != nil {
123
- if probeTLSConf.MinVersion == 0 || lease.tlsConfig.MinVersion > probeTLSConf.MinVersion {
124
- probeTLSConf.MinVersion = lease.tlsConfig.MinVersion
125
- }
126
- probeTLSConf.MaxVersion = lease.tlsConfig.MaxVersion
127
- if len(lease.tlsConfig.NextProtos) > 0 {
128
- probeTLSConf.NextProtos = append([]string(nil), lease.tlsConfig.NextProtos...)
129
- }
116
+ ServerName: lease.hostname,
117
+ InsecureSkipVerify: true,
118
+ MinVersion: keyless.MinTLSVersion(len(lease.echConfigList) > 0),
119
+ EncryptedClientHelloConfigList: append([]byte(nil), lease.echConfigList...),
120
}
121
122
dialer := &tls.Dialer{
types/api.go
+21
-25
@@ -64,14 +64,14 @@ type RegisterRequest struct {
64
}
65
66
type RegisterChallengeRequest struct {
67
- Identity Identity `json:"identity"`
68
- Metadata LeaseMetadata `json:"metadata"`
69
- TTL int `json:"ttl,omitempty"`
70
- UDPEnabled bool `json:"udp_enabled,omitempty"`
71
- TCPEnabled bool `json:"tcp_enabled,omitempty"`
72
- HopToken string `json:"hop_token,omitempty"`
73
- RouteHostname string `json:"route_hostname,omitempty"`
74
- FallbackHostnameHash string `json:"fallback_hostname_hash,omitempty"`
67
+ Identity Identity `json:"identity"`
68
+ Metadata LeaseMetadata `json:"metadata"`
69
+ TTL int `json:"ttl,omitempty"`
70
+ UDPEnabled bool `json:"udp_enabled,omitempty"`
71
+ TCPEnabled bool `json:"tcp_enabled,omitempty"`
72
+ HopToken string `json:"hop_token,omitempty"`
73
+ RouteHostname string `json:"route_hostname,omitempty"`
74
+ HostnameHash string `json:"hostname_hash,omitempty"`
75
}
76
77
type RegisterChallengeResponse struct {
@@ -85,7 +85,6 @@ type RegisterResponse struct {
85
ExpiresAt time.Time `json:"expires_at"`
86
Hostname string `json:"hostname"`
87
AccessToken string `json:"access_token"`
88
- KeylessURL string `json:"keyless_url,omitempty"`
88
SNIPort int `json:"sni_port,omitempty"`
89
UDPAddr string `json:"udp_addr,omitempty"`
90
UDPEnabled bool `json:"udp_enabled,omitempty"`
@@ -125,18 +124,17 @@ type UnregisterRequest struct {
124
}
125
126
type HopRoute struct {
128
- OwnerPublicKey string `json:"owner_public_key,omitempty"`
129
- RelayURL string `json:"relay_url"`
130
- RouteHostname string `json:"route_hostname,omitempty"`
131
- MatchHostname string `json:"match_hostname,omitempty"`
132
- MatchHostnameHash string `json:"match_hostname_hash,omitempty"`
133
- MatchToken string `json:"match_token,omitempty"`
134
- Metadata LeaseMetadata `json:"metadata,omitempty"`
135
- ForwardRelay RelayDescriptor `json:"forward_relay"`
136
- ForwardToken string `json:"forward_token"`
137
- FirstSeenAt time.Time `json:"first_seen_at,omitempty"`
138
- ExpiresAt time.Time `json:"expires_at,omitempty"`
139
- Signature string `json:"signature,omitempty"`
127
+ OwnerPublicKey string `json:"owner_public_key,omitempty"`
128
+ RelayURL string `json:"relay_url"`
129
+ RouteHostname string `json:"route_hostname,omitempty"`
130
+ HostnameHash string `json:"hostname_hash,omitempty"`
131
+ MatchToken string `json:"match_token,omitempty"`
132
+ Metadata LeaseMetadata `json:"metadata,omitempty"`
133
+ ForwardRelay RelayDescriptor `json:"forward_relay"`
134
+ ForwardToken string `json:"forward_token"`
135
+ FirstSeenAt time.Time `json:"first_seen_at,omitempty"`
136
+ ExpiresAt time.Time `json:"expires_at,omitempty"`
137
+ Signature string `json:"signature,omitempty"`
138
}
139
140
func HopRouteBytes(method string, route HopRoute) ([]byte, error) {
@@ -150,8 +148,7 @@ func HopRouteBytes(method string, route HopRoute) ([]byte, error) {
148
OwnerPublicKey string `json:"owner_public_key"`
149
RelayURL string `json:"relay_url"`
150
RouteHostname string `json:"route_hostname"`
153
- MatchHostname string `json:"match_hostname"`
154
- MatchHostnameHash string `json:"match_hostname_hash"`
151
+ HostnameHash string `json:"hostname_hash"`
152
MatchToken string `json:"match_token"`
153
ForwardRelay json.RawMessage `json:"forward_relay"`
154
ForwardToken string `json:"forward_token"`
@@ -163,8 +160,7 @@ func HopRouteBytes(method string, route HopRoute) ([]byte, error) {
160
OwnerPublicKey: strings.TrimSpace(route.OwnerPublicKey),
161
RelayURL: strings.TrimSpace(route.RelayURL),
162
RouteHostname: strings.TrimSpace(route.RouteHostname),
166
- MatchHostname: strings.TrimSpace(route.MatchHostname),
167
- MatchHostnameHash: strings.TrimSpace(route.MatchHostnameHash),
163
+ HostnameHash: strings.TrimSpace(route.HostnameHash),
164
MatchToken: strings.TrimSpace(route.MatchToken),
165
ForwardRelay: json.RawMessage(forwardRelay),
166
ForwardToken: strings.TrimSpace(route.ForwardToken),