refact: complete mechanism simplifications and dead code cleanup

- Remove dead functions (AppendUniqueRelayURL, FormatDuration, FormatLastSeen, ParseNonNegativeInt, IntFlag) - Unexport internal symbols (trimmedEnv, resolveStringEnv, resolveBoolEnv, resolveIntEnv, errDatagramTooSmall) - Delete CloseIcon.tsx, remove dead API paths, unexport internal frontend types, remove trivial constant-equality tests - Inline routeTable into leaseRegistry with standalone routeLookup - Switch RelayLocalState to pointer semantics with getOrCreateLocalState - Defer-based cleanup stack in Server.Start() - Extract PortPolicy struct for UDP/TCP policy methods - Extract handlePortSettings helper in admin handlers - Introduce apiError type with writeAPIErrorResponse - Inline WriteAPIEmpty, merge InvalidRequestMessage - Reduce redundant NormalizeEVMAddress calls in identity validation

cognitive committed Apr 4, 2026 at 07:36 UTC 2f87bff4225b06cb27f80df6e6fb5fd4fdeca7cb
18 files changed +179 -368
cmd/relay-server/admin.go
+50 -42
@@ -2,6 +2,7 @@ package main
2
3 import (
4 "crypto/subtle"
5 + "errors"
6 "net"
7 "net/http"
8 "strings"
@@ -150,7 +151,7 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
151 SameSite: http.SameSiteStrictMode,
152 MaxAge: -1,
153 })
153 - utils.WriteAPIEmpty(w, http.StatusOK)
154 + utils.WriteAPIData(w, http.StatusOK, map[string]any{})
155 return
156 case types.PathAdminAuthStatus:
157 if !utils.RequireMethod(w, r, http.MethodGet) {
@@ -170,7 +171,7 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
171
172 runtime := f.server.PolicyRuntime()
173 methodNotAllowed := utils.MethodNotAllowedError()
173 - invalidRequestBody := utils.InvalidRequestMessage("invalid request body")
174 + invalidRequestBody := utils.InvalidRequestError(errors.New("invalid request body"))
175
176 switch path {
177 case types.PathAdminSnapshot:
@@ -204,41 +205,19 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
205 Enabled: f.isLandingPageEnabled(),
206 })
207 case types.PathAdminUDP:
207 - if !utils.RequireMethod(w, r, http.MethodPost) {
208 - return
209 - }
210 - req, ok := utils.DecodeJSONRequestAs[types.AdminUDPSettingsRequest](w, r, 1<<16, invalidRequestBody)
211 - if !ok {
212 - return
213 - }
214 - if req.MaxLeases < 0 {
215 - utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, "max_leases must be non-negative")
216 - return
217 - }
218 - runtime.SetUDPPolicy(req.Enabled, req.MaxLeases)
219 - saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
220 - utils.WriteAPIData(w, http.StatusOK, types.AdminUDPSettingsResponse{
221 - Enabled: runtime.IsUDPEnabled(),
222 - MaxLeases: runtime.UDPMaxLeases(),
223 - })
208 + f.handlePortSettings(w, r, invalidRequestBody, runtime,
209 + runtime.SetUDPPolicy,
210 + func() any {
211 + return types.AdminUDPSettingsResponse{Enabled: runtime.IsUDPEnabled(), MaxLeases: runtime.UDPMaxLeases()}
212 + },
213 + )
214 case types.PathAdminTCPPort:
225 - if !utils.RequireMethod(w, r, http.MethodPost) {
226 - return
227 - }
228 - req, ok := utils.DecodeJSONRequestAs[types.AdminTCPPortSettingsRequest](w, r, 1<<16, invalidRequestBody)
229 - if !ok {
230 - return
231 - }
232 - if req.MaxLeases < 0 {
233 - utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, "max_leases must be non-negative")
234 - return
235 - }
236 - runtime.SetTCPPortPolicy(req.Enabled, req.MaxLeases)
237 - saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
238 - utils.WriteAPIData(w, http.StatusOK, types.AdminTCPPortSettingsResponse{
239 - Enabled: runtime.IsTCPPortEnabled(),
240 - MaxLeases: runtime.TCPPortMaxLeases(),
241 - })
215 + f.handlePortSettings(w, r, invalidRequestBody, runtime,
216 + runtime.SetTCPPortPolicy,
217 + func() any {
218 + return types.AdminTCPPortSettingsResponse{Enabled: runtime.IsTCPPortEnabled(), MaxLeases: runtime.TCPPortMaxLeases()}
219 + },
220 + )
221 case types.PathAdminApproval:
222 if !utils.RequireMethod(w, r, http.MethodPost) {
223 return
@@ -297,7 +276,7 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
276 return
277 }
278 saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
300 - utils.WriteAPIEmpty(w, http.StatusOK)
279 + utils.WriteAPIData(w, http.StatusOK, map[string]any{})
280 case "bps":
281 switch r.Method {
282 case http.MethodPost:
@@ -317,7 +296,7 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
296 return
297 }
298 saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
320 - utils.WriteAPIEmpty(w, http.StatusOK)
299 + utils.WriteAPIData(w, http.StatusOK, map[string]any{})
300 case "approve":
301 approver := runtime.Approver()
302 switch r.Method {
@@ -331,7 +310,7 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
310 return
311 }
312 saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
334 - utils.WriteAPIEmpty(w, http.StatusOK)
313 + utils.WriteAPIData(w, http.StatusOK, map[string]any{})
314 case "deny":
315 approver := runtime.Approver()
316 switch r.Method {
@@ -344,7 +323,7 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
323 return
324 }
325 saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
347 - utils.WriteAPIEmpty(w, http.StatusOK)
326 + utils.WriteAPIData(w, http.StatusOK, map[string]any{})
327 default:
328 http.NotFound(w, r)
329 }
@@ -372,13 +351,42 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
351 return
352 }
353 saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
375 - utils.WriteAPIEmpty(w, http.StatusOK)
354 + utils.WriteAPIData(w, http.StatusOK, map[string]any{})
355 default:
356 http.NotFound(w, r)
357 }
358 }
359 }
360
361 +type portSettingsRequest struct {
362 + Enabled bool `json:"enabled"`
363 + MaxLeases int `json:"max_leases"`
364 +}
365 +
366 +func (f *Frontend) handlePortSettings(
367 + w http.ResponseWriter,
368 + r *http.Request,
369 + invalidBody utils.APIErrorResponse,
370 + runtime *policy.Runtime,
371 + setPolicy func(bool, int),
372 + buildResponse func() any,
373 +) {
374 + if !utils.RequireMethod(w, r, http.MethodPost) {
375 + return
376 + }
377 + req, ok := utils.DecodeJSONRequestAs[portSettingsRequest](w, r, 1<<16, invalidBody)
378 + if !ok {
379 + return
380 + }
381 + if req.MaxLeases < 0 {
382 + utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, "max_leases must be non-negative")
383 + return
384 + }
385 + setPolicy(req.Enabled, req.MaxLeases)
386 + saveAdminState(f.adminSettingsPath, runtime, f.isLandingPageEnabled())
387 + utils.WriteAPIData(w, http.StatusOK, buildResponse())
388 +}
389 +
390 func (f *Frontend) handleLogin(w http.ResponseWriter, r *http.Request) {
391 if !utils.RequireMethod(w, r, http.MethodPost) {
392 return
@@ -388,7 +396,7 @@ func (f *Frontend) handleLogin(w http.ResponseWriter, r *http.Request) {
396 return
397 }
398
391 - req, ok := utils.DecodeJSONRequestAs[types.AdminLoginRequest](w, r, 1<<16, utils.InvalidRequestMessage("invalid request body"))
399 + req, ok := utils.DecodeJSONRequestAs[types.AdminLoginRequest](w, r, 1<<16, utils.InvalidRequestError(errors.New("invalid request body")))
400 if !ok {
401 return
402 }
cmd/relay-server/frontend.go
+1 -1
@@ -214,7 +214,7 @@ func (f *Frontend) serveTunnelStatus(w http.ResponseWriter, r *http.Request) {
214
215 hostname := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("hostname")))
216 if hostname == "" {
217 - utils.InvalidRequestMessage("hostname is required").Write(w)
217 + utils.InvalidRequestError(errors.New("hostname is required")).Write(w)
218 return
219 }
220
frontend/src/components/CloseIcon.tsx deleted
-17
@@ -1,17 +0,0 @@
1 -import clsx from "clsx";
2 -
3 -export const CloseIcon = ({ className }: { className?: string }) => (
4 - <svg
5 - xmlns="http://www.w3.org/2000/svg"
6 - className={clsx("w-5 h-5", className)}
7 - viewBox="0 0 24 24"
8 - fill="none"
9 - stroke="currentColor"
10 - strokeWidth="2"
11 - strokeLinecap="round"
12 - strokeLinejoin="round"
13 - >
14 - <line x1="18" y1="6" x2="6" y2="18" />
15 - <line x1="6" y1="6" x2="18" y2="18" />
16 - </svg>
17 -);
frontend/src/lib/apiClient.ts
+2 -2
@@ -1,9 +1,9 @@
1 -export type APIErrorPayload = {
1 +type APIErrorPayload = {
2 code?: string;
3 message?: string;
4 };
5
6 -export type APIEnvelope<T> = {
6 +type APIEnvelope<T> = {
7 ok?: boolean;
8 data?: T;
9 error?: APIErrorPayload;
frontend/src/lib/apiPaths.test.ts
-23
@@ -3,22 +3,6 @@ import { describe, expect, it } from "vitest";
3 import { API_PATHS, adminLeasePath, encodePathPart } from "@/lib/apiPaths";
4
5 describe("API_PATHS contract alignment", () => {
6 - it("keeps admin snapshot path aligned", () => {
7 - expect(API_PATHS.admin.snapshot).toBe("/admin/snapshot");
8 - expect(API_PATHS.admin.landingPage).toBe("/admin/settings/landing-page");
9 - });
10 -
11 - it("keeps sdk endpoint paths aligned", () => {
12 - expect(API_PATHS.sdk).toEqual({
13 - prefix: "/sdk",
14 - register: "/sdk/register",
15 - unregister: "/sdk/unregister",
16 - renew: "/sdk/renew",
17 - domain: "/sdk/domain",
18 - connect: "/sdk/connect",
19 - });
20 - });
21 -
6 it("encodes lease identities as base64url path segments", () => {
7 const name = "relay-1";
8 const address = "0x00000000000000000000000000000000000000A1";
@@ -42,11 +26,4 @@ describe("API_PATHS contract alignment", () => {
26 `${API_PATHS.admin.leases}/${encodeURIComponent(encodedName)}/${encodeURIComponent(encodedAddress)}/approve`
27 );
28 });
45 -
46 - it("keeps install script endpoints aligned", () => {
47 - expect(API_PATHS.install).toEqual({
48 - shell: "/install.sh",
49 - powershell: "/install.ps1",
50 - });
51 - });
29 });
frontend/src/lib/apiPaths.ts
+1 -1
@@ -6,7 +6,7 @@ export const API_PATHS = {
6 logout: "/admin/logout",
7 authStatus: "/admin/auth/status",
8 leases: "/admin/leases",
9 - stats: "/admin/stats",
9 +
10 approvalMode: "/admin/settings/approval-mode",
11 landingPage: "/admin/settings/landing-page",
12 udpSettings: "/admin/settings/udp",
frontend/src/lib/exposeName.ts
+1 -1
@@ -76,7 +76,7 @@ export function normalizeExposeName(value: string): string {
76 return ascii;
77 }
78
79 -export function normalizeExposeTarget(raw: string): string {
79 +function normalizeExposeTarget(raw: string): string {
80 const trimmed = raw.trim();
81 const candidate = trimmed === "" ? DEFAULT_TARGET_PORT : trimmed;
82
portal/acme/acme_test.go
-21
@@ -111,27 +111,6 @@ func TestNewManagerRejectsENSGaslessWithoutDNSProvider(t *testing.T) {
111 }
112 }
113
114 -func TestNewManagerAllowsENSGaslessWithACMEDNSProvider(t *testing.T) {
115 - t.Parallel()
116 -
117 - manager, err := NewManager(Config{
118 - BaseDomain: "portal.example.com",
119 - KeyDir: t.TempDir(),
120 - DNSProvider: TypeRoute53,
121 - ENSGaslessEnabled: true,
122 - ENSGaslessAddress: "0x1234567890123456789012345678901234567890",
123 - })
124 - if err != nil {
125 - t.Fatalf("NewManager() error = %v", err)
126 - }
127 - if manager == nil {
128 - t.Fatal("NewManager() = nil, want manager")
129 - }
130 - if manager.dns == nil {
131 - t.Fatal("manager.dns = nil, want dns provider")
132 - }
133 -}
134 -
114 func TestEnsureTLSMaterialUsesManualCertificateWithDNSProvider(t *testing.T) {
115 t.Parallel()
116
portal/api_server.go
+37 -68
@@ -25,21 +25,38 @@ import (
25 "github.com/gosuda/portal/v2/utils"
26 )
27
28 +type apiError struct {
29 + code string
30 + msg string
31 + status int
32 +}
33 +
34 +func (e *apiError) Error() string { return e.msg }
35 +
36 var (
29 - errFeatureUnavailable = errors.New(types.APIErrorCodeFeatureUnavailable)
30 - errHostnameConflict = errors.New(types.APIErrorCodeHostnameConflict)
31 - errIPBanned = errors.New(types.APIErrorCodeIPBanned)
32 - errLeaseNotFound = errors.New(types.APIErrorCodeLeaseNotFound)
33 - errLeaseRejected = errors.New(types.APIErrorCodeLeaseRejected)
34 - errTransportMismatch = errors.New(types.APIErrorCodeTransportMismatch)
35 - errUnauthorized = errors.New(types.APIErrorCodeUnauthorized)
36 - errUDPDisabled = errors.New(types.APIErrorCodeUDPDisabled)
37 - errUDPCapacityExceeded = errors.New(types.APIErrorCodeUDPCapacityExceeded)
38 - errTCPPortDisabled = errors.New(types.APIErrorCodeTCPPortDisabled)
39 - errTCPPortCapacityExceeded = errors.New(types.APIErrorCodeTCPPortCapacityExceeded)
40 - errTCPPortExhausted = errors.New("no tcp ports available")
37 + errFeatureUnavailable = &apiError{types.APIErrorCodeFeatureUnavailable, "feature unavailable", http.StatusServiceUnavailable}
38 + errHostnameConflict = &apiError{types.APIErrorCodeHostnameConflict, "hostname conflict", http.StatusConflict}
39 + errIPBanned = &apiError{types.APIErrorCodeIPBanned, "request denied because source IP is banned", http.StatusForbidden}
40 + errLeaseNotFound = &apiError{types.APIErrorCodeLeaseNotFound, "lease not found", http.StatusNotFound}
41 + errLeaseRejected = &apiError{types.APIErrorCodeLeaseRejected, "lease is not approved for routing", http.StatusForbidden}
42 + errTransportMismatch = &apiError{types.APIErrorCodeTransportMismatch, "transport mismatch", http.StatusConflict}
43 + errUnauthorized = &apiError{types.APIErrorCodeUnauthorized, "unauthorized", http.StatusForbidden}
44 + errUDPDisabled = &apiError{types.APIErrorCodeUDPDisabled, "udp disabled", http.StatusForbidden}
45 + errUDPCapacityExceeded = &apiError{types.APIErrorCodeUDPCapacityExceeded, "udp capacity exceeded", http.StatusServiceUnavailable}
46 + errTCPPortDisabled = &apiError{types.APIErrorCodeTCPPortDisabled, "tcp port disabled", http.StatusForbidden}
47 + errTCPPortCapacityExceeded = &apiError{types.APIErrorCodeTCPPortCapacityExceeded, "tcp port capacity exceeded", http.StatusServiceUnavailable}
48 + errTCPPortExhausted = &apiError{types.APIErrorCodeTCPPortExhausted, "no tcp ports available", http.StatusServiceUnavailable}
49 )
50
51 +func writeAPIErrorResponse(w http.ResponseWriter, err error) {
52 + var ae *apiError
53 + if errors.As(err, &ae) {
54 + utils.WriteAPIError(w, ae.status, ae.code, ae.msg)
55 + return
56 + }
57 + utils.InvalidRequestError(err).Write(w)
58 +}
59 +
60 func (s *Server) newAPIServer(listener net.Listener, apiMux *http.ServeMux, apiTLS keyless.TLSMaterialConfig) (net.Listener, *http.Server, io.Closer, error) {
61 var keylessSignerHandler http.Handler
62 if len(apiTLS.KeyPEM) > 0 {
@@ -124,17 +141,6 @@ func (s *Server) extractAllowedClientIP(w http.ResponseWriter, r *http.Request)
141 return "", false
142 }
143
127 -func leaseLookupError(err error) utils.APIErrorResponse {
128 - if errors.Is(err, errLeaseNotFound) {
129 - return utils.APIErrorResponse{
130 - Status: http.StatusNotFound,
131 - Code: types.APIErrorCodeLeaseNotFound,
132 - Message: err.Error(),
133 - }
134 - }
135 - return utils.InvalidRequestError(err)
136 -}
137 -
144 func (s *Server) handleRelayDiscovery(w http.ResponseWriter, r *http.Request) {
145 if !utils.RequireMethod(w, r, http.MethodGet) {
146 return
@@ -215,27 +221,10 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
221
222 resp, err := s.registerLease(challenge.Request, clientIP, req.ReportedIP)
223 if err != nil {
218 - switch {
219 - case errors.Is(err, errFeatureUnavailable):
220 - utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeFeatureUnavailable, err.Error())
221 - case errors.Is(err, errHostnameConflict):
222 - utils.WriteAPIError(w, http.StatusConflict, types.APIErrorCodeHostnameConflict, err.Error())
223 - case errors.Is(err, errIPBanned):
224 - utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeIPBanned, err.Error())
225 - case errors.Is(err, errTCPPortExhausted):
226 - utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeTCPPortExhausted, err.Error())
227 - case errors.Is(err, transport.ErrPortExhausted):
224 + if errors.Is(err, transport.ErrPortExhausted) {
225 utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeUDPPortExhausted, err.Error())
229 - case errors.Is(err, errUDPDisabled):
230 - utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeUDPDisabled, err.Error())
231 - case errors.Is(err, errUDPCapacityExceeded):
232 - utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeUDPCapacityExceeded, err.Error())
233 - case errors.Is(err, errTCPPortDisabled):
234 - utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeTCPPortDisabled, err.Error())
235 - case errors.Is(err, errTCPPortCapacityExceeded):
236 - utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeTCPPortCapacityExceeded, err.Error())
237 - default:
238 - utils.InvalidRequestError(err).Write(w)
226 + } else {
227 + writeAPIErrorResponse(w, err)
228 }
229 return
230 }
@@ -282,16 +271,7 @@ func (s *Server) handleRegisterChallenge(w http.ResponseWriter, r *http.Request)
271
272 resp, err := s.registry.issueRegisterChallenge(req, domain, registerURI)
273 if err != nil {
285 - switch {
286 - case errors.Is(err, errFeatureUnavailable):
287 - utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeFeatureUnavailable, err.Error())
288 - case errors.Is(err, errUDPDisabled):
289 - utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeUDPDisabled, err.Error())
290 - case errors.Is(err, errUDPCapacityExceeded):
291 - utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeUDPCapacityExceeded, err.Error())
292 - default:
293 - utils.InvalidRequestError(err).Write(w)
294 - }
274 + writeAPIErrorResponse(w, err)
275 return
276 }
277
@@ -325,7 +305,7 @@ func (s *Server) handleRenew(w http.ResponseWriter, r *http.Request) {
305 }
306 record, err := s.registry.Renew(claims.Identity, ttl, clientIP, utils.SanitizeReportedIP(req.ReportedIP))
307 if err != nil {
328 - leaseLookupError(err).Write(w)
308 + writeAPIErrorResponse(w, err)
309 return
310 }
311 nextAccessToken, _, err := auth.IssueLeaseAccessToken(s.identity.PrivateKey, s.identity.Address, s.cfg.PortalURL, record.Copy(), ttl)
@@ -357,7 +337,7 @@ func (s *Server) handleUnregister(w http.ResponseWriter, r *http.Request) {
337
338 record, err := s.registry.Unregister(claims.Identity)
339 if err != nil {
360 - leaseLookupError(err).Write(w)
340 + writeAPIErrorResponse(w, err)
341 return
342 }
343 deleteCtx, cancel := context.WithTimeout(context.Background(), defaultClaimTimeout)
@@ -373,7 +353,7 @@ func (s *Server) handleUnregister(w http.ResponseWriter, r *http.Request) {
353 record.Close()
354 }
355
376 - utils.WriteAPIEmpty(w, http.StatusOK)
356 + utils.WriteAPIData(w, http.StatusOK, map[string]any{})
357 }
358
359 func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) {
@@ -393,18 +373,7 @@ func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) {
373
374 lease, err := s.admitLeaseByToken(token, false)
375 if err != nil {
396 - switch {
397 - case errors.Is(err, errLeaseNotFound):
398 - utils.WriteAPIError(w, http.StatusNotFound, types.APIErrorCodeLeaseNotFound, err.Error())
399 - case errors.Is(err, errLeaseRejected):
400 - utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeLeaseRejected, "lease is not approved for routing")
401 - case errors.Is(err, errUnauthorized):
402 - utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeUnauthorized, err.Error())
403 - case errors.Is(err, errTransportMismatch):
404 - utils.WriteAPIError(w, http.StatusConflict, types.APIErrorCodeTransportMismatch, "lease does not support stream transport")
405 - default:
406 - utils.InvalidRequestError(err).Write(w)
407 - }
376 + writeAPIErrorResponse(w, err)
377 return
378 }
379
portal/discovery/relayset.go
+22 -23
@@ -32,7 +32,7 @@ type RelaySet struct {
32 bootstrapRelayURLs []string
33 relayKeysByURL map[string]string
34 relays map[string]types.RelayDescriptor
35 - localByURL map[string]RelayLocalState
35 + localByURL map[string]*RelayLocalState
36 activeRelayURLs []string
37 activeRelays []types.RelayDescriptor
38 selfRelayKey string
@@ -43,11 +43,20 @@ func NewRelaySet() *RelaySet {
43 return &RelaySet{
44 relayKeysByURL: make(map[string]string),
45 relays: make(map[string]types.RelayDescriptor),
46 - localByURL: make(map[string]RelayLocalState),
46 + localByURL: make(map[string]*RelayLocalState),
47 }
48 }
49
50 -func relayExpiredAt(desc types.RelayDescriptor, state RelayLocalState, now time.Time) bool {
50 +func (s *RelaySet) getOrCreateLocalState(relayURL string) *RelayLocalState {
51 + state := s.localByURL[relayURL]
52 + if state == nil {
53 + state = &RelayLocalState{}
54 + s.localByURL[relayURL] = state
55 + }
56 + return state
57 +}
58 +
59 +func relayExpiredAt(desc types.RelayDescriptor, state *RelayLocalState, now time.Time) bool {
60 if state.Expired {
61 return true
62 }
@@ -160,7 +169,7 @@ func (s *RelaySet) syncActiveLocked(now time.Time) {
169 if s.isSelfRelayURLLocked(relayURL) {
170 continue
171 }
163 - state := s.localByURL[relayURL]
172 + state := s.getOrCreateLocalState(relayURL)
173 if state.Banned {
174 continue
175 }
@@ -185,7 +194,7 @@ func (s *RelaySet) syncActiveLocked(now time.Time) {
194 if _, ok := seen[relayURL]; ok {
195 continue
196 }
188 - state := s.localByURL[relayURL]
197 + state := s.getOrCreateLocalState(relayURL)
198 if state.Banned || !state.Advertised || relayExpiredAt(desc, state, now) {
199 continue
200 }
@@ -231,7 +240,7 @@ func (s *RelaySet) bootstrapDescriptors() []types.RelayDescriptor {
240 if s.isSelfRelayURLLocked(relayURL) {
241 continue
242 }
234 - if s.localByURL[relayURL].Banned {
243 + if s.getOrCreateLocalState(relayURL).Banned {
244 continue
245 }
246 if desc, ok := s.descriptorByURLLocked(relayURL); ok && desc.APIHTTPSAddr != "" {
@@ -288,7 +297,7 @@ func (s *RelaySet) confirmableDescriptors() []types.RelayDescriptor {
297 if _, ok := bootstrapRelayURLs[relayURL]; ok {
298 continue
299 }
291 - state := s.localByURL[relayURL]
300 + state := s.getOrCreateLocalState(relayURL)
301 if state.Banned || relayExpiredAt(desc, state, now) {
302 continue
303 }
@@ -314,12 +323,11 @@ func (s *RelaySet) BanRelayURL(relayURL string) {
323 if relayURL == "" {
324 return
325 }
317 - state := s.localByURL[relayURL]
326 + state := s.getOrCreateLocalState(relayURL)
327 if state.Banned {
328 return
329 }
330 state.Banned = true
322 - s.localByURL[relayURL] = state
331 s.syncActiveLocked(time.Now().UTC())
332 }
333
@@ -345,9 +353,7 @@ func (s *RelaySet) SetBootstrapRelayURLs(relayURLs []string) {
353 }
354 seen[relayURL] = struct{}{}
355 filtered = append(filtered, relayURL)
348 - if _, ok := s.localByURL[relayURL]; !ok {
349 - s.localByURL[relayURL] = RelayLocalState{}
350 - }
356 + s.getOrCreateLocalState(relayURL)
357 }
358
359 for _, relayURL := range s.bootstrapRelayURLs {
@@ -396,9 +402,7 @@ func (s *RelaySet) registerDescriptorLocked(desc types.RelayDescriptor) error {
402
403 s.relays[relayKey] = normalized
404 s.relayKeysByURL[normalized.APIHTTPSAddr] = relayKey
399 - if _, ok := s.localByURL[normalized.APIHTTPSAddr]; !ok {
400 - s.localByURL[normalized.APIHTTPSAddr] = RelayLocalState{}
401 - }
405 + s.getOrCreateLocalState(normalized.APIHTTPSAddr)
406 return nil
407 }
408
@@ -424,11 +428,10 @@ func (s *RelaySet) ApplyRelayDiscoveryResponse(targetIdentity types.Identity, ta
428 if err := s.registerDescriptorLocked(selfDescriptor); err != nil {
429 return err
430 }
427 - selfState := s.localByURL[selfDescriptor.APIHTTPSAddr]
431 + selfState := s.getOrCreateLocalState(selfDescriptor.APIHTTPSAddr)
432 selfState.Advertised = true
433 selfState.Expired = false
434 selfState.ConsecutiveFailures = 0
431 - s.localByURL[selfDescriptor.APIHTTPSAddr] = selfState
435
436 for _, relayDescriptor := range relayDescriptors {
437 if s.isSelfRelayDescriptorLocked(relayDescriptor) {
@@ -441,7 +444,7 @@ func (s *RelaySet) ApplyRelayDiscoveryResponse(targetIdentity types.Identity, ta
444 Msg("skipping conflicting discovery relay hint")
445 continue
446 }
444 - state := s.localByURL[relayDescriptor.APIHTTPSAddr]
447 + state := s.getOrCreateLocalState(relayDescriptor.APIHTTPSAddr)
448 switch {
449 case state.Expired:
450 // Fresh hint re-enables direct confirmation but must not restore
@@ -453,7 +456,6 @@ func (s *RelaySet) ApplyRelayDiscoveryResponse(targetIdentity types.Identity, ta
456 state.Expired = false
457 state.ConsecutiveFailures = 0
458 }
456 - s.localByURL[relayDescriptor.APIHTTPSAddr] = state
459 }
460 s.syncActiveLocked(now)
461 return nil
@@ -483,13 +485,11 @@ func (s *RelaySet) RecordDiscoveryFailure(identity types.Identity, relayURL stri
485 return false, "", 0
486 }
487
486 - state := s.localByURL[relayURL]
488 + state := s.getOrCreateLocalState(relayURL)
489 state.ConsecutiveFailures++
488 - s.localByURL[relayURL] = state
490 if !state.Expired && state.ConsecutiveFailures >= discoveryRecoveryFailures {
491 state.Expired = true
492 state.Advertised = false
492 - s.localByURL[relayURL] = state
493 s.syncActiveLocked(time.Now().UTC())
494 return true, "recovery", state.ConsecutiveFailures
495 }
@@ -501,7 +501,6 @@ func (s *RelaySet) RecordDiscoveryFailure(identity types.Identity, relayURL stri
501 apiErr.StatusCode == http.StatusGone) {
502 state.Expired = true
503 state.Advertised = false
504 - s.localByURL[relayURL] = state
504 s.syncActiveLocked(time.Now().UTC())
505 return true, "status", state.ConsecutiveFailures
506 }
portal/lease.go
+12 -44
@@ -18,7 +18,7 @@ import (
18 const defaultRegisterChallengeTTL = 2 * time.Minute
19
20 type leaseRegistry struct {
21 - routes *routeTable
21 + routes map[string]string
22 leasesByKey map[string]*leaseRecord
23 registerChallenges map[string]*auth.RegisterChallenge
24 policy *policy.Runtime
@@ -31,7 +31,7 @@ func newLeaseRegistry(runtime *policy.Runtime) *leaseRegistry {
31 }
32
33 return &leaseRegistry{
34 - routes: newRouteTable(),
34 + routes: make(map[string]string),
35 leasesByKey: make(map[string]*leaseRecord),
36 registerChallenges: make(map[string]*auth.RegisterChallenge),
37 policy: runtime,
@@ -47,7 +47,7 @@ func (r *leaseRegistry) CloseAll() []*leaseRecord {
47 out = append(out, record)
48 r.policy.ForgetIdentity(record.Key())
49 }
50 - r.routes = newRouteTable()
50 + r.routes = make(map[string]string)
51 r.leasesByKey = make(map[string]*leaseRecord)
52 r.registerChallenges = make(map[string]*auth.RegisterChallenge)
53 return out
@@ -62,7 +62,7 @@ func (r *leaseRegistry) Lookup(host string) (*leaseRecord, bool) {
62 r.mu.RLock()
63 defer r.mu.RUnlock()
64
65 - key, ok := r.routes.Lookup(host)
65 + key, ok := routeLookup(r.routes, host)
66 if !ok {
67 return nil, false
68 }
@@ -86,7 +86,7 @@ func (r *leaseRegistry) Register(record *leaseRecord) error {
86
87 r.mu.Lock()
88
89 - if existingKey, ok := r.routes.LookupExact(hostname); ok && existingKey != key {
89 + if existingKey, ok := r.routes[hostname]; ok && existingKey != key {
90 r.mu.Unlock()
91 return errHostnameConflict
92 }
@@ -94,12 +94,12 @@ func (r *leaseRegistry) Register(record *leaseRecord) error {
94 var replaced *leaseRecord
95 if existing, ok := r.leasesByKey[key]; ok && existing != nil {
96 replaced = existing
97 - r.routes.Delete(existing.Hostname)
97 + delete(r.routes, utils.NormalizeHostname(existing.Hostname))
98 r.policy.ForgetIdentity(existing.Key())
99 }
100 record.Hostname = hostname
101 r.leasesByKey[key] = record
102 - r.routes.Set(hostname, key)
102 + r.routes[hostname] = key
103 if strings.TrimSpace(record.ClientIP) != "" {
104 r.policy.IPFilter().RegisterIdentityIP(key, record.ClientIP)
105 }
@@ -144,7 +144,7 @@ func (r *leaseRegistry) Unregister(identity types.Identity) (*leaseRecord, error
144 }
145
146 delete(r.leasesByKey, key)
147 - r.routes.Delete(record.Hostname)
147 + delete(r.routes, utils.NormalizeHostname(record.Hostname))
148 r.policy.ForgetIdentity(key)
149 return record, nil
150 }
@@ -246,7 +246,7 @@ func (r *leaseRegistry) cleanupExpired(now time.Time) []*leaseRecord {
246 if now.After(record.ExpiresAt) {
247 expired = append(expired, record)
248 delete(r.leasesByKey, key)
249 - r.routes.Delete(record.Hostname)
249 + delete(r.routes, utils.NormalizeHostname(record.Hostname))
250 r.policy.ForgetIdentity(key)
251 }
252 }
@@ -379,50 +379,18 @@ func (r *leaseRecord) Close() {
379 }
380 }
381
382 -type routeTable struct {
383 - exact map[string]string
384 -}
385 -
386 -func newRouteTable() *routeTable {
387 - return &routeTable{exact: make(map[string]string)}
388 -}
389 -
390 -func (t *routeTable) Set(host, identityKey string) {
391 - host = utils.NormalizeHostname(host)
392 - if host == "" {
393 - return
394 - }
395 - t.exact[host] = identityKey
396 -}
397 -
398 -func (t *routeTable) Delete(host string) {
399 - delete(t.exact, utils.NormalizeHostname(host))
400 -}
401 -
402 -func (t *routeTable) LookupExact(host string) (string, bool) {
403 - host = utils.NormalizeHostname(host)
382 +func routeLookup(routes map[string]string, host string) (string, bool) {
383 if host == "" {
384 return "", false
385 }
407 - identityKey, ok := t.exact[host]
408 - return identityKey, ok
409 -}
410 -
411 -func (t *routeTable) Lookup(host string) (string, bool) {
412 - host = utils.NormalizeHostname(host)
413 - if host == "" {
414 - return "", false
415 - }
416 -
417 - if identityKey, ok := t.exact[host]; ok {
386 + if identityKey, ok := routes[host]; ok {
387 return identityKey, true
388 }
420 -
389 parts := strings.Split(host, ".")
390 if len(parts) < 3 {
391 return "", false
392 }
393 wildcard := "*." + strings.Join(parts[1:], ".")
426 - identityKey, ok := t.exact[wildcard]
394 + identityKey, ok := routes[wildcard]
395 return identityKey, ok
396 }
portal/policy/runtime.go
+21 -12
@@ -4,15 +4,26 @@ import (
4 "sync"
5 )
6
7 +type PortPolicy struct {
8 + enabled bool
9 + maxLeases int
10 +}
11 +
12 +func (p PortPolicy) IsEnabled() bool { return p.enabled }
13 +func (p PortPolicy) MaxLeases() int { return p.maxLeases }
14 +
15 +func (p *PortPolicy) Set(enabled bool, maxLeases int) {
16 + p.enabled = enabled
17 + p.maxLeases = maxLeases
18 +}
19 +
20 type Runtime struct {
21 approver *Approver
22 bpsManager *BPSManager
23 ipFilter *IPFilter
24 bannedIdentityKeys map[string]struct{}
12 - udpEnabled bool
13 - udpMaxLeases int
14 - tcpPortEnabled bool
15 - tcpPortMaxLeases int
25 + udp PortPolicy
26 + tcpPort PortPolicy
27 mu sync.RWMutex
28 }
29
@@ -115,40 +126,38 @@ func (r *Runtime) IsIdentityRoutable(key string) bool {
126
127 func (r *Runtime) SetUDPPolicy(enabled bool, maxLeases int) {
128 r.mu.Lock()
118 - r.udpEnabled = enabled
119 - r.udpMaxLeases = maxLeases
129 + r.udp.Set(enabled, maxLeases)
130 r.mu.Unlock()
131 }
132
133 func (r *Runtime) IsUDPEnabled() bool {
134 r.mu.RLock()
135 defer r.mu.RUnlock()
126 - return r.udpEnabled
136 + return r.udp.IsEnabled()
137 }
138
139 func (r *Runtime) UDPMaxLeases() int {
140 r.mu.RLock()
141 defer r.mu.RUnlock()
132 - return r.udpMaxLeases
142 + return r.udp.MaxLeases()
143 }
144
145 func (r *Runtime) SetTCPPortPolicy(enabled bool, maxLeases int) {
146 r.mu.Lock()
137 - r.tcpPortEnabled = enabled
138 - r.tcpPortMaxLeases = maxLeases
147 + r.tcpPort.Set(enabled, maxLeases)
148 r.mu.Unlock()
149 }
150
151 func (r *Runtime) IsTCPPortEnabled() bool {
152 r.mu.RLock()
153 defer r.mu.RUnlock()
145 - return r.tcpPortEnabled
154 + return r.tcpPort.IsEnabled()
155 }
156
157 func (r *Runtime) TCPPortMaxLeases() int {
158 r.mu.RLock()
159 defer r.mu.RUnlock()
151 - return r.tcpPortMaxLeases
160 + return r.tcpPort.MaxLeases()
161 }
162
163 func (r *Runtime) ForgetIdentity(key string) {
portal/server.go
+15 -9
@@ -171,30 +171,35 @@ func (s *Server) Start(ctx context.Context, apiMux *http.ServeMux) error {
171 return err
172 }
173
174 + var cleanups []func()
175 + defer func() {
176 + for i := len(cleanups) - 1; i >= 0; i-- {
177 + cleanups[i]()
178 + }
179 + }()
180 +
181 + cleanups = append(cleanups, acmeManager.Stop)
182 +
183 serverCtx, cancel := context.WithCancel(ctx)
184 + cleanups = append(cleanups, cancel)
185 +
186 var listenConfig net.ListenConfig
187
188 apiListener, err := listenConfig.Listen(serverCtx, "tcp", s.cfg.APIListenAddr)
189 if err != nil {
179 - acmeManager.Stop()
180 - cancel()
190 return fmt.Errorf("listen api: %w", err)
191 }
192 + cleanups = append(cleanups, func() { _ = apiListener.Close() })
193 +
194 sniListener, err := listenConfig.Listen(serverCtx, "tcp", s.cfg.SNIListenAddr)
195 if err != nil {
185 - acmeManager.Stop()
186 - _ = apiListener.Close()
187 - cancel()
196 return fmt.Errorf("listen sni: %w", err)
197 }
198 + cleanups = append(cleanups, func() { _ = sniListener.Close() })
199
200 group, groupCtx := errgroup.WithContext(serverCtx)
201 wrappedAPIListener, apiServer, apiCloser, err := s.newAPIServer(apiListener, apiMux, apiTLS)
202 if err != nil {
194 - acmeManager.Stop()
195 - _ = apiListener.Close()
196 - _ = sniListener.Close()
197 - cancel()
203 return err
204 }
205
@@ -205,6 +210,7 @@ func (s *Server) Start(ctx context.Context, apiMux *http.ServeMux) error {
210 s.acmeManager = acmeManager
211 s.cancel = cancel
212 s.group = group
213 + cleanups = nil
214
215 group.Go(s.runAPIServer)
216 group.Go(func() error { return s.runSNIListener(groupCtx) })
types/transport.go
+3 -3
@@ -5,9 +5,9 @@ import (
5 "errors"
6 )
7
8 -// ErrDatagramTooSmall is returned when a datagram payload is too short to
8 +// errDatagramTooSmall is returned when a datagram payload is too short to
9 // contain a valid flow ID varint.
10 -var ErrDatagramTooSmall = errors.New("datagram too small to decode")
10 +var errDatagramTooSmall = errors.New("datagram too small to decode")
11
12 // DatagramFrame carries one relayed datagram.
13 // Wire encoding uses only FlowID and Payload with layout:
@@ -34,7 +34,7 @@ func EncodeDatagram(flowID uint32, payload []byte) []byte {
34 func DecodeDatagram(data []byte) (DatagramFrame, error) {
35 flowID, n := binary.Uvarint(data)
36 if n <= 0 {
37 - return DatagramFrame{}, ErrDatagramTooSmall
37 + return DatagramFrame{}, errDatagramTooSmall
38 }
39 return DatagramFrame{
40 FlowID: uint32(flowID),
utils/api.go
-12
@@ -29,10 +29,6 @@ func WriteAPIData(w http.ResponseWriter, status int, data any) {
29 _ = json.NewEncoder(w).Encode(types.APIEnvelope[any]{OK: true, Data: data})
30 }
31
32 -func WriteAPIEmpty(w http.ResponseWriter, status int) {
33 - WriteAPIData(w, status, map[string]any{})
34 -}
35 -
32 func WriteAPIError(w http.ResponseWriter, status int, code, message string) {
33 w.Header().Set("Content-Type", "application/json")
34 w.WriteHeader(status)
@@ -58,14 +54,6 @@ func InvalidRequestError(err error) APIErrorResponse {
54 }
55 }
56
61 -func InvalidRequestMessage(message string) APIErrorResponse {
62 - return APIErrorResponse{
63 - Status: http.StatusBadRequest,
64 - Code: types.APIErrorCodeInvalidRequest,
65 - Message: message,
66 - }
67 -}
68 -
57 func RequireMethod(w http.ResponseWriter, r *http.Request, method string) bool {
58 if r.Method == method {
59 return true
utils/cmd.go
+10 -26
@@ -19,14 +19,14 @@ type CommandFunc func([]string) error
19 type IntEnvParser func(string, int) int
20 type boolFlagValue interface{ IsBoolFlag() bool }
21
22 -func TrimmedEnv(name string) string {
22 +func trimmedEnv(name string) string {
23 return strings.TrimSpace(os.Getenv(name))
24 }
25
26 -func ResolveStringEnv(fallback string, envNames ...string) string {
26 +func resolveStringEnv(fallback string, envNames ...string) string {
27 value := fallback
28 for _, envName := range envNames {
29 - if envValue := TrimmedEnv(envName); envValue != "" {
29 + if envValue := trimmedEnv(envName); envValue != "" {
30 value = envValue
31 break
32 }
@@ -34,9 +34,9 @@ func ResolveStringEnv(fallback string, envNames ...string) string {
34 return value
35 }
36
37 -func ResolveBoolEnv(fallback bool, envNames ...string) bool {
37 +func resolveBoolEnv(fallback bool, envNames ...string) bool {
38 for _, envName := range envNames {
39 - raw := TrimmedEnv(envName)
39 + raw := trimmedEnv(envName)
40 if raw == "" {
41 continue
42 }
@@ -49,7 +49,7 @@ func ResolveBoolEnv(fallback bool, envNames ...string) bool {
49 return fallback
50 }
51
52 -func ResolveIntEnv(fallback int, parse IntEnvParser, envNames ...string) int {
52 +func resolveIntEnv(fallback int, parse IntEnvParser, envNames ...string) int {
53 if parse == nil {
54 parse = func(raw string, fallback int) int {
55 v, err := strconv.Atoi(strings.TrimSpace(raw))
@@ -60,7 +60,7 @@ func ResolveIntEnv(fallback int, parse IntEnvParser, envNames ...string) int {
60 }
61 }
62 for _, envName := range envNames {
63 - raw := TrimmedEnv(envName)
63 + raw := trimmedEnv(envName)
64 if raw == "" {
65 continue
66 }
@@ -92,18 +92,6 @@ func ParseOptionalPortNumber(raw string, fallback int) int {
92 return ParsePortNumber(raw, fallback)
93 }
94
95 -func ParseNonNegativeInt(raw string, fallback int) int {
96 - raw = strings.TrimSpace(raw)
97 - if raw == "" {
98 - return fallback
99 - }
100 - v, err := strconv.Atoi(raw)
101 - if err != nil || v < 0 {
102 - return fallback
103 - }
104 - return v
105 -}
106 -
95 func DurationOrDefault(v, fallback time.Duration) time.Duration {
96 if v > 0 {
97 return v
@@ -130,7 +118,7 @@ func StringFlag(fs *flag.FlagSet, target *string, name, fallback, usage string)
118 }
119
120 func StringFlagEnv(fs *flag.FlagSet, target *string, name, fallback, usage string, envNames ...string) {
133 - ensureFlagSet(fs).StringVar(target, name, ResolveStringEnv(fallback, envNames...), flagUsage(usage, envNames...))
121 + ensureFlagSet(fs).StringVar(target, name, resolveStringEnv(fallback, envNames...), flagUsage(usage, envNames...))
122 }
123
124 func BoolFlag(fs *flag.FlagSet, target *bool, name string, fallback bool, usage string) {
@@ -138,15 +126,11 @@ func BoolFlag(fs *flag.FlagSet, target *bool, name string, fallback bool, usage
126 }
127
128 func BoolFlagEnv(fs *flag.FlagSet, target *bool, name string, fallback bool, usage string, envNames ...string) {
141 - ensureFlagSet(fs).BoolVar(target, name, ResolveBoolEnv(fallback, envNames...), flagUsage(usage, envNames...))
142 -}
143 -
144 -func IntFlag(fs *flag.FlagSet, target *int, name string, fallback int, usage string) {
145 - ensureFlagSet(fs).IntVar(target, name, fallback, usage)
129 + ensureFlagSet(fs).BoolVar(target, name, resolveBoolEnv(fallback, envNames...), flagUsage(usage, envNames...))
130 }
131
132 func IntFlagEnv(fs *flag.FlagSet, target *int, name string, fallback int, parse IntEnvParser, usage string, envNames ...string) {
149 - ensureFlagSet(fs).IntVar(target, name, ResolveIntEnv(fallback, parse, envNames...), flagUsage(usage, envNames...))
133 + ensureFlagSet(fs).IntVar(target, name, resolveIntEnv(fallback, parse, envNames...), flagUsage(usage, envNames...))
134 }
135
136 func RepeatedStringFlag(fs *flag.FlagSet, target *[]string, name, usage string) {
utils/identity.go
+4 -13
@@ -42,14 +42,8 @@ func NormalizeStoredIdentity(identity types.Identity) (types.Identity, error) {
42 if normalized.PublicKey != "" && !strings.EqualFold(TrimHexPrefix(normalized.PublicKey), resolved.PublicKey) {
43 return types.Identity{}, errors.New("identity public key does not match private key")
44 }
45 - if normalized.Address != "" {
46 - address, err := NormalizeEVMAddress(normalized.Address)
47 - if err != nil {
48 - return types.Identity{}, err
49 - }
50 - if address != resolved.Address {
51 - return types.Identity{}, errors.New("identity address does not match private key")
52 - }
45 + if normalized.Address != "" && !strings.EqualFold(normalized.Address, resolved.Address) {
46 + return types.Identity{}, errors.New("identity address does not match private key")
47 }
48 normalized.Address = resolved.Address
49 normalized.PublicKey = resolved.PublicKey
@@ -67,13 +61,10 @@ func NormalizeStoredIdentity(identity types.Identity) (types.Identity, error) {
61 normalized.Address = address
62 return normalized, nil
63 }
70 - normalized.Address, err = NormalizeEVMAddress(normalized.Address)
71 - if err != nil {
72 - return types.Identity{}, err
73 - }
74 - if normalized.Address != address {
64 + if !strings.EqualFold(normalized.Address, address) {
65 return types.Identity{}, errors.New("identity address does not match public key")
66 }
67 + normalized.Address = address
68 return normalized, nil
69 }
70
utils/utils.go
-50
@@ -320,20 +320,6 @@ func RemoveRelayURL(inputs []string, target string) []string {
320 return filtered
321 }
322
323 -func AppendUniqueRelayURL(inputs []string, target string) []string {
324 - target = strings.TrimSpace(target)
325 - if target == "" {
326 - return append([]string(nil), inputs...)
327 - }
328 -
329 - for _, input := range inputs {
330 - if strings.TrimSpace(input) == target {
331 - return append([]string(nil), inputs...)
332 - }
333 - }
334 - return append(append([]string(nil), inputs...), target)
335 -}
336 -
323 func MergeRelayURLs(current, excluded, inputs []string) ([]string, error) {
324 merged, err := NormalizeRelayURLs(append(append([]string(nil), current...), inputs...)...)
325 if err != nil {
@@ -389,42 +375,6 @@ func LeaseHostname(name, rootHost string) (string, error) {
375 return label + "." + rootHost, nil
376 }
377
392 -func FormatDuration(d time.Duration) string {
393 - if d <= 0 {
394 - return ""
395 - }
396 - if d > time.Hour {
397 - return fmt.Sprintf("%.0fh", d.Hours())
398 - }
399 - if d > time.Minute {
400 - return fmt.Sprintf("%.0fm", d.Minutes())
401 - }
402 - return fmt.Sprintf("%.0fs", d.Seconds())
403 -}
404 -
405 -func FormatLastSeen(d time.Duration) string {
406 - if d <= 0 {
407 - return ""
408 - }
409 - if d >= time.Hour {
410 - hours := int(d / time.Hour)
411 - minutes := int((d % time.Hour) / time.Minute)
412 - if minutes > 0 {
413 - return fmt.Sprintf("%dh %dm", hours, minutes)
414 - }
415 - return fmt.Sprintf("%dh", hours)
416 - }
417 - if d >= time.Minute {
418 - minutes := int(d / time.Minute)
419 - seconds := int((d % time.Minute) / time.Second)
420 - if seconds > 0 {
421 - return fmt.Sprintf("%dm %ds", minutes, seconds)
422 - }
423 - return fmt.Sprintf("%dm", minutes)
424 - }
425 - return fmt.Sprintf("%ds", int(d/time.Second))
426 -}
427 -
378 func DecodeBase64URLString(encoded string) (string, error) {
379 decoded, err := base64.URLEncoding.DecodeString(encoded)
380 if err == nil {