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),