refact: extract types, table-driven dispatch, and deduplicate patterns
Replace repetitive switch cases in admin identity operations and QUIC error handling with table-driven dispatch. Extract udpFlowManager from 168-line closure-heavy function. Unify lease snapshot collection and predicate-based counting. Consolidate help commands and shared flags.
cognitive committed
Apr 4, 2026 at 08:51 UTC
e0d2aaecfdba8e59fd5f2742056f8b8420e574b6
9 files changed
+316
-308
cmd/demo-app/main.go
+20
-42
@@ -20,10 +20,13 @@ import (
20
func main() {
21
log.Logger = log.Output(zerolog.NewConsoleWriter())
22
if err := utils.RunCommands(os.Args[1:], os.Stdout, os.Stderr, printRootUsage, map[string]utils.CommandFunc{
23
- "": runTCPCommand,
24
- "tcp": runTCPCommand,
25
- "udp": runUDPCommand,
26
- "help": runHelpCommand,
23
+ "": runTCPCommand,
24
+ "tcp": runTCPCommand,
25
+ "udp": runUDPCommand,
26
+ "help": utils.MakeHelpCommand(printRootUsage, []utils.HelpTopic{
27
+ {Name: "tcp", Usage: printTCPUsage},
28
+ {Name: "udp", Usage: printUDPUsage},
29
+ }),
30
}); err != nil {
31
log.Error().Err(err).Msg("demo command failed")
32
os.Exit(1)
@@ -45,20 +48,26 @@ type demoConfig struct {
48
thumbnail string
49
}
50
48
-func runTCPCommand(args []string) error {
49
- cfg := demoConfig{}
50
-
51
- fs := utils.NewFlagSet("demo-app", printTCPUsage)
52
- utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://gosunuts.xyz", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays when discovery is enabled)", "RELAYS")
51
+// registerConnectivityFlags registers the relay, discovery, identity, and
52
+// owner flags that are shared across TCP and UDP demo commands.
53
+func registerConnectivityFlags(fs *flag.FlagSet, cfg *demoConfig, defaultRelays string) {
54
+ utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", defaultRelays, "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays when discovery is enabled)", "RELAYS")
55
utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include public registry relays and enable discovery", "DISCOVERY")
56
utils.BoolFlagEnv(fs, &cfg.banMITM, "ban-mitm", false, "ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
57
utils.StringFlagEnv(fs, &cfg.identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
58
utils.StringFlagEnv(fs, &cfg.identityJSON, "identity-json", "", "identity json payload; overrides --identity-path contents and is persisted there when both are set", "IDENTITY_JSON")
59
+ utils.StringFlag(fs, &cfg.owner, "owner", "PortalApp Developer", "lease owner")
60
+}
61
+
62
+func runTCPCommand(args []string) error {
63
+ cfg := demoConfig{}
64
+
65
+ fs := utils.NewFlagSet("demo-app", printTCPUsage)
66
+ registerConnectivityFlags(fs, &cfg, "https://gosunuts.xyz")
67
utils.StringFlag(fs, &cfg.addr, "addr", "127.0.0.1:8092", "local demo HTTP listen address (host:port or URL; disable if empty)")
68
utils.StringFlag(fs, &cfg.name, "name", "demo-app", "public hostname prefix (single DNS label)")
69
utils.StringFlag(fs, &cfg.desc, "description", "Portal demo connectivity app", "lease description")
70
utils.StringFlag(fs, &cfg.tags, "tags", "demo,connectivity,activity,cloud,sun,morning", "comma-separated lease tags")
61
- utils.StringFlag(fs, &cfg.owner, "owner", "PortalApp Developer", "lease owner")
71
utils.StringFlag(fs, &cfg.thumbnail, "thumbnail", "https://picsum.photos/640/360", "lease thumbnail")
72
utils.BoolFlag(fs, &cfg.hide, "hide", false, "hide this lease from listings")
73
@@ -88,15 +97,10 @@ func runUDPCommand(args []string) error {
97
cfg := demoConfig{}
98
fs := utils.NewFlagSet("demo-app-udp", printUDPUsage)
99
91
- utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://localhost:4017", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays when discovery is enabled)", "RELAYS")
92
- utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include public registry relays and enable discovery", "DISCOVERY")
93
- utils.BoolFlagEnv(fs, &cfg.banMITM, "ban-mitm", false, "ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
94
- utils.StringFlagEnv(fs, &cfg.identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
95
- utils.StringFlagEnv(fs, &cfg.identityJSON, "identity-json", "", "identity json payload; overrides --identity-path contents and is persisted there when both are set", "IDENTITY_JSON")
100
+ registerConnectivityFlags(fs, &cfg, "https://localhost:4017")
101
utils.StringFlag(fs, &cfg.name, "name", "demo-udp", "public hostname prefix (single DNS label)")
102
utils.StringFlag(fs, &cfg.desc, "description", "Portal demo UDP echo service", "lease description")
103
utils.StringFlag(fs, &cfg.tags, "tags", "demo,udp,echo", "comma-separated lease tags")
99
- utils.StringFlag(fs, &cfg.owner, "owner", "PortalApp Developer", "lease owner")
104
utils.StringFlag(fs, &cfg.thumbnail, "thumbnail", "", "lease thumbnail")
105
utils.BoolFlag(fs, &cfg.hide, "hide", true, "hide this lease from listings")
106
@@ -122,32 +126,6 @@ func runUDPCommand(args []string) error {
126
return runUDPDemo(ctx, cfg)
127
}
128
125
-func runHelpCommand(args []string) error {
126
- if len(args) == 0 {
127
- printRootUsage(os.Stdout)
128
- return nil
129
- }
130
- if len(args) > 1 {
131
- printRootUsage(os.Stderr)
132
- return errors.New("only one help topic is supported")
133
- }
134
-
135
- switch args[0] {
136
- case "", "help", "-h", "--help":
137
- printRootUsage(os.Stdout)
138
- return nil
139
- case "tcp":
140
- printTCPUsage(os.Stdout)
141
- return nil
142
- case "udp":
143
- printUDPUsage(os.Stdout)
144
- return nil
145
- default:
146
- printRootUsage(os.Stderr)
147
- return fmt.Errorf("unknown help topic %q", args[0])
148
- }
149
-}
150
-
129
func runTCPDemo(ctx context.Context, cfg demoConfig) error {
130
exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
131
RelayURLs: utils.SplitCSV(cfg.relayURLs),
cmd/portal-tunnel/main.go
+4
-27
@@ -25,7 +25,10 @@ func main() {
25
if err := utils.RunCommands(os.Args[1:], os.Stdout, os.Stderr, printRootUsage, map[string]utils.CommandFunc{
26
"expose": runExposeCommand,
27
"list": runListCommand,
28
- "help": runHelpCommand,
28
+ "help": utils.MakeHelpCommand(printRootUsage, []utils.HelpTopic{
29
+ {Name: "expose", Usage: printExposeUsage},
30
+ {Name: "list", Usage: printListUsage},
31
+ }),
32
}); err != nil {
33
log.Error().Err(err).Msg("portal tunnel exited with error")
34
os.Exit(1)
@@ -203,32 +206,6 @@ func runListCommand(args []string) error {
206
return nil
207
}
208
206
-func runHelpCommand(args []string) error {
207
- if len(args) == 0 {
208
- printRootUsage(os.Stdout)
209
- return nil
210
- }
211
- if len(args) > 1 {
212
- printRootUsage(os.Stderr)
213
- return errors.New("only one help topic is supported")
214
- }
215
-
216
- switch strings.TrimSpace(args[0]) {
217
- case "", "help", "-h", "--help":
218
- printRootUsage(os.Stdout)
219
- return nil
220
- case "expose":
221
- printExposeUsage(os.Stdout)
222
- return nil
223
- case "list":
224
- printListUsage(os.Stdout)
225
- return nil
226
- default:
227
- printRootUsage(os.Stderr)
228
- return fmt.Errorf("unknown help topic %q", strings.TrimSpace(args[0]))
229
- }
230
-}
231
-
209
var exposeNameOpeners = []string{
210
"arcade", "bouncy", "bravo", "bubble", "candy", "cosmic", "dapper", "electric",
211
"fancy", "fizzy", "flashy", "fuzzy", "gentle", "glitter", "golden", "happy",
cmd/portal-tunnel/relays.go
+3
-118
@@ -248,123 +248,8 @@ func proxyExposureDatagrams(ctx context.Context, exposure *sdk.Exposure, localAd
248
return fmt.Errorf("resolve udp addr %q: %w", localAddr, err)
249
}
250
251
- type flowKey struct {
252
- flowID uint32
253
- address string
254
- relayURL string
255
- }
256
- type flowEntry struct {
257
- conn *net.UDPConn
258
- lastSeen time.Time
259
- frame types.DatagramFrame
260
- }
261
-
262
- var mu sync.Mutex
263
- flows := make(map[flowKey]*flowEntry)
264
-
265
- go func() {
266
- ticker := time.NewTicker(15 * time.Second)
267
- defer ticker.Stop()
268
- for {
269
- select {
270
- case <-ctx.Done():
271
- return
272
- case <-ticker.C:
273
- mu.Lock()
274
- now := time.Now()
275
- for key, f := range flows {
276
- if now.Sub(f.lastSeen) > 30*time.Second {
277
- _ = f.conn.Close()
278
- delete(flows, key)
279
- }
280
- }
281
- mu.Unlock()
282
- }
283
- }
284
- }()
285
-
286
- getOrCreateFlow := func(frame types.DatagramFrame) (*net.UDPConn, error) {
287
- key := flowKey{
288
- flowID: frame.FlowID,
289
- address: frame.Address,
290
- relayURL: frame.RelayURL,
291
- }
292
-
293
- mu.Lock()
294
- if f, ok := flows[key]; ok {
295
- f.lastSeen = time.Now()
296
- mu.Unlock()
297
- return f.conn, nil
298
- }
299
- mu.Unlock()
300
-
301
- localConn, err := net.DialUDP("udp", nil, resolvedAddr)
302
- if err != nil {
303
- return nil, err
304
- }
305
-
306
- mu.Lock()
307
- if f, ok := flows[key]; ok {
308
- mu.Unlock()
309
- _ = localConn.Close()
310
- f.lastSeen = time.Now()
311
- return f.conn, nil
312
- }
313
- flows[key] = &flowEntry{
314
- conn: localConn,
315
- lastSeen: time.Now(),
316
- frame: types.DatagramFrame{
317
- FlowID: frame.FlowID,
318
- Address: frame.Address,
319
- RelayURL: frame.RelayURL,
320
- UDPAddr: frame.UDPAddr,
321
- },
322
- }
323
- mu.Unlock()
324
-
325
- go func() {
326
- buf := make([]byte, 65535)
327
- for {
328
- n, err := localConn.Read(buf)
329
- if err != nil {
330
- if ctx.Err() != nil {
331
- return
332
- }
333
- log.Debug().
334
- Err(err).
335
- Uint32("flow_id", key.flowID).
336
- Str("address", key.address).
337
- Str("relay_url", key.relayURL).
338
- Msg("local read ended")
339
- return
340
- }
341
-
342
- mu.Lock()
343
- entry := flows[key]
344
- if entry != nil {
345
- entry.lastSeen = time.Now()
346
- }
347
- replyFrame := types.DatagramFrame{}
348
- if entry != nil {
349
- replyFrame = entry.frame
350
- replyFrame.Payload = append([]byte(nil), buf[:n]...)
351
- }
352
- mu.Unlock()
353
-
354
- if sendErr := exposure.SendDatagram(replyFrame); sendErr != nil {
355
- log.Debug().
356
- Err(sendErr).
357
- Uint32("flow_id", key.flowID).
358
- Str("address", key.address).
359
- Str("relay_url", key.relayURL).
360
- Msg("send datagram to relay failed")
361
- return
362
- }
363
- }
364
- }()
365
-
366
- return localConn, nil
367
- }
251
+ mgr := newUDPFlowManager(resolvedAddr, exposure)
252
+ go mgr.runCleanup(ctx)
253
254
log.Info().Str("target", localAddr).Msg("udp proxy loop started, waiting for datagrams")
255
for {
@@ -388,7 +273,7 @@ func proxyExposureDatagrams(ctx context.Context, exposure *sdk.Exposure, localAd
273
Str("target", localAddr).
274
Msg("datagram received from relay, forwarding to local")
275
391
- localConn, err := getOrCreateFlow(frame)
276
+ localConn, err := mgr.getOrCreate(ctx, frame)
277
if err != nil {
278
log.Warn().
279
Err(err).
cmd/portal-tunnel/udp_flow.go
new
+155
@@ -0,0 +1,155 @@
1
+package main
2
+
3
+import (
4
+ "context"
5
+ "net"
6
+ "sync"
7
+ "time"
8
+
9
+ "github.com/rs/zerolog/log"
10
+
11
+ "github.com/gosuda/portal/v2/sdk"
12
+ "github.com/gosuda/portal/v2/types"
13
+)
14
+
15
+type udpFlowKey struct {
16
+ flowID uint32
17
+ address string
18
+ relayURL string
19
+}
20
+
21
+type udpFlowEntry struct {
22
+ conn *net.UDPConn
23
+ lastSeen time.Time
24
+ frame types.DatagramFrame
25
+}
26
+
27
+type udpFlowManager struct {
28
+ target *net.UDPAddr
29
+ exposure *sdk.Exposure
30
+ mu sync.Mutex
31
+ flows map[udpFlowKey]*udpFlowEntry
32
+}
33
+
34
+func newUDPFlowManager(target *net.UDPAddr, exposure *sdk.Exposure) *udpFlowManager {
35
+ return &udpFlowManager{
36
+ target: target,
37
+ exposure: exposure,
38
+ flows: make(map[udpFlowKey]*udpFlowEntry),
39
+ }
40
+}
41
+
42
+func (m *udpFlowManager) runCleanup(ctx context.Context) {
43
+ ticker := time.NewTicker(15 * time.Second)
44
+ defer ticker.Stop()
45
+ for {
46
+ select {
47
+ case <-ctx.Done():
48
+ return
49
+ case <-ticker.C:
50
+ m.mu.Lock()
51
+ now := time.Now()
52
+ for key, f := range m.flows {
53
+ if now.Sub(f.lastSeen) > 30*time.Second {
54
+ _ = f.conn.Close()
55
+ delete(m.flows, key)
56
+ }
57
+ }
58
+ m.mu.Unlock()
59
+ }
60
+ }
61
+}
62
+
63
+func (m *udpFlowManager) getOrCreate(ctx context.Context, frame types.DatagramFrame) (*net.UDPConn, error) {
64
+ key := udpFlowKey{
65
+ flowID: frame.FlowID,
66
+ address: frame.Address,
67
+ relayURL: frame.RelayURL,
68
+ }
69
+
70
+ m.mu.Lock()
71
+ if f, ok := m.flows[key]; ok {
72
+ f.lastSeen = time.Now()
73
+ m.mu.Unlock()
74
+ return f.conn, nil
75
+ }
76
+ m.mu.Unlock()
77
+
78
+ localConn, err := net.DialUDP("udp", nil, m.target)
79
+ if err != nil {
80
+ return nil, err
81
+ }
82
+
83
+ m.mu.Lock()
84
+ if f, ok := m.flows[key]; ok {
85
+ m.mu.Unlock()
86
+ _ = localConn.Close()
87
+ f.lastSeen = time.Now()
88
+ return f.conn, nil
89
+ }
90
+ m.flows[key] = &udpFlowEntry{
91
+ conn: localConn,
92
+ lastSeen: time.Now(),
93
+ frame: types.DatagramFrame{
94
+ FlowID: frame.FlowID,
95
+ Address: frame.Address,
96
+ RelayURL: frame.RelayURL,
97
+ UDPAddr: frame.UDPAddr,
98
+ },
99
+ }
100
+ m.mu.Unlock()
101
+
102
+ go m.readLoop(ctx, key, localConn)
103
+ return localConn, nil
104
+}
105
+
106
+func (m *udpFlowManager) removeFlow(key udpFlowKey) {
107
+ m.mu.Lock()
108
+ if f, ok := m.flows[key]; ok {
109
+ _ = f.conn.Close()
110
+ delete(m.flows, key)
111
+ }
112
+ m.mu.Unlock()
113
+}
114
+
115
+func (m *udpFlowManager) readLoop(ctx context.Context, key udpFlowKey, conn *net.UDPConn) {
116
+ buf := make([]byte, 65535)
117
+ for {
118
+ n, err := conn.Read(buf)
119
+ if err != nil {
120
+ if ctx.Err() != nil {
121
+ return
122
+ }
123
+ log.Debug().
124
+ Err(err).
125
+ Uint32("flow_id", key.flowID).
126
+ Str("address", key.address).
127
+ Str("relay_url", key.relayURL).
128
+ Msg("local read ended")
129
+ m.removeFlow(key)
130
+ return
131
+ }
132
+
133
+ m.mu.Lock()
134
+ entry := m.flows[key]
135
+ if entry == nil {
136
+ m.mu.Unlock()
137
+ return
138
+ }
139
+ entry.lastSeen = time.Now()
140
+ replyFrame := entry.frame
141
+ replyFrame.Payload = append([]byte(nil), buf[:n]...)
142
+ m.mu.Unlock()
143
+
144
+ if sendErr := m.exposure.SendDatagram(replyFrame); sendErr != nil {
145
+ log.Debug().
146
+ Err(sendErr).
147
+ Uint32("flow_id", key.flowID).
148
+ Str("address", key.address).
149
+ Str("relay_url", key.relayURL).
150
+ Msg("send datagram to relay failed")
151
+ m.removeFlow(key)
152
+ return
153
+ }
154
+ }
155
+}
cmd/relay-server/admin.go
+49
-59
@@ -263,70 +263,60 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
263
return
264
}
265
identityKey := identity.Key()
266
+ approver := runtime.Approver()
267
267
- switch parts[2] {
268
- case "ban":
269
- switch r.Method {
270
- case http.MethodPost:
271
- runtime.BanIdentity(identityKey)
272
- case http.MethodDelete:
273
- runtime.UnbanIdentity(identityKey)
274
- default:
275
- methodNotAllowed.Write(w)
276
- return
277
- }
278
- saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
279
- utils.WriteAPIData(w, http.StatusOK, map[string]any{})
280
- case "bps":
281
- switch r.Method {
282
- case http.MethodPost:
283
- req, ok := utils.DecodeJSONRequestAs[types.AdminBPSRequest](w, r, 1<<16, invalidRequestBody)
284
- if !ok {
285
- return
286
- }
287
- if req.BPS <= 0 {
288
- utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, "bps must be greater than zero")
289
- return
290
- }
291
- runtime.BPSManager().SetIdentityBPS(identityKey, req.BPS)
292
- case http.MethodDelete:
293
- runtime.BPSManager().DeleteIdentityBPS(identityKey)
294
- default:
295
- methodNotAllowed.Write(w)
296
- return
297
- }
298
- saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
299
- utils.WriteAPIData(w, http.StatusOK, map[string]any{})
300
- case "approve":
301
- approver := runtime.Approver()
302
- switch r.Method {
303
- case http.MethodPost:
304
- approver.Approve(identityKey)
305
- approver.Undeny(identityKey)
306
- case http.MethodDelete:
307
- approver.Revoke(identityKey)
308
- default:
309
- methodNotAllowed.Write(w)
310
- return
311
- }
312
- saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
313
- utils.WriteAPIData(w, http.StatusOK, map[string]any{})
314
- case "deny":
315
- approver := runtime.Approver()
316
- switch r.Method {
317
- case http.MethodPost:
318
- approver.Deny(identityKey)
319
- case http.MethodDelete:
320
- approver.Undeny(identityKey)
321
- default:
322
- methodNotAllowed.Write(w)
268
+ type identityAction struct {
269
+ post func() bool // returns true if response was already written (error path)
270
+ delete func()
271
+ }
272
+ actions := map[string]identityAction{
273
+ "ban": {
274
+ post: func() bool { runtime.BanIdentity(identityKey); return false },
275
+ delete: func() { runtime.UnbanIdentity(identityKey) },
276
+ },
277
+ "bps": {
278
+ post: func() bool {
279
+ req, ok := utils.DecodeJSONRequestAs[types.AdminBPSRequest](w, r, 1<<16, invalidRequestBody)
280
+ if !ok {
281
+ return true
282
+ }
283
+ if req.BPS <= 0 {
284
+ utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, "bps must be greater than zero")
285
+ return true
286
+ }
287
+ runtime.BPSManager().SetIdentityBPS(identityKey, req.BPS)
288
+ return false
289
+ },
290
+ delete: func() { runtime.BPSManager().DeleteIdentityBPS(identityKey) },
291
+ },
292
+ "approve": {
293
+ post: func() bool { approver.Approve(identityKey); approver.Undeny(identityKey); return false },
294
+ delete: func() { approver.Revoke(identityKey) },
295
+ },
296
+ "deny": {
297
+ post: func() bool { approver.Deny(identityKey); return false },
298
+ delete: func() { approver.Undeny(identityKey) },
299
+ },
300
+ }
301
+
302
+ action, ok := actions[parts[2]]
303
+ if !ok {
304
+ http.NotFound(w, r)
305
+ return
306
+ }
307
+ switch r.Method {
308
+ case http.MethodPost:
309
+ if action.post() {
310
return
311
}
325
- saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
326
- utils.WriteAPIData(w, http.StatusOK, map[string]any{})
312
+ case http.MethodDelete:
313
+ action.delete()
314
default:
328
- http.NotFound(w, r)
315
+ methodNotAllowed.Write(w)
316
+ return
317
}
318
+ saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
319
+ utils.WriteAPIData(w, http.StatusOK, map[string]any{})
320
case strings.HasPrefix(path, types.PathAdminIPsPrefix):
321
if !strings.HasSuffix(path, "/ban") {
322
http.NotFound(w, r)
portal/api_server.go
+25
-21
@@ -48,6 +48,17 @@ var (
48
errTCPPortExhausted = &apiError{types.APIErrorCodeTCPPortExhausted, "no tcp ports available", http.StatusServiceUnavailable}
49
)
50
51
+var quicRejectTable = []struct {
52
+ sentinel error
53
+ code string
54
+ reason string
55
+}{
56
+ {errLeaseNotFound, types.APIErrorCodeLeaseNotFound, "lease not found"},
57
+ {errLeaseRejected, types.APIErrorCodeLeaseRejected, "lease rejected"},
58
+ {errUnauthorized, types.APIErrorCodeUnauthorized, "unauthorized"},
59
+ {errTransportMismatch, types.APIErrorCodeTransportMismatch, "transport mismatch"},
60
+}
61
+
62
func writeAPIErrorResponse(w http.ResponseWriter, err error) {
63
var ae *apiError
64
if errors.As(err, &ae) {
@@ -441,28 +452,21 @@ func (s *Server) handleQUICTunnelConn(conn *quic.Conn) {
452
return
453
}
454
455
+ rejectConn := func(code, reason string) {
456
+ _ = json.NewEncoder(stream).Encode(types.QUICControlResponse{OK: false, Error: code})
457
+ _ = conn.CloseWithError(1, reason)
458
+ }
459
+
460
lease, err := s.admitLeaseByToken(msg.AccessToken, true)
445
- switch {
446
- case err == nil:
447
- case errors.Is(err, errLeaseNotFound):
448
- _ = json.NewEncoder(stream).Encode(types.QUICControlResponse{OK: false, Error: types.APIErrorCodeLeaseNotFound})
449
- _ = conn.CloseWithError(1, "lease not found")
450
- return
451
- case errors.Is(err, errLeaseRejected):
452
- _ = json.NewEncoder(stream).Encode(types.QUICControlResponse{OK: false, Error: types.APIErrorCodeLeaseRejected})
453
- _ = conn.CloseWithError(1, "lease rejected")
454
- return
455
- case errors.Is(err, errUnauthorized):
456
- _ = json.NewEncoder(stream).Encode(types.QUICControlResponse{OK: false, Error: types.APIErrorCodeUnauthorized})
457
- _ = conn.CloseWithError(1, "unauthorized")
458
- return
459
- case errors.Is(err, errTransportMismatch):
460
- _ = json.NewEncoder(stream).Encode(types.QUICControlResponse{OK: false, Error: types.APIErrorCodeTransportMismatch})
461
- _ = conn.CloseWithError(1, "transport mismatch")
462
- return
463
- default:
464
- _ = json.NewEncoder(stream).Encode(types.QUICControlResponse{OK: false, Error: types.APIErrorCodeInvalidRequest})
465
- _ = conn.CloseWithError(1, "invalid control message")
461
+ if err != nil {
462
+ code, reason := types.APIErrorCodeInvalidRequest, "invalid control message"
463
+ for _, entry := range quicRejectTable {
464
+ if errors.Is(err, entry.sentinel) {
465
+ code, reason = entry.code, entry.reason
466
+ break
467
+ }
468
+ }
469
+ rejectConn(code, reason)
470
return
471
}
472
portal/lease.go
+15
-6
@@ -258,30 +258,39 @@ func (r *leaseRegistry) cleanupExpired(now time.Time) []*leaseRecord {
258
return expired
259
}
260
261
-func (r *leaseRegistry) CountDatagramLeases() int {
261
+func (r *leaseRegistry) countActiveLeasesWhere(pred func(*leaseRecord) bool) int {
262
r.mu.RLock()
263
defer r.mu.RUnlock()
264
now := time.Now()
265
count := 0
266
for _, record := range r.leasesByKey {
267
- if record.datagram != nil && now.Before(record.ExpiresAt) {
267
+ if now.Before(record.ExpiresAt) && pred(record) {
268
count++
269
}
270
}
271
return count
272
}
273
274
+func (r *leaseRegistry) CountDatagramLeases() int {
275
+ return r.countActiveLeasesWhere(func(rec *leaseRecord) bool { return rec.datagram != nil })
276
+}
277
+
278
func (r *leaseRegistry) CountTCPPortLeases() int {
279
+ return r.countActiveLeasesWhere(func(rec *leaseRecord) bool { return rec.tcpPort != nil })
280
+}
281
+
282
+func (r *leaseRegistry) activeAdminSnapshots() []types.AdminLease {
283
r.mu.RLock()
284
defer r.mu.RUnlock()
285
+
286
now := time.Now()
278
- count := 0
287
+ out := make([]types.AdminLease, 0, len(r.leasesByKey))
288
for _, record := range r.leasesByKey {
280
- if record.tcpPort != nil && now.Before(record.ExpiresAt) {
281
- count++
289
+ if !now.After(record.ExpiresAt) {
290
+ out = append(out, r.AdminSnapshot(record))
291
}
292
}
284
- return count
293
+ return out
294
}
295
296
func (r *leaseRegistry) Snapshot(record *leaseRecord) types.Lease {
portal/server.go
+10
-35
@@ -317,52 +317,27 @@ func (s *Server) PortalURL() string {
317
}
318
319
func (s *Server) LeaseSnapshots() []types.Lease {
320
- s.registry.mu.RLock()
321
- defer s.registry.mu.RUnlock()
322
-
320
now := time.Now()
324
- records := make([]*leaseRecord, 0, len(s.registry.leasesByKey))
325
- for _, record := range s.registry.leasesByKey {
326
- records = append(records, record)
327
- }
328
- snapshots := make([]types.Lease, 0, len(records))
329
- for _, record := range records {
330
- if now.After(record.ExpiresAt) {
331
- continue
332
- }
333
- adminSnapshot := s.registry.AdminSnapshot(record)
321
+ all := s.registry.activeAdminSnapshots()
322
+ out := make([]types.Lease, 0, len(all))
323
+ for _, snap := range all {
324
since := time.Duration(0)
335
- if !adminSnapshot.LastSeenAt.IsZero() {
336
- since = max(now.Sub(adminSnapshot.LastSeenAt), 0)
325
+ if !snap.LastSeenAt.IsZero() {
326
+ since = max(now.Sub(snap.LastSeenAt), 0)
327
}
338
- if adminSnapshot.IsBanned || adminSnapshot.IsDenied || !adminSnapshot.IsApproved || adminSnapshot.Metadata.Hide {
328
+ if snap.IsBanned || snap.IsDenied || !snap.IsApproved || snap.Metadata.Hide {
329
continue
330
}
341
- if adminSnapshot.Ready == 0 && since >= 3*time.Minute {
331
+ if snap.Ready == 0 && since >= 3*time.Minute {
332
continue
333
}
344
- snapshots = append(snapshots, adminSnapshot.Lease)
334
+ out = append(out, snap.Lease)
335
}
346
- return snapshots
336
+ return out
337
}
338
339
func (s *Server) AdminLeaseSnapshots() []types.AdminLease {
350
- s.registry.mu.RLock()
351
- defer s.registry.mu.RUnlock()
352
-
353
- now := time.Now()
354
- records := make([]*leaseRecord, 0, len(s.registry.leasesByKey))
355
- for _, record := range s.registry.leasesByKey {
356
- records = append(records, record)
357
- }
358
- snapshots := make([]types.AdminLease, 0, len(records))
359
- for _, record := range records {
360
- if now.After(record.ExpiresAt) {
361
- continue
362
- }
363
- snapshots = append(snapshots, s.registry.AdminSnapshot(record))
364
- }
365
- return snapshots
340
+ return s.registry.activeAdminSnapshots()
341
}
342
343
func (s *Server) LeaseSnapshotByHostname(hostname string) (types.Lease, bool) {
utils/cmd.go
+35
@@ -351,6 +351,41 @@ func NormalizeLoopbackTarget(raw string) (string, error) {
351
return NormalizeTargetAddr(raw)
352
}
353
354
+// HelpTopic maps a subcommand name to its usage printer.
355
+type HelpTopic struct {
356
+ Name string
357
+ Usage func(io.Writer)
358
+}
359
+
360
+// MakeHelpCommand returns a CommandFunc that dispatches help topics.
361
+// Topics are matched in order; the slice provides deterministic output.
362
+func MakeHelpCommand(rootUsage func(io.Writer), topics []HelpTopic) CommandFunc {
363
+ return func(args []string) error {
364
+ if len(args) == 0 {
365
+ rootUsage(os.Stdout)
366
+ return nil
367
+ }
368
+ if len(args) > 1 {
369
+ rootUsage(os.Stderr)
370
+ return errors.New("only one help topic is supported")
371
+ }
372
+ topic := strings.TrimSpace(args[0])
373
+ switch topic {
374
+ case "", "help", "-h", "--help":
375
+ rootUsage(os.Stdout)
376
+ return nil
377
+ }
378
+ for _, t := range topics {
379
+ if t.Name == topic {
380
+ t.Usage(os.Stdout)
381
+ return nil
382
+ }
383
+ }
384
+ rootUsage(os.Stderr)
385
+ return fmt.Errorf("unknown help topic %q", topic)
386
+ }
387
+}
388
+
389
func WriteCommandUsage(w io.Writer, usage []string, examples []string) {
390
if w == nil {
391
return