refact: remove and consolidate utils

Kim committed Apr 9, 2026 at 15:37 UTC 4e9f7853f43c0086abae409af13e0628ceb582a6
7 files changed +179 -252
cmd/relay-server/main.go
-7
@@ -42,7 +42,6 @@ type relayServerConfig struct {
42 LandingPageEnabled bool
43 Bootstraps string
44 DiscoveryEnabled bool
45 - MaxRouting int
45 WireGuardPrivateKey string
46 WireGuardEndpoint string
47 OverlayIPv4 string
@@ -82,7 +81,6 @@ func runServeCommand(args []string) error {
81 utils.BoolFlagEnv(fs, &cfg.LandingPageEnabled, "landing-page-enabled", false, "enable landing page by default when no admin setting has been saved yet", "LANDING_PAGE_ENABLED")
82 utils.StringFlagEnv(fs, &cfg.Bootstraps, "bootstraps", "", "additional bootstrap relay API URLs used for discovery expansion", "BOOTSTRAPS")
83 utils.BoolFlagEnv(fs, &cfg.DiscoveryEnabled, "discovery", false, "serve relay discovery endpoints and poll discovery peers", "DISCOVERY")
85 - utils.IntFlagEnv(fs, &cfg.MaxRouting, "max-routing", 1, nil, "maximum number of discovery routing attempts per refresh", "MAX_ROUTING")
84 utils.StringFlagEnv(fs, &cfg.WireGuardPrivateKey, "wireguard-private-key", "", "wireguard private key for relay overlay", "WIREGUARD_PRIVATE_KEY")
85 utils.StringFlagEnv(fs, &cfg.WireGuardEndpoint, "wireguard-endpoint", "", "wireguard endpoint (host:port) for relay overlay", "WIREGUARD_ENDPOINT")
86 utils.StringFlagEnv(fs, &cfg.OverlayIPv4, "overlay-ipv4", "", "explicit overlay IPv4 override (auto-derived from public key when unset)", "OVERLAY_IPV4")
@@ -118,9 +116,6 @@ func runServeCommand(args []string) error {
116 printRootUsage(os.Stderr)
117 return err
118 }
121 - if err := utils.ValidateMaxRouting(cfg.MaxRouting); err != nil {
122 - return err
123 - }
119
120 log.Info().
121 Str("release_version", types.ReleaseVersion).
@@ -132,7 +127,6 @@ func runServeCommand(args []string) error {
127 Int("max_port", cfg.MaxPort).
128 Bool("landing_page_enabled", cfg.LandingPageEnabled).
129 Bool("discovery_enabled", cfg.DiscoveryEnabled).
135 - Int("max_routing", cfg.MaxRouting).
130 Str("acme_dns_provider", cfg.ACMEDNSProvider).
131 Bool("ens_gasless_enabled", cfg.ENSGaslessEnabled).
132 Bool("udp_enabled", cfg.UDPEnabled).
@@ -180,7 +174,6 @@ func runServer(ctx context.Context, cfg relayServerConfig) error {
174 TrustedProxyCIDRs: cfg.TrustedProxyCIDRs,
175 TrustProxyHeaders: cfg.TrustProxyHeaders,
176 DiscoveryEnabled: cfg.DiscoveryEnabled,
183 - MaxRouting: cfg.MaxRouting,
177 MinPort: cfg.MinPort,
178 MaxPort: cfg.MaxPort,
179 UDPEnabled: cfg.UDPEnabled,
portal/server.go
-1
@@ -55,7 +55,6 @@ type ServerConfig struct {
55 TrustedProxyCIDRs string
56 TrustProxyHeaders bool
57 DiscoveryEnabled bool
58 - MaxRouting int
58 MinPort int
59 MaxPort int
60 UDPEnabled bool
types/types.go
-3
@@ -5,9 +5,6 @@ const (
5 ProtocolVersion = "5"
6 PortalRelayRegistryURL = "https://raw.githubusercontent.com/gosuda/portal-tunnel/main/registry.json"
7
8 - MinDiscoveryRoutingAttempts = 1
9 - MaxDiscoveryRoutingAttempts = 32
10 -
8 HeaderAccessToken = "X-Portal-Access-Token"
9 MarkerKeepalive = byte(0x00)
10 MarkerRawStart = byte(0x01)
utils/crypto.go
+179
@@ -1,14 +1,21 @@
1 package utils
2
3 import (
4 + "crypto/rand"
5 "crypto/sha256"
6 + "encoding/base64"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 + "net"
11 + "net/netip"
12 + "sort"
13 + "strconv"
14 "strings"
15
16 "github.com/decred/dcrd/dcrec/secp256k1/v4"
17 "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
18 + "golang.org/x/crypto/curve25519"
19 "golang.org/x/crypto/sha3"
20
21 "github.com/gosuda/portal-tunnel/v2/types"
@@ -173,6 +180,178 @@ func VerifySHA256Secp256k1DER(payload []byte, publicKeyHex, signatureHex string)
180 return nil
181 }
182
183 +func NormalizeWireGuardPrivateKey(raw string) (string, error) {
184 + key, err := decodeWireGuardKey(raw)
185 + if err != nil {
186 + return "", err
187 + }
188 + clampWireGuardPrivateKey(&key)
189 + return base64.StdEncoding.EncodeToString(key[:]), nil
190 +}
191 +
192 +func GenerateWireGuardPrivateKey() (string, error) {
193 + var key [32]byte
194 + if _, err := rand.Read(key[:]); err != nil {
195 + return "", err
196 + }
197 + clampWireGuardPrivateKey(&key)
198 + return base64.StdEncoding.EncodeToString(key[:]), nil
199 +}
200 +
201 +func WireGuardPublicKeyFromPrivate(raw string) (string, error) {
202 + privateKey, err := decodeWireGuardKey(raw)
203 + if err != nil {
204 + return "", err
205 + }
206 + clampWireGuardPrivateKey(&privateKey)
207 + var publicKey [32]byte
208 + curve25519.ScalarBaseMult(&publicKey, &privateKey)
209 + return base64.StdEncoding.EncodeToString(publicKey[:]), nil
210 +}
211 +
212 +func WireGuardListenPort(rawEndpoint string) (int, error) {
213 + endpoint := strings.TrimSpace(rawEndpoint)
214 + if endpoint == "" {
215 + return 0, errors.New("wireguard endpoint is required")
216 + }
217 + _, portText, err := net.SplitHostPort(endpoint)
218 + if err != nil {
219 + return 0, errors.New("wireguard endpoint must be host:port")
220 + }
221 + port, err := strconv.Atoi(portText)
222 + if err != nil || port <= 0 || port > 65535 {
223 + return 0, errors.New("wireguard endpoint port is invalid")
224 + }
225 + return port, nil
226 +}
227 +
228 +func DeriveWireGuardOverlayIPv4(publicKey string) (string, error) {
229 + decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(publicKey))
230 + if err != nil {
231 + return "", errors.New("wireguard public key must be base64 encoded")
232 + }
233 + if len(decoded) != 32 {
234 + return "", errors.New("wireguard public key must be 32 bytes")
235 + }
236 +
237 + sum := sha256.Sum256(decoded)
238 + return netip.AddrFrom4([4]byte{
239 + 100,
240 + 64 + (sum[0] & 0x3f),
241 + sum[1],
242 + 1 + (sum[2] % 254),
243 + }).String(), nil
244 +}
245 +
246 +func WireGuardKeyHex(raw string) (string, error) {
247 + key, err := decodeWireGuardKey(raw)
248 + if err != nil {
249 + return "", err
250 + }
251 + return hex.EncodeToString(key[:]), nil
252 +}
253 +
254 +func decodeWireGuardKey(raw string) ([32]byte, error) {
255 + var key [32]byte
256 + value := strings.TrimSpace(raw)
257 + if value == "" {
258 + return key, errors.New("wireguard key is required")
259 + }
260 +
261 + var decoded []byte
262 + var err error
263 + if len(value) == 64 && !strings.Contains(value, "=") {
264 + decoded, err = hex.DecodeString(value)
265 + } else {
266 + decoded, err = base64.StdEncoding.DecodeString(value)
267 + }
268 + if err != nil {
269 + return key, errors.New("wireguard key must be base64 or hex encoded")
270 + }
271 + if len(decoded) != len(key) {
272 + return key, errors.New("wireguard key must be 32 bytes")
273 + }
274 + copy(key[:], decoded)
275 + return key, nil
276 +}
277 +
278 +func clampWireGuardPrivateKey(key *[32]byte) {
279 + key[0] &= 248
280 + key[31] = (key[31] & 127) | 64
281 +}
282 +
283 +func ValidateWireGuardPublicKey(raw string) error {
284 + key := strings.TrimSpace(raw)
285 + if key == "" {
286 + return errors.New("wireguard_public_key is required")
287 + }
288 + decoded, err := base64.StdEncoding.DecodeString(key)
289 + if err != nil {
290 + return errors.New("wireguard_public_key must be base64 encoded")
291 + }
292 + if len(decoded) != 32 {
293 + return errors.New("wireguard_public_key must be 32 bytes")
294 + }
295 + return nil
296 +}
297 +
298 +func ValidateWireGuardEndpoint(raw string) error {
299 + endpoint := strings.TrimSpace(raw)
300 + if endpoint == "" {
301 + return errors.New("wireguard_endpoint is required")
302 + }
303 + host, port, err := net.SplitHostPort(endpoint)
304 + if err != nil {
305 + return errors.New("wireguard_endpoint must be host:port")
306 + }
307 + if strings.TrimSpace(host) == "" {
308 + return errors.New("wireguard_endpoint host is required")
309 + }
310 + portNum, err := strconv.Atoi(port)
311 + if err != nil || portNum <= 0 || portNum > 65535 {
312 + return errors.New("wireguard_endpoint port is invalid")
313 + }
314 + return nil
315 +}
316 +
317 +func ValidateOverlayIPv4(raw string) error {
318 + ipText := strings.TrimSpace(raw)
319 + if ipText == "" {
320 + return errors.New("overlay_ipv4 is required")
321 + }
322 + ip := net.ParseIP(ipText)
323 + if ip == nil || ip.To4() == nil {
324 + return errors.New("overlay_ipv4 must be a valid IPv4 address")
325 + }
326 + return nil
327 +}
328 +
329 +func NormalizeOverlayCIDRs(inputs []string) ([]string, error) {
330 + if len(inputs) == 0 {
331 + return nil, nil
332 + }
333 + seen := make(map[string]struct{}, len(inputs))
334 + out := make([]string, 0, len(inputs))
335 + for _, input := range inputs {
336 + input = strings.TrimSpace(input)
337 + if input == "" {
338 + continue
339 + }
340 + _, network, err := net.ParseCIDR(input)
341 + if err != nil {
342 + return nil, fmt.Errorf("invalid overlay cidr %q", input)
343 + }
344 + normalized := network.String()
345 + if _, ok := seen[normalized]; ok {
346 + continue
347 + }
348 + seen[normalized] = struct{}{}
349 + out = append(out, normalized)
350 + }
351 + sort.Strings(out)
352 + return out, nil
353 +}
354 +
355 func ParseSecp256k1PublicKeyHex(raw string) (*secp256k1.PublicKey, error) {
356 publicKeyHex := strings.TrimSpace(raw)
357 if publicKeyHex == "" {
utils/discovery.go deleted
-16
@@ -1,16 +0,0 @@
1 -package utils
2 -
3 -import (
4 - "fmt"
5 -
6 - "github.com/gosuda/portal-tunnel/v2/types"
7 -)
8 -
9 -// ValidateMaxRouting ensures the configured MaxRouting is within the supported range.
10 -func ValidateMaxRouting(attempts int) error {
11 - if attempts < types.MinDiscoveryRoutingAttempts || attempts > types.MaxDiscoveryRoutingAttempts {
12 - return fmt.Errorf("max routing attempts must be between %d and %d (got %d)",
13 - types.MinDiscoveryRoutingAttempts, types.MaxDiscoveryRoutingAttempts, attempts)
14 - }
15 - return nil
16 -}
utils/identity_test.go deleted
-36
@@ -1,36 +0,0 @@
1 -package utils
2 -
3 -import "testing"
4 -
5 -// Cross-language parity vectors -- keep in sync with frontend/src/lib/exposeName.test.ts
6 -var exposeNameVectors = []struct {
7 - target, seed, expectedNormalized, expectedName string
8 -}{
9 - {"3000", "test_seed", "127.0.0.1:3000", "bubble-cricket-beacon"},
10 - {"", "portal", "127.0.0.1:3000", "zesty-beacon-sketch"},
11 - {"http://localhost:8080", "cli_abc", "localhost:8080", "sprightly-rocket-zap"},
12 - {"192.168.1.1:8080", "web_xyz", "192.168.1.1:8080", "velvet-yeti-march"},
13 - {"localhost", "cli_", "localhost:80", "misty-rocket-ripple"},
14 -}
15 -
16 -func TestNormalizeExposeTarget(t *testing.T) {
17 - for _, v := range exposeNameVectors {
18 - got := normalizeExposeTarget(v.target)
19 - if got != v.expectedNormalized {
20 - t.Errorf("normalizeExposeTarget(%q) = %q, want %q", v.target, got, v.expectedNormalized)
21 - }
22 - }
23 -}
24 -
25 -func TestDefaultExposeName(t *testing.T) {
26 - for _, v := range exposeNameVectors {
27 - got, err := DefaultExposeName(v.target, v.seed)
28 - if err != nil {
29 - t.Errorf("DefaultExposeName(%q, %q) error: %v", v.target, v.seed, err)
30 - continue
31 - }
32 - if got != v.expectedName {
33 - t.Errorf("DefaultExposeName(%q, %q) = %q, want %q", v.target, v.seed, got, v.expectedName)
34 - }
35 - }
36 -}
utils/wireguard.go deleted
-189
@@ -1,189 +0,0 @@
1 -package utils
2 -
3 -import (
4 - "crypto/rand"
5 - "crypto/sha256"
6 - "encoding/base64"
7 - "encoding/hex"
8 - "errors"
9 - "fmt"
10 - "net"
11 - "net/netip"
12 - "sort"
13 - "strconv"
14 - "strings"
15 -
16 - "golang.org/x/crypto/curve25519"
17 -)
18 -
19 -func NormalizeWireGuardPrivateKey(raw string) (string, error) {
20 - key, err := decodeWireGuardKey(raw)
21 - if err != nil {
22 - return "", err
23 - }
24 - clampWireGuardPrivateKey(&key)
25 - return base64.StdEncoding.EncodeToString(key[:]), nil
26 -}
27 -
28 -func GenerateWireGuardPrivateKey() (string, error) {
29 - var key [32]byte
30 - if _, err := rand.Read(key[:]); err != nil {
31 - return "", err
32 - }
33 - clampWireGuardPrivateKey(&key)
34 - return base64.StdEncoding.EncodeToString(key[:]), nil
35 -}
36 -
37 -func WireGuardPublicKeyFromPrivate(raw string) (string, error) {
38 - privateKey, err := decodeWireGuardKey(raw)
39 - if err != nil {
40 - return "", err
41 - }
42 - clampWireGuardPrivateKey(&privateKey)
43 - var publicKey [32]byte
44 - curve25519.ScalarBaseMult(&publicKey, &privateKey)
45 - return base64.StdEncoding.EncodeToString(publicKey[:]), nil
46 -}
47 -
48 -func WireGuardListenPort(rawEndpoint string) (int, error) {
49 - endpoint := strings.TrimSpace(rawEndpoint)
50 - if endpoint == "" {
51 - return 0, errors.New("wireguard endpoint is required")
52 - }
53 - _, portText, err := net.SplitHostPort(endpoint)
54 - if err != nil {
55 - return 0, errors.New("wireguard endpoint must be host:port")
56 - }
57 - port, err := strconv.Atoi(portText)
58 - if err != nil || port <= 0 || port > 65535 {
59 - return 0, errors.New("wireguard endpoint port is invalid")
60 - }
61 - return port, nil
62 -}
63 -
64 -func DeriveWireGuardOverlayIPv4(publicKey string) (string, error) {
65 - decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(publicKey))
66 - if err != nil {
67 - return "", errors.New("wireguard public key must be base64 encoded")
68 - }
69 - if len(decoded) != 32 {
70 - return "", errors.New("wireguard public key must be 32 bytes")
71 - }
72 -
73 - sum := sha256.Sum256(decoded)
74 - return netip.AddrFrom4([4]byte{
75 - 100,
76 - 64 + (sum[0] & 0x3f),
77 - sum[1],
78 - 1 + (sum[2] % 254),
79 - }).String(), nil
80 -}
81 -
82 -func WireGuardKeyHex(raw string) (string, error) {
83 - key, err := decodeWireGuardKey(raw)
84 - if err != nil {
85 - return "", err
86 - }
87 - return hex.EncodeToString(key[:]), nil
88 -}
89 -
90 -func decodeWireGuardKey(raw string) ([32]byte, error) {
91 - var key [32]byte
92 - value := strings.TrimSpace(raw)
93 - if value == "" {
94 - return key, errors.New("wireguard key is required")
95 - }
96 -
97 - var decoded []byte
98 - var err error
99 - if len(value) == 64 && !strings.Contains(value, "=") {
100 - decoded, err = hex.DecodeString(value)
101 - } else {
102 - decoded, err = base64.StdEncoding.DecodeString(value)
103 - }
104 - if err != nil {
105 - return key, errors.New("wireguard key must be base64 or hex encoded")
106 - }
107 - if len(decoded) != len(key) {
108 - return key, errors.New("wireguard key must be 32 bytes")
109 - }
110 - copy(key[:], decoded)
111 - return key, nil
112 -}
113 -
114 -func clampWireGuardPrivateKey(key *[32]byte) {
115 - key[0] &= 248
116 - key[31] = (key[31] & 127) | 64
117 -}
118 -
119 -func ValidateWireGuardPublicKey(raw string) error {
120 - key := strings.TrimSpace(raw)
121 - if key == "" {
122 - return errors.New("wireguard_public_key is required")
123 - }
124 - decoded, err := base64.StdEncoding.DecodeString(key)
125 - if err != nil {
126 - return errors.New("wireguard_public_key must be base64 encoded")
127 - }
128 - if len(decoded) != 32 {
129 - return errors.New("wireguard_public_key must be 32 bytes")
130 - }
131 - return nil
132 -}
133 -
134 -func ValidateWireGuardEndpoint(raw string) error {
135 - endpoint := strings.TrimSpace(raw)
136 - if endpoint == "" {
137 - return errors.New("wireguard_endpoint is required")
138 - }
139 - host, port, err := net.SplitHostPort(endpoint)
140 - if err != nil {
141 - return errors.New("wireguard_endpoint must be host:port")
142 - }
143 - if strings.TrimSpace(host) == "" {
144 - return errors.New("wireguard_endpoint host is required")
145 - }
146 - portNum, err := strconv.Atoi(port)
147 - if err != nil || portNum <= 0 || portNum > 65535 {
148 - return errors.New("wireguard_endpoint port is invalid")
149 - }
150 - return nil
151 -}
152 -
153 -func ValidateOverlayIPv4(raw string) error {
154 - ipText := strings.TrimSpace(raw)
155 - if ipText == "" {
156 - return errors.New("overlay_ipv4 is required")
157 - }
158 - ip := net.ParseIP(ipText)
159 - if ip == nil || ip.To4() == nil {
160 - return errors.New("overlay_ipv4 must be a valid IPv4 address")
161 - }
162 - return nil
163 -}
164 -
165 -func NormalizeOverlayCIDRs(inputs []string) ([]string, error) {
166 - if len(inputs) == 0 {
167 - return nil, nil
168 - }
169 - seen := make(map[string]struct{}, len(inputs))
170 - out := make([]string, 0, len(inputs))
171 - for _, input := range inputs {
172 - input = strings.TrimSpace(input)
173 - if input == "" {
174 - continue
175 - }
176 - _, network, err := net.ParseCIDR(input)
177 - if err != nil {
178 - return nil, fmt.Errorf("invalid overlay cidr %q", input)
179 - }
180 - normalized := network.String()
181 - if _, ok := seen[normalized]; ok {
182 - continue
183 - }
184 - seen[normalized] = struct{}{}
185 - out = append(out, normalized)
186 - }
187 - sort.Strings(out)
188 - return out, nil
189 -}