Skip to content

Commit 8de49f7

Browse files
committed
feat(socket): implement formatSocketAddress function for IPv6 handling
1 parent 33adfc7 commit 8de49f7

File tree

5 files changed

+167
-15
lines changed

5 files changed

+167
-15
lines changed

internal/upstream/dynamic_resolver.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func TestDynamicTargets(dynamicTargets []ProxyTarget) map[string]*Status {
242242
dynamicTargetsByResolver[target.Resolver] = append(dynamicTargetsByResolver[target.Resolver], target)
243243
} else {
244244
// No resolver specified, mark as offline
245-
key := target.Host + ":" + target.Port
245+
key := formatSocketAddress(target.Host, target.Port)
246246
result[key] = &Status{
247247
Online: false,
248248
Latency: 0,
@@ -255,7 +255,7 @@ func TestDynamicTargets(dynamicTargets []ProxyTarget) map[string]*Status {
255255
dynamicResolver := NewDynamicResolver(resolver)
256256

257257
for _, target := range targets {
258-
key := target.Host + ":" + target.Port
258+
key := formatSocketAddress(target.Host, target.Port)
259259

260260
// Try to resolve the service
261261
addresses, err := dynamicResolver.ResolveService(target.ServiceURL)
@@ -305,8 +305,8 @@ func EnhancedAvailabilityTest(targets []ProxyTarget) map[string]*Status {
305305
if target.IsConsul && target.Resolver != "" {
306306
dynamicTargets = append(dynamicTargets, target)
307307
} else {
308-
// Regular target - use existing format for traditional AvailabilityTest
309-
key := target.Host + ":" + target.Port
308+
// Regular target - use properly formatted socket address for traditional AvailabilityTest
309+
key := formatSocketAddress(target.Host, target.Port)
310310
regularTargets = append(regularTargets, key)
311311
}
312312
}

internal/upstream/ipv6_socket_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package upstream
2+
3+
import (
4+
"sync"
5+
"testing"
6+
)
7+
8+
func TestFormatSocketAddress_IPv6(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
host string
12+
port string
13+
expected string
14+
}{
15+
{
16+
name: "IPv6 all addresses",
17+
host: "::",
18+
port: "9001",
19+
expected: "[::]:9001",
20+
},
21+
{
22+
name: "IPv6 localhost",
23+
host: "::1",
24+
port: "8080",
25+
expected: "[::1]:8080",
26+
},
27+
{
28+
name: "IPv6 full address",
29+
host: "2001:db8::1",
30+
port: "9000",
31+
expected: "[2001:db8::1]:9000",
32+
},
33+
{
34+
name: "IPv6 with brackets already",
35+
host: "[::1]",
36+
port: "8080",
37+
expected: "[::1]:8080",
38+
},
39+
{
40+
name: "IPv4 address",
41+
host: "127.0.0.1",
42+
port: "9001",
43+
expected: "127.0.0.1:9001",
44+
},
45+
{
46+
name: "hostname",
47+
host: "example.com",
48+
port: "80",
49+
expected: "example.com:80",
50+
},
51+
}
52+
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
result := formatSocketAddress(tt.host, tt.port)
56+
if result != tt.expected {
57+
t.Errorf("formatSocketAddress(%q, %q) = %q, want %q", tt.host, tt.port, result, tt.expected)
58+
}
59+
})
60+
}
61+
}
62+
63+
func TestAvailabilityTest_IPv6Socket(t *testing.T) {
64+
// Test that IPv6 socket addresses are properly formatted
65+
// This test verifies that the socket string passed to net.DialTimeout is correct
66+
67+
// Test with properly formatted IPv6 addresses
68+
sockets := []string{
69+
"[::1]:8080", // IPv6 localhost with port
70+
"127.0.0.1:8080", // IPv4 for comparison
71+
}
72+
73+
// This should not panic or cause parsing errors
74+
results := AvailabilityTest(sockets)
75+
76+
// Verify we get results for both sockets (even if they're offline)
77+
if len(results) != 2 {
78+
t.Errorf("Expected 2 results, got %d", len(results))
79+
}
80+
81+
// Check that the keys are preserved correctly
82+
for _, socket := range sockets {
83+
if _, exists := results[socket]; !exists {
84+
t.Errorf("Expected result for socket %q", socket)
85+
}
86+
}
87+
}
88+
89+
func TestTCPLatency_IPv6Support(t *testing.T) {
90+
// Test that testTCPLatency can handle IPv6 addresses correctly
91+
// Note: This test verifies the function doesn't panic with IPv6 addresses
92+
// The actual connection will likely fail since we're testing non-existent services
93+
94+
tests := []struct {
95+
name string
96+
socket string
97+
}{
98+
{
99+
name: "IPv6 localhost",
100+
socket: "[::1]:8080",
101+
},
102+
{
103+
name: "IPv6 all addresses",
104+
socket: "[::]:9001",
105+
},
106+
{
107+
name: "IPv4 for comparison",
108+
socket: "127.0.0.1:8080",
109+
},
110+
}
111+
112+
for _, tt := range tests {
113+
t.Run(tt.name, func(t *testing.T) {
114+
var wg sync.WaitGroup
115+
status := &Status{}
116+
117+
wg.Add(1)
118+
119+
// This should not panic even if the connection fails
120+
defer func() {
121+
if r := recover(); r != nil {
122+
t.Errorf("testTCPLatency panicked with socket %q: %v", tt.socket, r)
123+
}
124+
}()
125+
126+
testTCPLatency(&wg, tt.socket, status)
127+
wg.Wait()
128+
129+
// We don't check if it's online since the service likely doesn't exist
130+
// We just verify the function completed without panicking
131+
})
132+
}
133+
}

internal/upstream/service.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package upstream
33
import (
44
"maps"
55
"slices"
6+
"strings"
67
"sync"
78
"time"
89

@@ -44,6 +45,22 @@ var (
4445
serviceOnce sync.Once
4546
)
4647

48+
// formatSocketAddress formats a host:port combination into a proper socket address
49+
// For IPv6 addresses, it adds brackets around the host if they're not already present
50+
func formatSocketAddress(host, port string) string {
51+
// Check if this is an IPv6 address by looking for colons
52+
if strings.Contains(host, ":") {
53+
// IPv6 address - check if it already has brackets
54+
if !strings.HasPrefix(host, "[") {
55+
return "[" + host + "]:" + port
56+
}
57+
// Already has brackets, just append port
58+
return host + ":" + port
59+
}
60+
// IPv4 address or hostname
61+
return host + ":" + port
62+
}
63+
4764
// GetUpstreamService returns the singleton upstream service instance
4865
func GetUpstreamService() *UpstreamService {
4966
serviceOnce.Do(func() {
@@ -117,7 +134,7 @@ func (s *UpstreamService) updateTargetsFromConfig(configPath string, targets []P
117134
// Add/update new targets
118135
newTargetKeys := make([]string, 0, len(targets))
119136
for _, target := range targets {
120-
key := target.Host + ":" + target.Port
137+
key := formatSocketAddress(target.Host, target.Port)
121138
newTargetKeys = append(newTargetKeys, key)
122139

123140
if existingTarget, exists := s.targets[key]; exists {
@@ -228,8 +245,8 @@ func (s *UpstreamService) PerformAvailabilityTest() {
228245
if targetInfo.ProxyTarget.IsConsul {
229246
consulTargets = append(consulTargets, targetInfo.ProxyTarget)
230247
} else {
231-
// Traditional target - use host:port key format
232-
key := targetInfo.ProxyTarget.Host + ":" + targetInfo.ProxyTarget.Port
248+
// Traditional target - use properly formatted socket address
249+
key := formatSocketAddress(targetInfo.ProxyTarget.Host, targetInfo.ProxyTarget.Port)
233250
regularTargetKeys = append(regularTargetKeys, key)
234251
}
235252
}

internal/upstream/upstream_parser.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,9 @@ func deduplicateTargets(targets []ProxyTarget) []ProxyTarget {
319319

320320
for _, target := range targets {
321321
// Create a unique key that includes resolver and consul information
322-
key := target.Host + ":" + target.Port + ":" + target.Type + ":" + target.Resolver
322+
// Use formatSocketAddress for proper IPv6 handling in the key
323+
socketAddr := formatSocketAddress(target.Host, target.Port)
324+
key := socketAddr + ":" + target.Type + ":" + target.Resolver
323325
if target.IsConsul {
324326
key += ":consul:" + target.ServiceURL
325327
}

internal/upstream/upstream_parser_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,12 @@ server {
201201
// Create a map for easier comparison
202202
targetMap := make(map[string]ProxyTarget)
203203
for _, target := range targets {
204-
key := target.Host + ":" + target.Port + ":" + target.Type
204+
key := formatSocketAddress(target.Host, target.Port) + ":" + target.Type
205205
targetMap[key] = target
206206
}
207207

208208
for _, expected := range expectedTargets {
209-
key := expected.Host + ":" + expected.Port + ":" + expected.Type
209+
key := formatSocketAddress(expected.Host, expected.Port) + ":" + expected.Type
210210
if _, found := targetMap[key]; !found {
211211
t.Errorf("Expected target not found: %+v", expected)
212212
}
@@ -258,12 +258,12 @@ server {
258258
// Create a map for easier comparison
259259
targetMap := make(map[string]ProxyTarget)
260260
for _, target := range targets {
261-
key := target.Host + ":" + target.Port + ":" + target.Type
261+
key := formatSocketAddress(target.Host, target.Port) + ":" + target.Type
262262
targetMap[key] = target
263263
}
264264

265265
for _, expected := range expectedTargets {
266-
key := expected.Host + ":" + expected.Port + ":" + expected.Type
266+
key := formatSocketAddress(expected.Host, expected.Port) + ":" + expected.Type
267267
if _, found := targetMap[key]; !found {
268268
t.Errorf("Expected target not found: %+v", expected)
269269
}
@@ -332,12 +332,12 @@ server {
332332
// Create a map for easier comparison
333333
targetMap := make(map[string]ProxyTarget)
334334
for _, target := range targets {
335-
key := target.Host + ":" + target.Port + ":" + target.Type
335+
key := formatSocketAddress(target.Host, target.Port) + ":" + target.Type
336336
targetMap[key] = target
337337
}
338338

339339
for _, expected := range expectedTargets {
340-
key := expected.Host + ":" + expected.Port + ":" + expected.Type
340+
key := formatSocketAddress(expected.Host, expected.Port) + ":" + expected.Type
341341
if _, found := targetMap[key]; !found {
342342
t.Errorf("Expected target not found: %+v", expected)
343343
}
@@ -680,7 +680,7 @@ server {
680680
// Verify specific targets exist
681681
found := make(map[string]bool)
682682
for _, target := range targets {
683-
key := target.Host + ":" + target.Port + ":" + target.Type
683+
key := formatSocketAddress(target.Host, target.Port) + ":" + target.Type
684684
found[key] = true
685685
}
686686

0 commit comments

Comments
 (0)