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
+}