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