Browse Source
fix: canonical replica address resolution (CP13-2)
fix: canonical replica address resolution (CP13-2)
ReplicaReceiver.DataAddr()/CtrlAddr() now return canonical ip:port instead of raw listener addresses that may be wildcard (:port, 0.0.0.0:port, [::]:port). New canonicalizeListenerAddr() resolves wildcard IPs using the provided advertised host (from VS listen address). Falls back to outbound-IP detection when no advertised host is available. NewReplicaReceiver accepts optional advertisedHost parameter for multi-NIC correctness. In production, the assignment path already provides canonical addresses; this fix ensures test patterns with :0 bind also produce routable addresses. 7 new tests. TestBug3_ReplicaAddr_MustBeIPPort_WildcardBind flips from FAIL to PASS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>feature/sw-block
3 changed files with 142 additions and 5 deletions
-
53weed/storage/blockvol/net_util.go
-
73weed/storage/blockvol/net_util_test.go
-
21weed/storage/blockvol/replica_apply.go
@ -0,0 +1,53 @@ |
|||
package blockvol |
|||
|
|||
import ( |
|||
"net" |
|||
"strconv" |
|||
) |
|||
|
|||
// canonicalizeListenerAddr resolves wildcard listener addresses to a routable
|
|||
// ip:port string using the provided advertised host as the preferred IP.
|
|||
//
|
|||
// If the listener bound to a wildcard (":0", "0.0.0.0:port", "[::]:port"),
|
|||
// the returned address uses advertisedHost instead of the wildcard.
|
|||
//
|
|||
// If advertisedHost is empty, falls back to preferredOutboundIP() as a
|
|||
// best-effort guess. On multi-NIC hosts, the advertised host should always
|
|||
// be provided to avoid selecting the wrong network interface.
|
|||
func canonicalizeListenerAddr(addr net.Addr, advertisedHost string) string { |
|||
tcpAddr, ok := addr.(*net.TCPAddr) |
|||
if !ok { |
|||
return addr.String() |
|||
} |
|||
ip := tcpAddr.IP |
|||
if ip != nil && !ip.IsUnspecified() { |
|||
// Already bound to a specific IP — return as-is.
|
|||
return net.JoinHostPort(ip.String(), strconv.Itoa(tcpAddr.Port)) |
|||
} |
|||
// Wildcard bind — use advertised host or fallback.
|
|||
host := advertisedHost |
|||
if host == "" { |
|||
host = preferredOutboundIP() |
|||
} |
|||
if host == "" { |
|||
// Last resort: return raw address (will be ":port").
|
|||
return addr.String() |
|||
} |
|||
return net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)) |
|||
} |
|||
|
|||
// preferredOutboundIP returns the machine's preferred outbound IP as a string.
|
|||
// Uses the standard Go pattern: dial a UDP socket (no data sent), read the
|
|||
// local address. Returns "" if discovery fails.
|
|||
//
|
|||
// This is a fallback — callers should prefer an explicitly configured
|
|||
// advertised host when available.
|
|||
func preferredOutboundIP() string { |
|||
conn, err := net.Dial("udp", "8.8.8.8:80") |
|||
if err != nil { |
|||
return "" |
|||
} |
|||
defer conn.Close() |
|||
localAddr := conn.LocalAddr().(*net.UDPAddr) |
|||
return localAddr.IP.String() |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
package blockvol |
|||
|
|||
import ( |
|||
"net" |
|||
"strings" |
|||
"testing" |
|||
) |
|||
|
|||
func TestCanonicalizeAddr_WildcardIPv4_UsesAdvertised(t *testing.T) { |
|||
addr := &net.TCPAddr{IP: net.IPv4zero, Port: 5099} |
|||
result := canonicalizeListenerAddr(addr, "192.168.1.184") |
|||
if result != "192.168.1.184:5099" { |
|||
t.Fatalf("expected 192.168.1.184:5099, got %q", result) |
|||
} |
|||
} |
|||
|
|||
func TestCanonicalizeAddr_WildcardIPv6_UsesAdvertised(t *testing.T) { |
|||
addr := &net.TCPAddr{IP: net.IPv6zero, Port: 5099} |
|||
result := canonicalizeListenerAddr(addr, "10.0.0.3") |
|||
if result != "10.0.0.3:5099" { |
|||
t.Fatalf("expected 10.0.0.3:5099, got %q", result) |
|||
} |
|||
} |
|||
|
|||
func TestCanonicalizeAddr_NilIP_UsesAdvertised(t *testing.T) { |
|||
addr := &net.TCPAddr{IP: nil, Port: 5099} |
|||
result := canonicalizeListenerAddr(addr, "192.168.1.184") |
|||
if result != "192.168.1.184:5099" { |
|||
t.Fatalf("expected 192.168.1.184:5099, got %q", result) |
|||
} |
|||
} |
|||
|
|||
func TestCanonicalizeAddr_AlreadyCanonical_Unchanged(t *testing.T) { |
|||
addr := &net.TCPAddr{IP: net.ParseIP("192.168.1.5"), Port: 5099} |
|||
result := canonicalizeListenerAddr(addr, "10.0.0.1") |
|||
if result != "192.168.1.5:5099" { |
|||
t.Fatalf("expected 192.168.1.5:5099 (unchanged), got %q", result) |
|||
} |
|||
} |
|||
|
|||
func TestCanonicalizeAddr_Loopback_Unchanged(t *testing.T) { |
|||
addr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 3260} |
|||
result := canonicalizeListenerAddr(addr, "192.168.1.184") |
|||
if result != "127.0.0.1:3260" { |
|||
t.Fatalf("expected 127.0.0.1:3260 (loopback intentional), got %q", result) |
|||
} |
|||
} |
|||
|
|||
func TestCanonicalizeAddr_NoAdvertised_FallsBackToOutbound(t *testing.T) { |
|||
addr := &net.TCPAddr{IP: net.IPv4zero, Port: 5099} |
|||
result := canonicalizeListenerAddr(addr, "") |
|||
// Should not be wildcard.
|
|||
if strings.HasPrefix(result, "0.0.0.0:") || strings.HasPrefix(result, "[::]:") || strings.HasPrefix(result, ":") { |
|||
t.Fatalf("fallback should produce routable addr, got %q", result) |
|||
} |
|||
if !strings.Contains(result, ":5099") { |
|||
t.Fatalf("port should be preserved, got %q", result) |
|||
} |
|||
} |
|||
|
|||
func TestPreferredOutboundIP_NotEmpty(t *testing.T) { |
|||
ip := preferredOutboundIP() |
|||
if ip == "" { |
|||
t.Skip("no network interface available") |
|||
} |
|||
parsed := net.ParseIP(ip) |
|||
if parsed == nil { |
|||
t.Fatalf("not a valid IP: %q", ip) |
|||
} |
|||
if parsed.IsUnspecified() { |
|||
t.Fatalf("returned unspecified IP: %q", ip) |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue