feat: Refactor lease registration and hostname handling for improved clarity and functionality

Kim committed May 6, 2026 at 17:25 UTC 400e7eda5ff279b89ca879fdcbbafa4936bc8b56
6 files changed +58 -69
portal/lease.go
+7 -4
@@ -335,14 +335,13 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
335
336 resp := types.RegisterResponse{
337 Identity: record.Identity,
338 - Hostname: record.Hostname,
338 ExpiresAt: record.ExpiresAt,
339 AccessToken: accessToken,
340 + SNIPort: r.sniPort,
341 UDPEnabled: record.datagram != nil,
342 TCPEnabled: record.tcpPort != nil,
343 }
344 if record.datagram != nil {
345 - resp.SNIPort = r.sniPort
345 resp.UDPAddr = fmt.Sprintf("%s:%d", r.rootHostname, record.datagram.UDPPort())
346 }
347 if record.tcpPort != nil {
@@ -469,7 +468,7 @@ func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) (
468 case !expiresAt.After(now):
469 return nil, errors.New("route expiry must be in the future")
470 case matchToken != "" && hasPublicMatcher:
472 - return nil, errors.New("hostname and token matchers are mutually exclusive")
471 + return nil, errors.New("route and token matchers are mutually exclusive")
472 case matchToken == "" && routeHostname == "":
473 return nil, errors.New("route hostname or token matcher is required")
474 case overlayErr != nil:
@@ -779,7 +778,11 @@ func (r *leaseRegistry) deleteRecord(i int) {
778 func (r *leaseRegistry) publicLease(record *leaseRecord) types.Lease {
779 name := record.Name
780 hostname := record.Hostname
782 - if record.HostnameHash != "" && record.Hostname != "" {
781 + if record.stream != nil && record.HostnameHash != "" {
782 + if publicHostname, err := utils.LeaseHostname(record.Name, r.rootHostname); err == nil && utils.HostnameHash(publicHostname) == record.HostnameHash {
783 + hostname = publicHostname
784 + }
785 + } else if record.HostnameHash != "" && record.Hostname != "" {
786 label, _, _ := strings.Cut(record.Hostname, ".")
787 name = label
788 }
portal/lease_test.go
+12 -13
@@ -96,7 +96,7 @@ func TestLeaseRegistryAutomaticECHRouteFallsBackToPlainSNI(t *testing.T) {
96 registry := newTestRegistry(t)
97 routeHostname := "ech-auto-ech.example.com"
98 publicHostname := "auto-ech.example.com"
99 - record, registered, err := registry.Register(types.RegisterChallengeRequest{
99 + record, _, err := registry.Register(types.RegisterChallengeRequest{
100 Identity: newTestLeaseIdentity(t, "auto-ech"),
101 RouteHostname: routeHostname,
102 HostnameHash: utils.HostnameHash(publicHostname),
@@ -104,34 +104,33 @@ func TestLeaseRegistryAutomaticECHRouteFallsBackToPlainSNI(t *testing.T) {
104 if err != nil {
105 t.Fatalf("Register() error = %v", err)
106 }
107 - if registered.Hostname != routeHostname {
108 - t.Fatalf("Register() hostname = %q, want route hostname %q", registered.Hostname, routeHostname)
107 + if record.Hostname != routeHostname {
108 + t.Fatalf("Register() route hostname = %q, want %q", record.Hostname, routeHostname)
109 }
110 - if registered.Hostname == publicHostname {
111 - t.Fatalf("Register() hostname = public hostname = %q", registered.Hostname)
110 + if record.Hostname == publicHostname {
111 + t.Fatalf("Register() route hostname = public hostname = %q", record.Hostname)
112 }
113 if lookedUp, ok := registry.Lookup(publicHostname); !ok || lookedUp != record {
114 t.Fatalf("Lookup(public hostname) = %v, %v, want fallback lease", lookedUp, ok)
115 }
116 - lookedUp, ok := registry.Lookup(registered.Hostname)
116 + lookedUp, ok := registry.Lookup(record.Hostname)
117 if !ok || lookedUp != record {
118 t.Fatalf("Lookup(route hostname) = %v, %v, want registered lease", lookedUp, ok)
119 }
120 leases := registry.PublicLeases(time.Now())
121 - publicHostnameFromLease := ""
122 - if len(leases) == 1 {
123 - publicHostnameFromLease = leases[0].Hostname
121 + if len(leases) != 1 {
122 + t.Fatalf("PublicLeases() length = %d, want 1", len(leases))
123 }
125 - if len(leases) != 1 || publicHostnameFromLease != registered.Hostname {
126 - t.Fatalf("PublicLeases() hostname = len %d, host %q, want len 1, host %q", len(leases), publicHostnameFromLease, registered.Hostname)
124 + if leases[0].Hostname != publicHostname {
125 + t.Fatalf("PublicLeases()[0].Hostname = %q, want %q", leases[0].Hostname, publicHostname)
126 }
127
128 adminLeases := registry.AdminLeases(time.Now())
129 if len(adminLeases) != 1 {
130 t.Fatalf("AdminLeases() length = %d, want 1", len(adminLeases))
131 }
133 - if adminLeases[0].Hostname != registered.Hostname {
134 - t.Fatalf("AdminLeases()[0] hostname = %q, want %q", adminLeases[0].Hostname, registered.Hostname)
132 + if adminLeases[0].Hostname != publicHostname {
133 + t.Fatalf("AdminLeases()[0] hostname = %q, want %q", adminLeases[0].Hostname, publicHostname)
134 }
135
136 if _, _, err := registry.Register(types.RegisterChallengeRequest{
portal/server_test.go
+6 -6
@@ -263,7 +263,7 @@ func TestServerStartDomainReportsCompatibilityInfo(t *testing.T) {
263 }
264 }
265
266 -func TestRegisterLeaseOmitsSNIPortWithoutUDP(t *testing.T) {
266 +func TestRegisterLeaseIncludesSNIPortForPublicIngress(t *testing.T) {
267 t.Parallel()
268
269 server, err := NewServer(ServerConfig{
@@ -292,8 +292,8 @@ func TestRegisterLeaseOmitsSNIPortWithoutUDP(t *testing.T) {
292 record.Close()
293 })
294
295 - if resp.SNIPort != 0 {
296 - t.Fatalf("RegisterResponse.SNIPort = %d, want 0 without udp", resp.SNIPort)
295 + if resp.SNIPort != server.cfg.SNIPort {
296 + t.Fatalf("RegisterResponse.SNIPort = %d, want %d", resp.SNIPort, server.cfg.SNIPort)
297 }
298 }
299
@@ -374,7 +374,7 @@ func TestRegisterLeaseDerivesFixedHostnameFromName(t *testing.T) {
374 t.Fatalf("NewServer() error = %v", err)
375 }
376
377 - record, resp, err := server.registry.Register(types.RegisterChallengeRequest{
377 + record, _, err := server.registry.Register(types.RegisterChallengeRequest{
378 Identity: types.Identity{
379 Name: "Demo-App",
380 Address: server.identity.Address,
@@ -385,8 +385,8 @@ func TestRegisterLeaseDerivesFixedHostnameFromName(t *testing.T) {
385 }
386
387 wantHostname := "demo-app.portal.example.com"
388 - if resp.Hostname != wantHostname {
389 - t.Fatalf("registry.Register() hostname = %q, want %q", resp.Hostname, wantHostname)
388 + if record.Hostname != wantHostname {
389 + t.Fatalf("registry.Register() route hostname = %q, want %q", record.Hostname, wantHostname)
390 }
391
392 lease := server.registry.publicLease(record)
sdk/api_client.go
+21 -36
@@ -87,7 +87,7 @@ func (l *listener) initHTTPTransport(ctx context.Context) error {
87 return nil
88 }
89
90 -func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnabled, tcpEnabled bool) (types.RegisterResponse, []types.HopRoute, string, error) {
90 +func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnabled, tcpEnabled bool) (types.RegisterResponse, []types.HopRoute, string, string, error) {
91 var exitHopToken string
92 var publicHostname string
93 var routeHostname string
@@ -96,27 +96,15 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
96 var hopPath []types.RelayDescriptor
97 streamLease := !udpEnabled && !tcpEnabled
98 registerIdentity := l.identity
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)
103 - }
104 - sum := sha256.Sum256([]byte(token))
105 - label := "lid-" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(sum[:20]))
106 - registerIdentity.Name, err = utils.NormalizeDNSLabel(label)
107 - if err != nil {
108 - return types.RegisterResponse{}, nil, "", fmt.Errorf("derive opaque lease name: %w", err)
109 - }
110 - }
99 if len(l.multiHop) > 0 {
100 if !streamLease {
113 - return types.RegisterResponse{}, nil, "", errors.New("multi-hop requires stream lease")
101 + return types.RegisterResponse{}, nil, "", "", errors.New("multi-hop requires stream lease")
102 }
103 if len(l.multiHop) < 2 {
116 - return types.RegisterResponse{}, nil, "", errors.New("multi-hop requires at least entry and exit relay urls")
104 + return types.RegisterResponse{}, nil, "", "", errors.New("multi-hop requires at least entry and exit relay urls")
105 }
106 if l.relaySet == nil {
119 - return types.RegisterResponse{}, nil, "", errors.New("multi-hop relay set is unavailable")
107 + return types.RegisterResponse{}, nil, "", "", errors.New("multi-hop relay set is unavailable")
108 }
109
110 now := time.Now().UTC()
@@ -124,31 +112,31 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
112 for i, relayURL := range l.multiHop {
113 desc, ok := l.relaySet.OverlayRelayDescriptor(relayURL, now)
114 if !ok {
127 - return types.RegisterResponse{}, nil, "", fmt.Errorf("multi-hop relay %d descriptor is unavailable", i)
115 + return types.RegisterResponse{}, nil, "", "", fmt.Errorf("multi-hop relay %d descriptor is unavailable", i)
116 }
117 hopPath = append(hopPath, desc)
118 }
119
120 rootHostname = utils.PortalRootHost(hopPath[0].APIHTTPSAddr)
133 - } else if streamLease {
121 + } else {
122 rootHostname = utils.PortalRootHost(l.relayURL.String())
123 }
124
125 + var err error
126 + publicHostname, err = utils.LeaseHostname(l.identity.Name, rootHostname)
127 + if err != nil {
128 + return types.RegisterResponse{}, nil, "", "", err
129 + }
130 if streamLease {
138 - var err error
139 - publicHostname, err = utils.LeaseHostname(l.identity.Name, rootHostname)
140 - if err != nil {
141 - return types.RegisterResponse{}, nil, "", err
142 - }
131 routeToken, err := l.identity.DeriveToken("ech-route", publicHostname, rootHostname)
132 if err != nil {
145 - return types.RegisterResponse{}, nil, "", err
133 + return types.RegisterResponse{}, nil, "", "", err
134 }
135 routeSum := sha256.Sum256([]byte(routeToken))
136 routeLabel := "ech-" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(routeSum[:20]))
137 routeHostname, err = utils.LeaseHostname(routeLabel, rootHostname)
138 if err != nil {
151 - return types.RegisterResponse{}, nil, "", err
139 + return types.RegisterResponse{}, nil, "", "", err
140 }
141 }
142
@@ -164,7 +152,7 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
152 hopPath[i+1].APIHTTPSAddr,
153 )
154 if err != nil {
167 - return types.RegisterResponse{}, nil, "", err
155 + return types.RegisterResponse{}, nil, "", "", err
156 }
157 forwardToken := "hpt_" + token
158 route := types.HopRoute{
@@ -201,12 +189,12 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
189
190 var challenge types.RegisterChallengeResponse
191 if err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKRegisterChallenge, registerReq, nil, &challenge); err != nil {
204 - return types.RegisterResponse{}, nil, "", err
192 + return types.RegisterResponse{}, nil, "", "", err
193 }
194
195 signature, err := utils.SignEthereumPersonalMessage(challenge.SIWEMessage, l.identity.PrivateKey)
196 if err != nil {
209 - return types.RegisterResponse{}, nil, "", err
197 + return types.RegisterResponse{}, nil, "", "", err
198 }
199
200 var resp types.RegisterResponse
@@ -216,27 +204,24 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
204 SIWESignature: signature,
205 ReportedIP: utils.ResolvePublicIP(ctx),
206 }, nil, &resp); err != nil {
219 - return types.RegisterResponse{}, nil, "", err
207 + return types.RegisterResponse{}, nil, "", "", err
208 }
209 registeredIdentity, err := utils.NormalizeIdentity(resp.Identity)
210 if err != nil {
211 _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
224 - return types.RegisterResponse{}, nil, "", err
212 + return types.RegisterResponse{}, nil, "", "", err
213 }
214 if registeredIdentity.Key() != registerIdentity.Key() {
215 _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
228 - return types.RegisterResponse{}, nil, "", errors.New("relay returned mismatched lease identity")
216 + return types.RegisterResponse{}, nil, "", "", errors.New("relay returned mismatched lease identity")
217 }
218 if len(hopRoutes) > 0 {
219 if err := l.syncHopRoutes(ctx, http.MethodPost, resp.ExpiresAt, hopRoutes); err != nil {
220 _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
233 - return types.RegisterResponse{}, nil, "", err
221 + return types.RegisterResponse{}, nil, "", "", err
222 }
223 }
236 - if publicHostname != "" {
237 - resp.Hostname = publicHostname
238 - }
239 - return resp, hopRoutes, routeHostname, nil
224 + return resp, hopRoutes, publicHostname, routeHostname, nil
225 }
226
227 func (l *listener) renewRegisteredLease(ctx context.Context, ttl time.Duration, accessToken string, hopRoutes []types.HopRoute) (types.RenewResponse, error) {
sdk/listener.go
+12 -9
@@ -379,8 +379,13 @@ func (l *listener) publicURLForLease(lease listenerLease) string {
379 }
380
381 host := lease.hostname
382 - if port := baseURL.Port(); port != "" {
383 - host = net.JoinHostPort(lease.hostname, port)
382 + sniPort := lease.sniPort
383 + scheme := strings.ToLower(strings.TrimSpace(baseURL.Scheme))
384 + if (scheme == "https" && sniPort == 443) || (scheme == "http" && sniPort == 80) {
385 + sniPort = 0
386 + }
387 + if sniPort > 0 {
388 + host = net.JoinHostPort(lease.hostname, fmt.Sprintf("%d", sniPort))
389 }
390
391 return (&url.URL{
@@ -697,7 +702,7 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
702 return err
703 }
704
700 - resp, hopRoutes, routeHostname, err := l.registerLease(ctx, l.leaseTTL, l.udpEnabled, l.tcpEnabled)
705 + resp, hopRoutes, publicHostname, routeHostname, err := l.registerLease(ctx, l.leaseTTL, l.udpEnabled, l.tcpEnabled)
706 if err != nil {
707 return err
708 }
@@ -729,7 +734,7 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
734 var echKeys []tls.EncryptedClientHelloKey
735 var echConfigList []byte
736 if routeHostname != "" {
732 - echSeed, err := l.identity.DeriveToken("tenant-ech", resp.Hostname, routeHostname)
737 + echSeed, err := l.identity.DeriveToken("tenant-ech", publicHostname, routeHostname)
738 if err != nil {
739 _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
740 return fmt.Errorf("derive tenant ech seed: %w", err)
@@ -742,7 +747,7 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
747 echConfigList = keyless.EncryptedClientHelloConfigList(echKeys)
748 }
749
745 - tlsConf, tenantTLSCloser, err := keyless.BuildClientTLSConfig(keylessURL, resp.Hostname, echKeys)
750 + tlsConf, tenantTLSCloser, err := keyless.BuildClientTLSConfig(keylessURL, publicHostname, echKeys)
751 if err != nil {
752 _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
753 if tenantTLSCloser != nil {
@@ -759,20 +764,18 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
764 return ctx.Err()
765 }
766 next := &listenerLease{
762 - hostname: resp.Hostname,
767 + hostname: publicHostname,
768 echConfigList: echConfigList,
769 udpAddr: resp.UDPAddr,
770 tcpAddr: resp.TCPAddr,
771 accessToken: resp.AccessToken,
772 expiresAt: resp.ExpiresAt,
773 + sniPort: resp.SNIPort,
774 publicURLBase: publicURLBase,
775 tlsConfig: tlsConf,
776 tlsCloser: tenantTLSCloser,
777 hopRoutes: hopRoutes,
778 }
773 - if l.udpEnabled {
774 - next.sniPort = resp.SNIPort
775 - }
779 l.leaseMu.Lock()
780 oldLease := l.lease
781 l.lease = next
types/api.go
-1
@@ -83,7 +83,6 @@ type RegisterChallengeResponse struct {
83 type RegisterResponse struct {
84 Identity Identity `json:"identity"`
85 ExpiresAt time.Time `json:"expires_at"`
86 - Hostname string `json:"hostname"`
86 AccessToken string `json:"access_token"`
87 SNIPort int `json:"sni_port,omitempty"`
88 UDPAddr string `json:"udp_addr,omitempty"`