refact: move tunnel name resolution into utils

rabbitprincess committed Apr 4, 2026 at 21:19 UTC 271ce604966b7c7cf1899cd604459a512de4c521
5 files changed +101 -143
cmd/portal-tunnel/main.go
+1 -136
@@ -6,10 +6,7 @@ import (
6 "flag"
7 "fmt"
8 "io"
9 - "net/url"
9 "os"
11 - "regexp"
12 - "strings"
10 "time"
11
12 "github.com/rs/zerolog"
@@ -98,35 +95,6 @@ func runExposeCommand(args []string) error {
95 printExposeUsage(os.Stderr)
96 return errors.New("--udp cannot be combined with --http-route")
97 }
101 - if flags.name == "" && strings.TrimSpace(flags.identityJSON) != "" {
102 - identity, err := utils.ParseIdentityJSON(flags.identityJSON)
103 - if err != nil {
104 - return fmt.Errorf("parse --identity-json: %w", err)
105 - }
106 - if strings.TrimSpace(identity.Name) != "" {
107 - flags.name = identity.Name
108 - }
109 - }
110 - if flags.name == "" {
111 - storedIdentity, err := utils.LoadIdentity(flags.identityPath)
112 - if err == nil && strings.TrimSpace(storedIdentity.Name) != "" {
113 - flags.name = storedIdentity.Name
114 - }
115 - }
116 - if flags.name == "" {
117 - defaultTarget := flags.targetAddr
118 - if defaultTarget == "" && len(flags.httpRoutes) > 0 {
119 - defaultTarget = strings.Join(flags.httpRoutes, ",")
120 - }
121 - flags.name, err = defaultExposeName(defaultTarget, utils.RandomID("cli_"))
122 - if err != nil {
123 - return fmt.Errorf("derive service name: %w", err)
124 - }
125 - }
126 - flags.name, err = utils.NormalizeDNSLabel(flags.name)
127 - if err != nil {
128 - return fmt.Errorf("invalid service name: %w", err)
129 - }
98 ctx, stop := utils.SignalContext()
99 defer stop()
100
@@ -150,7 +118,7 @@ func runExposeCommand(args []string) error {
118 },
119 })
120 if err != nil {
153 - return fmt.Errorf("service %s: failed to start relays: %w", flags.name, err)
121 + return fmt.Errorf("failed to start relays: %w", err)
122 }
123 if len(flags.httpRoutes) > 0 {
124 handler, err := newHTTPRouteHandler(flags.httpRoutes)
@@ -206,109 +174,6 @@ func runListCommand(args []string) error {
174 return nil
175 }
176
209 -var exposeNameOpeners = []string{
210 - "arcade", "bouncy", "bravo", "bubble", "candy", "cosmic", "dapper", "electric",
211 - "fancy", "fizzy", "flashy", "fuzzy", "gentle", "glitter", "golden", "happy",
212 - "hyper", "jazzy", "jolly", "lively", "lucky", "magic", "mellow", "minty",
213 - "misty", "moonlit", "mystic", "neon", "nova", "peppy", "pixel", "playful",
214 - "poppy", "rapid", "rocket", "rowdy", "snappy", "snazzy", "sparkly", "spicy",
215 - "sprightly", "starry", "sunny", "swift", "tangy", "tidy", "toasty", "turbo",
216 - "velvet", "vivid", "wavy", "whimsy", "wild", "wonky", "zany", "zesty",
217 -}
218 -
219 -var exposeNameCenters = []string{
220 - "alpaca", "badger", "banjo", "beacon", "biscuit", "capybara", "comet", "cricket",
221 - "dragon", "falcon", "feather", "fjord", "fox", "gadget", "gecko", "gizmo",
222 - "harbor", "heron", "iguana", "jelly", "koala", "lemur", "mango", "narwhal",
223 - "nebula", "noodle", "octopus", "otter", "panda", "pepper", "phoenix", "pickle",
224 - "puffin", "quokka", "radar", "ranger", "rocket", "scooter", "seahorse", "skylark",
225 - "sprocket", "starling", "sunbeam", "taco", "thimble", "tiger", "toucan", "triton",
226 - "walrus", "widget", "willow", "wombat", "yeti", "zeppelin", "zigzag", "zinnia",
227 -}
228 -
229 -var exposeNameClosers = []string{
230 - "arcade", "beacon", "boogie", "bounce", "burst", "cascade", "chorus", "dash",
231 - "disco", "drift", "echo", "fiesta", "flare", "flash", "flight", "flip",
232 - "glow", "groove", "jam", "jive", "launch", "loop", "march", "orbit",
233 - "parade", "party", "pulse", "quest", "rally", "riot", "ripple", "rodeo",
234 - "roll", "rush", "serenade", "shuffle", "signal", "sketch", "spark", "sprint",
235 - "starlight", "stride", "sway", "swoop", "twirl", "uplift", "vibe", "voyage",
236 - "whirl", "wink", "zap", "zenith", "zip", "zoom", "zest", "zone",
237 -}
238 -
239 -var digitsOnly = regexp.MustCompile(`^\d+$`)
240 -
241 -func normalizeExposeTarget(raw string) string {
242 - trimmed := strings.TrimSpace(raw)
243 - candidate := trimmed
244 - if candidate == "" {
245 - candidate = "3000"
246 - }
247 -
248 - if digitsOnly.MatchString(candidate) {
249 - return "127.0.0.1:" + candidate
250 - }
251 -
252 - if strings.Contains(candidate, "://") {
253 - parsed, err := url.Parse(candidate)
254 - if err == nil &&
255 - (parsed.Scheme == "http" || parsed.Scheme == "https") &&
256 - parsed.Host != "" &&
257 - (parsed.Path == "" || parsed.Path == "/") &&
258 - parsed.RawQuery == "" &&
259 - parsed.Fragment == "" {
260 - return parsed.Host
261 - }
262 - return candidate
263 - }
264 -
265 - parsed, err := url.Parse("tcp://" + candidate)
266 - if err != nil || parsed.Hostname() == "" {
267 - return candidate
268 - }
269 - port := parsed.Port()
270 - if port == "" {
271 - port = "80"
272 - }
273 - host := parsed.Hostname()
274 - if strings.Contains(host, ":") {
275 - return "[" + host + "]:" + port
276 - }
277 - return host + ":" + port
278 -}
279 -
280 -func fnv1a32(data []byte, seed uint32) uint32 {
281 - h := seed
282 - for _, b := range data {
283 - h ^= uint32(b)
284 - h *= 0x01000193
285 - }
286 - return h
287 -}
288 -
289 -func defaultExposeName(target, rawSeed string) (string, error) {
290 - seed := strings.TrimSpace(rawSeed)
291 - if cut, ok := strings.CutPrefix(seed, "cli_"); ok {
292 - seed = cut
293 - }
294 - if seed == "" {
295 - seed = "portal"
296 - }
297 -
298 - input := []byte(seed + "|" + normalizeExposeTarget(target))
299 - first := fnv1a32(input, 0x811c9dc5)
300 - second := fnv1a32(input, 0x9e3779b9)
301 - third := fnv1a32(input, 0x85ebca6b)
302 -
303 - label := strings.Join([]string{
304 - exposeNameOpeners[int(first&0xff)%len(exposeNameOpeners)],
305 - exposeNameCenters[int(second&0xff)%len(exposeNameCenters)],
306 - exposeNameClosers[int(third&0xff)%len(exposeNameClosers)],
307 - }, "-")
308 -
309 - return utils.NormalizeDNSLabel(label)
310 -}
311 -
177 func printRootUsage(w io.Writer) {
178 utils.WriteCommandUsage(w,
179 []string{
go.sum
+1 -6
@@ -55,8 +55,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
55 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
56 github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
57 github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
58 -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
59 -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
58 github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
59 github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
60 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -115,9 +113,8 @@ go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF
113 go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
114 go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
115 go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
118 -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
119 -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
116 go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
117 +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
118 go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
119 go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
120 go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
@@ -153,8 +150,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:
150 google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
151 google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
152 google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
156 -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
157 -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
153 google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
154 google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
155 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
sdk/expose.go
-1
@@ -133,7 +133,6 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
133
134 return exposure, nil
135 }
136 -
136 func (e *Exposure) ActiveRelayURLs() []string {
137 return e.relaySet.ActiveRelayURLs()
138 }
sdk/sdk_test.go
+3
@@ -122,6 +122,9 @@ func TestExposeLoadsPrivateKeyFromIdentityPath(t *testing.T) {
122 if challengeReq.Identity.Address != identity.Address {
123 t.Fatalf("register challenge Identity.Address = %q, want %q", challengeReq.Identity.Address, identity.Address)
124 }
125 + if challengeReq.Identity.Name != identity.Name {
126 + t.Fatalf("register challenge Identity.Name = %q, want %q", challengeReq.Identity.Name, identity.Name)
127 + }
128 }
129
130 func TestExposeLoadsIdentityFromJSON(t *testing.T) {
utils/identity.go
+96
@@ -201,11 +201,17 @@ func LoadOrCreateIdentity(path string, identity types.Identity) (types.Identity,
201 func ResolveListenerIdentity(identity types.Identity, identityPath, identityJSON string) (types.Identity, bool, error) {
202 identityPath = strings.TrimSpace(identityPath)
203 identityJSON = strings.TrimSpace(identityJSON)
204 + resolvedName, err := resolveExposeName(identity.Name, identityPath, identityJSON)
205 + if err != nil {
206 + return types.Identity{}, false, err
207 + }
208 + identity.Name = resolvedName
209 if identityJSON != "" {
210 provided, err := ParseIdentityJSON(identityJSON)
211 if err != nil {
212 return types.Identity{}, false, err
213 }
214 + provided.Name = identity.Name
215 if identityPath != "" {
216 if err := SaveIdentity(identityPath, provided); err != nil {
217 return types.Identity{}, false, fmt.Errorf("persist identity: %w", err)
@@ -299,3 +305,93 @@ func ResolveLeaseIdentity(identity types.Identity) (types.Identity, error) {
305 resolved.PrivateKey = signingIdentity.PrivateKey
306 return resolved, nil
307 }
308 +
309 +var exposeNameOpeners = []string{
310 + "arcade", "bouncy", "bravo", "bubble", "candy", "cosmic", "dapper", "electric",
311 + "fancy", "fizzy", "flashy", "fuzzy", "gentle", "glitter", "golden", "happy",
312 + "hyper", "jazzy", "jolly", "lively", "lucky", "magic", "mellow", "minty",
313 + "misty", "moonlit", "mystic", "neon", "nova", "peppy", "pixel", "playful",
314 + "poppy", "rapid", "rocket", "rowdy", "snappy", "snazzy", "sparkly", "spicy",
315 + "sprightly", "starry", "sunny", "swift", "tangy", "tidy", "toasty", "turbo",
316 + "velvet", "vivid", "wavy", "whimsy", "wild", "wonky", "zany", "zesty",
317 +}
318 +
319 +var exposeNameCenters = []string{
320 + "alpaca", "badger", "banjo", "beacon", "biscuit", "capybara", "comet", "cricket",
321 + "dragon", "falcon", "feather", "fjord", "fox", "gadget", "gecko", "gizmo",
322 + "harbor", "heron", "iguana", "jelly", "koala", "lemur", "mango", "narwhal",
323 + "nebula", "noodle", "octopus", "otter", "panda", "pepper", "phoenix", "pickle",
324 + "puffin", "quokka", "radar", "ranger", "rocket", "scooter", "seahorse", "skylark",
325 + "sprocket", "starling", "sunbeam", "taco", "thimble", "tiger", "toucan", "triton",
326 + "walrus", "widget", "willow", "wombat", "yeti", "zeppelin", "zigzag", "zinnia",
327 +}
328 +
329 +var exposeNameClosers = []string{
330 + "arcade", "beacon", "boogie", "bounce", "burst", "cascade", "chorus", "dash",
331 + "disco", "drift", "echo", "fiesta", "flare", "flash", "flight", "flip",
332 + "glow", "groove", "jam", "jive", "launch", "loop", "march", "orbit",
333 + "parade", "party", "pulse", "quest", "rally", "riot", "ripple", "rodeo",
334 + "roll", "rush", "serenade", "shuffle", "signal", "sketch", "spark", "sprint",
335 + "starlight", "stride", "sway", "swoop", "twirl", "uplift", "vibe", "voyage",
336 + "whirl", "wink", "zap", "zenith", "zip", "zoom", "zest", "zone",
337 +}
338 +
339 +func DefaultExposeName(rawSeed string) (string, error) {
340 + seed := strings.TrimSpace(rawSeed)
341 + if cut, ok := strings.CutPrefix(seed, "cli_"); ok {
342 + seed = cut
343 + }
344 + if seed == "" {
345 + seed = "portal"
346 + }
347 +
348 + input := []byte(seed)
349 + first := fnv1a32(input, 0x811c9dc5)
350 + second := fnv1a32(input, 0x9e3779b9)
351 + third := fnv1a32(input, 0x85ebca6b)
352 +
353 + label := strings.Join([]string{
354 + exposeNameOpeners[int(first&0xff)%len(exposeNameOpeners)],
355 + exposeNameCenters[int(second&0xff)%len(exposeNameCenters)],
356 + exposeNameClosers[int(third&0xff)%len(exposeNameClosers)],
357 + }, "-")
358 +
359 + return NormalizeDNSLabel(label)
360 +}
361 +
362 +func resolveExposeName(name, identityPath, identityJSON string) (string, error) {
363 + if name = strings.TrimSpace(name); name != "" {
364 + return name, nil
365 + }
366 + if identityJSON = strings.TrimSpace(identityJSON); identityJSON != "" {
367 + identity, err := ParseIdentityJSON(identityJSON)
368 + if err != nil {
369 + return "", err
370 + }
371 + if name := strings.TrimSpace(identity.Name); name != "" {
372 + return name, nil
373 + }
374 + }
375 + if identityPath = strings.TrimSpace(identityPath); identityPath != "" {
376 + identity, err := LoadIdentity(identityPath)
377 + switch {
378 + case err == nil:
379 + if name := strings.TrimSpace(identity.Name); name != "" {
380 + return name, nil
381 + }
382 + case !errors.Is(err, os.ErrNotExist):
383 + return "", err
384 + }
385 + }
386 +
387 + return DefaultExposeName(RandomID("cli_"))
388 +}
389 +
390 +func fnv1a32(data []byte, seed uint32) uint32 {
391 + h := seed
392 + for _, b := range data {
393 + h ^= uint32(b)
394 + h *= 0x01000193
395 + }
396 + return h
397 +}