chore: move ip utils

Kim committed Mar 26, 2026 at 10:00 UTC ed9f8a47e9c682f37d941258e99191932538305c
6 files changed +138 -126
portal/api_server.go
+2 -13
@@ -432,17 +432,6 @@ func (s *Server) handleQUICTunnelConn(conn *quic.Conn) {
432 Msg("quic tunnel connected")
433 }
434
435 -func sanitizeReportedIP(raw string) string {
436 - candidate := strings.TrimSpace(raw)
437 - if candidate == "" {
438 - return ""
439 - }
440 - if net.ParseIP(candidate) == nil {
441 - return ""
442 - }
443 - return candidate
444 -}
445 -
435 func (s *Server) registerLease(req types.RegisterRequest, clientIP string) (types.RegisterResponse, error) {
436 name, err := utils.NormalizeDNSLabel(req.Name)
437 if err != nil {
@@ -503,7 +492,7 @@ func (s *Server) registerLease(req types.RegisterRequest, clientIP string) (type
492 FirstSeenAt: now,
493 LastSeenAt: now,
494 ClientIP: clientIP,
506 - ReportedIP: sanitizeReportedIP(req.ReportedIP),
495 + ReportedIP: utils.SanitizeReportedIP(req.ReportedIP),
496 UDPEnabled: req.UDPEnabled,
497 },
498 ReverseToken: req.ReverseToken,
@@ -580,7 +569,7 @@ func (s *Server) renewLease(req types.RenewRequest, clientIP string) (types.Rene
569 if req.TTL > 0 {
570 ttl = time.Duration(req.TTL) * time.Second
571 }
583 - record, err := s.registry.Renew(req.LeaseID, req.ReverseToken, ttl, clientIP, sanitizeReportedIP(req.ReportedIP))
572 + record, err := s.registry.Renew(req.LeaseID, req.ReverseToken, ttl, clientIP, utils.SanitizeReportedIP(req.ReportedIP))
573 if err != nil {
574 return types.RenewResponse{}, err
575 }
sdk/api_client.go
+1 -1
@@ -147,7 +147,7 @@ func (a *apiClient) ensureReady(ctx context.Context) error {
147 a.rawTLSConfig = rawTLSConfig
148
149 if a.resolvedPublicIP == "" {
150 - a.resolvedPublicIP = resolvePublicIP(ctx)
150 + a.resolvedPublicIP = utils.ResolvePublicIP(ctx)
151 }
152
153 return nil
sdk/public_ip.go deleted
-60
@@ -1,60 +0,0 @@
1 -package sdk
2 -
3 -import (
4 - "context"
5 - "io"
6 - "net"
7 - "net/http"
8 - "strings"
9 - "time"
10 -)
11 -
12 -// resolvePublicIP attempts to determine the caller's public IP address
13 -// using well-known external services. Returns empty string on failure.
14 -// Best-effort with a short timeout to avoid blocking registration.
15 -func resolvePublicIP(ctx context.Context) string {
16 - ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
17 - defer cancel()
18 -
19 - endpoints := []string{
20 - "https://api.ipify.org",
21 - "https://ifconfig.me/ip",
22 - }
23 -
24 - for _, endpoint := range endpoints {
25 - if ip := queryIPEndpoint(ctx, endpoint); ip != "" {
26 - return ip
27 - }
28 - }
29 - return ""
30 -}
31 -
32 -func queryIPEndpoint(ctx context.Context, url string) string {
33 - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
34 - if err != nil {
35 - return ""
36 - }
37 - req.Header.Set("User-Agent", "portal-tunnel")
38 -
39 - client := &http.Client{Timeout: 3 * time.Second}
40 - resp, err := client.Do(req)
41 - if err != nil {
42 - return ""
43 - }
44 - defer resp.Body.Close()
45 -
46 - if resp.StatusCode != http.StatusOK {
47 - return ""
48 - }
49 -
50 - body, err := io.ReadAll(io.LimitReader(resp.Body, 256))
51 - if err != nil {
52 - return ""
53 - }
54 -
55 - candidate := strings.TrimSpace(string(body))
56 - if net.ParseIP(candidate) == nil {
57 - return ""
58 - }
59 - return candidate
60 -}
utils/network.go new
+105
@@ -0,0 +1,105 @@
1 +package utils
2 +
3 +import (
4 + "context"
5 + "encoding/json"
6 + "io"
7 + "net"
8 + "net/http"
9 + "strings"
10 + "time"
11 +
12 + "github.com/gosuda/portal/v2/types"
13 +)
14 +
15 +// ResolvePublicIP attempts to determine the caller's public IP address
16 +// using well-known external services. Returns empty string on failure.
17 +// Best-effort with a short timeout to avoid blocking registration.
18 +func ResolvePublicIP(ctx context.Context) string {
19 + ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
20 + defer cancel()
21 +
22 + endpoints := []string{
23 + "https://api.ipify.org",
24 + "https://ifconfig.me/ip",
25 + }
26 + client := &http.Client{Timeout: 3 * time.Second}
27 +
28 + for _, endpoint := range endpoints {
29 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
30 + if err != nil {
31 + continue
32 + }
33 + req.Header.Set("User-Agent", "portal-tunnel")
34 +
35 + resp, err := client.Do(req)
36 + if err != nil {
37 + continue
38 + }
39 +
40 + body, readErr := io.ReadAll(io.LimitReader(resp.Body, 256))
41 + _ = resp.Body.Close()
42 + if resp.StatusCode != http.StatusOK || readErr != nil {
43 + continue
44 + }
45 +
46 + if candidate := SanitizeReportedIP(string(body)); candidate != "" {
47 + return candidate
48 + }
49 + }
50 +
51 + return ""
52 +}
53 +
54 +func SanitizeReportedIP(raw string) string {
55 + candidate := strings.TrimSpace(raw)
56 + if candidate == "" {
57 + return ""
58 + }
59 + if net.ParseIP(candidate) == nil {
60 + return ""
61 + }
62 + return candidate
63 +}
64 +
65 +func ResolvePortalRelayURLs(ctx context.Context, explicit []string, includeDefaults bool) ([]string, error) {
66 + explicit, err := NormalizeRelayURLs(explicit...)
67 + if err != nil {
68 + return nil, err
69 + }
70 + if !includeDefaults {
71 + return explicit, nil
72 + }
73 +
74 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, types.PortalRelayRegistryURL, nil)
75 + if err != nil {
76 + return explicit, nil
77 + }
78 +
79 + client := &http.Client{Timeout: 5 * time.Second}
80 + resp, err := client.Do(req)
81 + if err != nil {
82 + return explicit, nil
83 + }
84 + defer resp.Body.Close()
85 +
86 + if resp.StatusCode != http.StatusOK {
87 + return explicit, nil
88 + }
89 +
90 + var registry struct {
91 + Relays []string `json:"relays"`
92 + }
93 + if err := json.NewDecoder(resp.Body).Decode(&registry); err != nil {
94 + return explicit, nil
95 + }
96 +
97 + defaults, err := NormalizeRelayURLs(registry.Relays...)
98 + if err != nil {
99 + return explicit, nil
100 + }
101 + if len(defaults) == 0 {
102 + return explicit, nil
103 + }
104 + return MergeRelayURLs(defaults, nil, explicit)
105 +}
utils/network_test.go new
+30
@@ -0,0 +1,30 @@
1 +package utils
2 +
3 +import "testing"
4 +
5 +func TestSanitizeReportedIP(t *testing.T) {
6 + t.Parallel()
7 +
8 + tests := []struct {
9 + name string
10 + raw string
11 + want string
12 + }{
13 + {name: "empty", raw: "", want: ""},
14 + {name: "whitespace", raw: " ", want: ""},
15 + {name: "ipv4", raw: " 203.0.113.10 ", want: "203.0.113.10"},
16 + {name: "ipv6", raw: " 2001:db8::1 ", want: "2001:db8::1"},
17 + {name: "invalid", raw: "not-an-ip", want: ""},
18 + {name: "host port", raw: "203.0.113.10:443", want: ""},
19 + }
20 +
21 + for _, tc := range tests {
22 + t.Run(tc.name, func(t *testing.T) {
23 + t.Parallel()
24 +
25 + if got := SanitizeReportedIP(tc.raw); got != tc.want {
26 + t.Fatalf("SanitizeReportedIP(%q) = %q, want %q", tc.raw, got, tc.want)
27 + }
28 + })
29 + }
30 +}
utils/registry.go deleted
-52
@@ -1,52 +0,0 @@
1 -package utils
2 -
3 -import (
4 - "context"
5 - "encoding/json"
6 - "net/http"
7 - "time"
8 -
9 - "github.com/gosuda/portal/v2/types"
10 -)
11 -
12 -func ResolvePortalRelayURLs(ctx context.Context, explicit []string, includeDefaults bool) ([]string, error) {
13 - explicit, err := NormalizeRelayURLs(explicit...)
14 - if err != nil {
15 - return nil, err
16 - }
17 - if !includeDefaults {
18 - return explicit, nil
19 - }
20 -
21 - req, err := http.NewRequestWithContext(ctx, http.MethodGet, types.PortalRelayRegistryURL, nil)
22 - if err != nil {
23 - return explicit, nil
24 - }
25 -
26 - client := &http.Client{Timeout: 5 * time.Second}
27 - resp, err := client.Do(req)
28 - if err != nil {
29 - return explicit, nil
30 - }
31 - defer resp.Body.Close()
32 -
33 - if resp.StatusCode != http.StatusOK {
34 - return explicit, nil
35 - }
36 -
37 - var registry struct {
38 - Relays []string `json:"relays"`
39 - }
40 - if err := json.NewDecoder(resp.Body).Decode(&registry); err != nil {
41 - return explicit, nil
42 - }
43 -
44 - defaults, err := NormalizeRelayURLs(registry.Relays...)
45 - if err != nil {
46 - return explicit, nil
47 - }
48 - if len(defaults) == 0 {
49 - return explicit, nil
50 - }
51 - return MergeRelayURLs(defaults, nil, explicit)
52 -}