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