mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-21 17:21:34 +00:00
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>
This commit is contained in:
53
weed/storage/blockvol/net_util.go
Normal file
53
weed/storage/blockvol/net_util.go
Normal file
@@ -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()
|
||||
}
|
||||
73
weed/storage/blockvol/net_util_test.go
Normal file
73
weed/storage/blockvol/net_util_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ var (
|
||||
type ReplicaReceiver struct {
|
||||
vol *BlockVol
|
||||
barrierTimeout time.Duration
|
||||
advertisedHost string // canonical IP for address reporting; empty = auto-detect
|
||||
|
||||
mu sync.Mutex
|
||||
receivedLSN uint64
|
||||
@@ -38,7 +39,10 @@ type ReplicaReceiver struct {
|
||||
const defaultBarrierTimeout = 5 * time.Second
|
||||
|
||||
// NewReplicaReceiver creates and starts listening on the data and control ports.
|
||||
func NewReplicaReceiver(vol *BlockVol, dataAddr, ctrlAddr string) (*ReplicaReceiver, error) {
|
||||
// advertisedHost is the canonical IP for this server (from VS listen addr or
|
||||
// heartbeat identity). If empty, DataAddr()/CtrlAddr() will fall back to
|
||||
// outbound-IP detection. On multi-NIC hosts, always provide advertisedHost.
|
||||
func NewReplicaReceiver(vol *BlockVol, dataAddr, ctrlAddr string, advertisedHost ...string) (*ReplicaReceiver, error) {
|
||||
dataLn, err := net.Listen("tcp", dataAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("replica: listen data %s: %w", dataAddr, err)
|
||||
@@ -49,9 +53,14 @@ func NewReplicaReceiver(vol *BlockVol, dataAddr, ctrlAddr string) (*ReplicaRecei
|
||||
return nil, fmt.Errorf("replica: listen ctrl %s: %w", ctrlAddr, err)
|
||||
}
|
||||
|
||||
var advHost string
|
||||
if len(advertisedHost) > 0 {
|
||||
advHost = advertisedHost[0]
|
||||
}
|
||||
r := &ReplicaReceiver{
|
||||
vol: vol,
|
||||
barrierTimeout: defaultBarrierTimeout,
|
||||
advertisedHost: advHost,
|
||||
dataListener: dataLn,
|
||||
ctrlListener: ctrlLn,
|
||||
stopCh: make(chan struct{}),
|
||||
@@ -293,12 +302,14 @@ func (r *ReplicaReceiver) ReceivedLSN() uint64 {
|
||||
return r.receivedLSN
|
||||
}
|
||||
|
||||
// DataAddr returns the data listener's address (useful for tests with :0 ports).
|
||||
// DataAddr returns the data listener's canonical address (ip:port).
|
||||
// Wildcard listener addresses are resolved using the advertised host
|
||||
// or outbound-IP fallback.
|
||||
func (r *ReplicaReceiver) DataAddr() string {
|
||||
return r.dataListener.Addr().String()
|
||||
return canonicalizeListenerAddr(r.dataListener.Addr(), r.advertisedHost)
|
||||
}
|
||||
|
||||
// CtrlAddr returns the control listener's address.
|
||||
// CtrlAddr returns the control listener's canonical address (ip:port).
|
||||
func (r *ReplicaReceiver) CtrlAddr() string {
|
||||
return r.ctrlListener.Addr().String()
|
||||
return canonicalizeListenerAddr(r.ctrlListener.Addr(), r.advertisedHost)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user