diff --git a/configs/milvus.yaml b/configs/milvus.yaml index f39ddd77f2..58eb3d70e2 100644 --- a/configs/milvus.yaml +++ b/configs/milvus.yaml @@ -1035,6 +1035,9 @@ common: useVectorAsClusteringKey: false # if true, do clustering compaction and segment prune on vector field enableVectorClusteringKey: false # if true, enable vector clustering key and vector clustering compaction localRPCEnabled: false # enable local rpc for internal communication when mix or standalone mode. + # Prefer IPv6 addresses when automatically selecting the local IP. + # If enabled, IPv6 ULA/global addresses will be prioritized ahead of IPv4. + preferIPv6: false sync: taskPoolReleaseTimeoutSeconds: 60 # The maximum time to wait for the task to finish and release resources in the pool enabledOptimizeExpr: true # Indicates whether to enable optimize expr diff --git a/pkg/util/funcutil/func.go b/pkg/util/funcutil/func.go index 27168f0436..9a092a22ed 100644 --- a/pkg/util/funcutil/func.go +++ b/pkg/util/funcutil/func.go @@ -30,6 +30,7 @@ import ( "time" "github.com/cockroachdb/errors" + "go.uber.org/atomic" "go.uber.org/zap" "google.golang.org/grpc/codes" grpcStatus "google.golang.org/grpc/status" @@ -46,6 +47,9 @@ const ( ControlChannelSuffix = "vcchan" // is the suffix of the virtual control channel ) +// PreferIPv6LocalIP controls whether IPv6 addresses are preferred when selecting local IPs. +var PreferIPv6LocalIP atomic.Bool + // CheckGrpcReady wait for context timeout, or wait 100ms then send nil to targetCh func CheckGrpcReady(ctx context.Context, targetCh chan error) { timer := time.NewTimer(100 * time.Millisecond) @@ -98,34 +102,114 @@ func GetIP(ip string) string { // GetLocalIP return the local ip address func GetLocalIP() string { addrs, err := net.InterfaceAddrs() - if err == nil { - ip := GetValidLocalIP(addrs) - if len(ip) != 0 { - return ip - } + if err != nil { + log.Warn("Failed to get interface addresses", zap.Error(err)) + return "127.0.0.1" } + + preferIPv6 := PreferIPv6LocalIP.Load() + + ip := getValidLocalIP(addrs, preferIPv6) + if len(ip) != 0 { + return ip + } + + log.Warn("No valid local IP found, falling back to loopback") return "127.0.0.1" } // GetValidLocalIP return the first valid local ip address func GetValidLocalIP(addrs []net.Addr) string { - // Search for valid ipv4 addresses + return getValidLocalIP(addrs, PreferIPv6LocalIP.Load()) +} + +type ipCategory int + +const ( + ipCategoryIPv4Public ipCategory = iota + ipCategoryIPv4Private + ipCategoryIPv6Public + ipCategoryIPv6Private + ipCategoryIPv6LinkLocal +) + +var ( + // Default priority: private first, IPv4 first + defaultIPPriority = []ipCategory{ + ipCategoryIPv4Private, + ipCategoryIPv4Public, + ipCategoryIPv6Private, + ipCategoryIPv6Public, + ipCategoryIPv6LinkLocal, + } + // When IPv6 is preferred: private first, IPv6 first + preferIPv6Priority = []ipCategory{ + ipCategoryIPv6Private, + ipCategoryIPv6Public, + ipCategoryIPv4Private, + ipCategoryIPv4Public, + ipCategoryIPv6LinkLocal, + } +) + +func getValidLocalIP(addrs []net.Addr, preferIPv6 bool) string { + candidates := make(map[ipCategory]net.IP, 5) + for _, addr := range addrs { - ipaddr, ok := addr.(*net.IPNet) - if ok && ipaddr.IP.IsGlobalUnicast() && ipaddr.IP.To4() != nil { - return ipaddr.IP.String() + ipNet, ok := addr.(*net.IPNet) + if !ok { + continue + } + + category, valid := categorizeLocalIP(ipNet.IP) + if !valid { + continue + } + + if _, exists := candidates[category]; !exists { + ipCopy := make(net.IP, len(ipNet.IP)) + copy(ipCopy, ipNet.IP) + candidates[category] = ipCopy } } - // Search for valid ipv6 addresses - for _, addr := range addrs { - ipaddr, ok := addr.(*net.IPNet) - if ok && ipaddr.IP.IsGlobalUnicast() && ipaddr.IP.To16() != nil && ipaddr.IP.To4() == nil { - return "[" + ipaddr.IP.String() + "]" + + priorities := defaultIPPriority + if preferIPv6 { + priorities = preferIPv6Priority + } + + for _, category := range priorities { + if ip, exists := candidates[category]; exists { + result := formatLocalIP(ip) + log.Debug("Selected IP by priority", + zap.String("ip", result), + zap.String("categoryName", getCategoryName(category))) + return result } } + + log.Warn("No valid IP found in candidates") return "" } +// getCategoryName returns human-readable name for IP category (for debugging) +func getCategoryName(category ipCategory) string { + switch category { + case ipCategoryIPv4Private: + return "IPv4Private" + case ipCategoryIPv4Public: + return "IPv4Public" + case ipCategoryIPv6Private: + return "IPv6Private" + case ipCategoryIPv6Public: + return "IPv6Public" + case ipCategoryIPv6LinkLocal: + return "IPv6LinkLocal" + default: + return "Unknown" + } +} + // JSONToMap parse the jsonic index parameters to map func JSONToMap(mStr string) (map[string]string, error) { buffer := make(map[string]any) @@ -659,3 +743,73 @@ func DecodeUserRoleCache(cache string) (string, string, error) { role := cache[index+1:] return user, role, nil } + +// isIPv4Private checks if an IPv4 address is in RFC 1918 private ranges +func isIPv4Private(ip net.IP) bool { + ipv4 := ip.To4() + if ipv4 == nil { + return false + } + // RFC 1918 private address ranges: + // 10.0.0.0/8 (10.0.0.0 to 10.255.255.255) + // 172.16.0.0/12 (172.16.0.0 to 172.31.255.255) + // 192.168.0.0/16 (192.168.0.0 to 192.168.255.255) + return ipv4[0] == 10 || + (ipv4[0] == 172 && ipv4[1] >= 16 && ipv4[1] <= 31) || + (ipv4[0] == 192 && ipv4[1] == 168) +} + +func categorizeLocalIP(ip net.IP) (ipCategory, bool) { + if ip == nil { + return 0, false + } + + if ip.IsLoopback() || ip.IsInterfaceLocalMulticast() || ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() { + return 0, false + } + + if ipv4 := ip.To4(); ipv4 != nil { + if !ip.IsGlobalUnicast() { + return 0, false + } + if isIPv4Private(ipv4) { + return ipCategoryIPv4Private, true + } + return ipCategoryIPv4Public, true + } + + ipv6 := ip.To16() + if ipv6 == nil { + return 0, false + } + + if ip.IsLinkLocalUnicast() { + return ipCategoryIPv6LinkLocal, true + } + if isIPv6Private(ipv6) { + return ipCategoryIPv6Private, true + } + if ip.IsGlobalUnicast() { + return ipCategoryIPv6Public, true + } + + log.Debug("IP categorization: uncategorized IPv6", zap.String("ip", ip.String())) + return 0, false +} + +// isIPv6Private checks if an IPv6 address is in private ranges +func isIPv6Private(ip net.IP) bool { + ip = ip.To16() + if len(ip) != net.IPv6len { + return false + } + // RFC 4193 Unique Local Addresses (ULA): fc00::/7 + return ip[0]&0xfe == 0xfc +} + +func formatLocalIP(ip net.IP) string { + if ip.To4() != nil { + return ip.String() + } + return "[" + ip.String() + "]" +} diff --git a/pkg/util/funcutil/func_test.go b/pkg/util/funcutil/func_test.go index 61d75a1211..62afa44325 100644 --- a/pkg/util/funcutil/func_test.go +++ b/pkg/util/funcutil/func_test.go @@ -40,6 +40,14 @@ import ( "github.com/milvus-io/milvus/pkg/v2/util/typeutil" ) +func setPreferIPv6ForTest(t *testing.T, prefer bool) { + original := PreferIPv6LocalIP.Load() + PreferIPv6LocalIP.Store(prefer) + t.Cleanup(func() { + PreferIPv6LocalIP.Store(original) + }) +} + func Test_CheckGrpcReady(t *testing.T) { errChan := make(chan error) @@ -56,6 +64,7 @@ func Test_CheckGrpcReady(t *testing.T) { } func Test_GetValidLocalIPNoValid(t *testing.T) { + setPreferIPv6ForTest(t, false) addrs := make([]net.Addr, 0, 1) addrs = append(addrs, &net.IPNet{IP: net.IPv4(127, 1, 1, 1), Mask: net.IPv4Mask(255, 255, 255, 255)}) ip := GetValidLocalIP(addrs) @@ -63,6 +72,7 @@ func Test_GetValidLocalIPNoValid(t *testing.T) { } func Test_GetValidLocalIPIPv4(t *testing.T) { + setPreferIPv6ForTest(t, false) addrs := make([]net.Addr, 0, 1) addrs = append(addrs, &net.IPNet{IP: net.IPv4(100, 1, 1, 1), Mask: net.IPv4Mask(255, 255, 255, 255)}) ip := GetValidLocalIP(addrs) @@ -70,13 +80,33 @@ func Test_GetValidLocalIPIPv4(t *testing.T) { } func Test_GetValidLocalIPIPv6(t *testing.T) { + setPreferIPv6ForTest(t, false) addrs := make([]net.Addr, 0, 1) addrs = append(addrs, &net.IPNet{IP: net.IP{8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.IPMask{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}) ip := GetValidLocalIP(addrs) assert.Equal(t, "[800::]", ip) } +func Test_GetValidLocalIPIPv6ULA(t *testing.T) { + setPreferIPv6ForTest(t, false) + addrs := []net.Addr{ + &net.IPNet{IP: net.ParseIP("fd12::1"), Mask: net.CIDRMask(64, 128)}, + } + ip := GetValidLocalIP(addrs) + assert.Equal(t, "[fd12::1]", ip) +} + +func Test_GetValidLocalIPIPv6LinkLocal(t *testing.T) { + setPreferIPv6ForTest(t, false) + addrs := []net.Addr{ + &net.IPNet{IP: net.ParseIP("fe80::1"), Mask: net.CIDRMask(64, 128)}, + } + ip := GetValidLocalIP(addrs) + assert.Equal(t, "[fe80::1]", ip) +} + func Test_GetValidLocalIPIPv4Priority(t *testing.T) { + setPreferIPv6ForTest(t, false) addrs := make([]net.Addr, 0, 1) addrs = append(addrs, &net.IPNet{IP: net.IP{8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.IPMask{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}) addrs = append(addrs, &net.IPNet{IP: net.IPv4(100, 1, 1, 1), Mask: net.IPv4Mask(255, 255, 255, 255)}) @@ -84,7 +114,18 @@ func Test_GetValidLocalIPIPv4Priority(t *testing.T) { assert.Equal(t, "100.1.1.1", ip) } +func Test_GetValidLocalIPPreferIPv6(t *testing.T) { + setPreferIPv6ForTest(t, true) + addrs := []net.Addr{ + &net.IPNet{IP: net.IPv4(100, 1, 1, 1), Mask: net.IPv4Mask(255, 255, 255, 255)}, + &net.IPNet{IP: net.ParseIP("fd12::1"), Mask: net.CIDRMask(64, 128)}, + } + ip := GetValidLocalIP(addrs) + assert.Equal(t, "[fd12::1]", ip) +} + func Test_GetLocalIP(t *testing.T) { + setPreferIPv6ForTest(t, false) ip := GetLocalIP() assert.NotNil(t, ip) assert.NotZero(t, len(ip)) @@ -1002,3 +1043,288 @@ func TestString2KeyValuePair(t *testing.T) { assert.Len(t, kvs, 0) }) } + +func Test_PrivatePublicAddresses(t *testing.T) { + t.Run("isIPv4Private", func(t *testing.T) { + // Test private IPv4 addresses + assert.True(t, isIPv4Private(net.IPv4(10, 0, 0, 1))) + assert.True(t, isIPv4Private(net.IPv4(10, 255, 255, 255))) + assert.True(t, isIPv4Private(net.IPv4(172, 16, 0, 1))) + assert.True(t, isIPv4Private(net.IPv4(172, 31, 255, 255))) + assert.True(t, isIPv4Private(net.IPv4(192, 168, 1, 1))) + assert.True(t, isIPv4Private(net.IPv4(192, 168, 255, 255))) + + // Test public IPv4 addresses + assert.False(t, isIPv4Private(net.IPv4(8, 8, 8, 8))) + assert.False(t, isIPv4Private(net.IPv4(1, 1, 1, 1))) + assert.False(t, isIPv4Private(net.IPv4(172, 15, 0, 1))) // Just outside private range + assert.False(t, isIPv4Private(net.IPv4(172, 32, 0, 1))) // Just outside private range + assert.False(t, isIPv4Private(net.IPv4(192, 167, 1, 1))) // Just outside private range + assert.False(t, isIPv4Private(net.IPv4(192, 169, 1, 1))) // Just outside private range + + // Test IPv6 addresses (should return false) + assert.False(t, isIPv4Private(net.ParseIP("2001:db8::1"))) + assert.False(t, isIPv4Private(net.ParseIP("fd12::1"))) + }) + + t.Run("isIPv6Private", func(t *testing.T) { + // Test IPv6 private addresses (ULA) + assert.True(t, isIPv6Private(net.ParseIP("fd12::1"))) + assert.True(t, isIPv6Private(net.ParseIP("fc00::1"))) + assert.True(t, isIPv6Private(net.ParseIP("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"))) + + // Test IPv6 public addresses + assert.False(t, isIPv6Private(net.ParseIP("2001:db8::1"))) + assert.False(t, isIPv6Private(net.ParseIP("2001:4860:4860::8888"))) // Google DNS + assert.False(t, isIPv6Private(net.ParseIP("2606:4700:4700::1111"))) // Cloudflare DNS + + // Test IPv6 link-local (not considered private in our definition) + assert.False(t, isIPv6Private(net.ParseIP("fe80::1"))) + + // Test IPv4 addresses (should return false) + assert.False(t, isIPv6Private(net.IPv4(192, 168, 1, 1))) + assert.False(t, isIPv6Private(net.IPv4(8, 8, 8, 8))) + }) + + t.Run("categorizeLocalIP_IPv4", func(t *testing.T) { + // Test IPv4 private address categorization + category, valid := categorizeLocalIP(net.IPv4(192, 168, 1, 1)) + assert.True(t, valid) + assert.Equal(t, ipCategoryIPv4Private, category) + + // Test IPv4 public address categorization + category, valid = categorizeLocalIP(net.IPv4(8, 8, 8, 8)) + assert.True(t, valid) + assert.Equal(t, ipCategoryIPv4Public, category) + + // Test invalid addresses + _, valid = categorizeLocalIP(net.IPv4(127, 0, 0, 1)) // loopback + assert.False(t, valid) + + _, valid = categorizeLocalIP(net.IPv4(224, 0, 0, 1)) // multicast + assert.False(t, valid) + }) + + t.Run("categorizeLocalIP_IPv6", func(t *testing.T) { + // Test IPv6 private address categorization (ULA) + category, valid := categorizeLocalIP(net.ParseIP("fd12::1")) + assert.True(t, valid) + assert.Equal(t, ipCategoryIPv6Private, category) + + // Test IPv6 public address categorization + category, valid = categorizeLocalIP(net.ParseIP("2001:db8::1")) + assert.True(t, valid) + assert.Equal(t, ipCategoryIPv6Public, category) + + // Test IPv6 link-local address categorization + category, valid = categorizeLocalIP(net.ParseIP("fe80::1")) + assert.True(t, valid) + assert.Equal(t, ipCategoryIPv6LinkLocal, category) + + // Test invalid IPv6 addresses + _, valid = categorizeLocalIP(net.ParseIP("::1")) // loopback + assert.False(t, valid) + + _, valid = categorizeLocalIP(net.ParseIP("ff02::1")) // multicast + assert.False(t, valid) + }) +} + +func Test_GetValidLocalIPSimplified(t *testing.T) { + t.Run("default_behavior_private_first_ipv4_first", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + addrs := []net.Addr{ + &net.IPNet{IP: net.IPv4(8, 8, 8, 8), Mask: net.IPv4Mask(255, 255, 255, 0)}, // Public IPv4 + &net.IPNet{IP: net.IPv4(192, 168, 1, 1), Mask: net.IPv4Mask(255, 255, 255, 0)}, // Private IPv4 + &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)}, // Public IPv6 + &net.IPNet{IP: net.ParseIP("fd12::1"), Mask: net.CIDRMask(64, 128)}, // Private IPv6 + } + ip := GetValidLocalIP(addrs) + // Should prefer private IPv4 first + assert.Equal(t, "192.168.1.1", ip) + }) + + t.Run("ipv6_preferred_private_first", func(t *testing.T) { + setPreferIPv6ForTest(t, true) + + addrs := []net.Addr{ + &net.IPNet{IP: net.IPv4(8, 8, 8, 8), Mask: net.IPv4Mask(255, 255, 255, 0)}, // Public IPv4 + &net.IPNet{IP: net.IPv4(192, 168, 1, 1), Mask: net.IPv4Mask(255, 255, 255, 0)}, // Private IPv4 + &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)}, // Public IPv6 + &net.IPNet{IP: net.ParseIP("fd12::1"), Mask: net.CIDRMask(64, 128)}, // Private IPv6 + } + ip := GetValidLocalIP(addrs) + // Should prefer private IPv6 first when IPv6 is preferred + assert.Equal(t, "[fd12::1]", ip) + }) + + t.Run("only_public_addresses_available", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + addrs := []net.Addr{ + &net.IPNet{IP: net.IPv4(8, 8, 8, 8), Mask: net.IPv4Mask(255, 255, 255, 0)}, // Public IPv4 + &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)}, // Public IPv6 + } + ip := GetValidLocalIP(addrs) + // Should fallback to public IPv4 when no private available + assert.Equal(t, "8.8.8.8", ip) + }) + + t.Run("only_ipv6_addresses_available", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + addrs := []net.Addr{ + &net.IPNet{IP: net.ParseIP("fd12::1"), Mask: net.CIDRMask(64, 128)}, // Private IPv6 + &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)}, // Public IPv6 + } + ip := GetValidLocalIP(addrs) + // Should prefer private IPv6 even when IPv4 is preferred but not available + assert.Equal(t, "[fd12::1]", ip) + }) +} + +func Test_GetValidLocalIPEdgeCases(t *testing.T) { + t.Run("empty_address_list", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + addrs := []net.Addr{} + ip := GetValidLocalIP(addrs) + assert.Equal(t, "", ip) + }) + + t.Run("nil_address_list", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + ip := GetValidLocalIP(nil) + assert.Equal(t, "", ip) + }) + + t.Run("only_loopback_ipv4", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + addrs := []net.Addr{ + &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)}, + } + ip := GetValidLocalIP(addrs) + // Loopback should be filtered out + assert.Equal(t, "", ip) + }) + + t.Run("only_loopback_ipv6", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + addrs := []net.Addr{ + &net.IPNet{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)}, + } + ip := GetValidLocalIP(addrs) + // Loopback should be filtered out + assert.Equal(t, "", ip) + }) + + t.Run("only_multicast", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + addrs := []net.Addr{ + &net.IPNet{IP: net.ParseIP("224.0.0.1"), Mask: net.IPv4Mask(240, 0, 0, 0)}, + &net.IPNet{IP: net.ParseIP("ff02::1"), Mask: net.CIDRMask(8, 128)}, + } + ip := GetValidLocalIP(addrs) + // Multicast should be filtered out + assert.Equal(t, "", ip) + }) + + t.Run("ipv4_mapped_ipv6_treated_as_ipv4", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + // IPv4-mapped IPv6 address (::ffff:192.168.1.1) + ipv4Mapped := net.ParseIP("::ffff:192.168.1.1") + addrs := []net.Addr{ + &net.IPNet{IP: ipv4Mapped, Mask: net.CIDRMask(128, 128)}, + } + ip := GetValidLocalIP(addrs) + // IPv4-mapped IPv6 should be treated as IPv4 (no brackets) + assert.Equal(t, "192.168.1.1", ip) + }) + + t.Run("mixed_invalid_and_valid", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + addrs := []net.Addr{ + &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)}, // Loopback - filtered + &net.IPNet{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)}, // Loopback - filtered + &net.IPNet{IP: net.ParseIP("224.0.0.1"), Mask: net.IPv4Mask(240, 0, 0, 0)}, // Multicast - filtered + &net.IPNet{IP: net.IPv4(192, 168, 1, 100), Mask: net.IPv4Mask(255, 255, 255, 0)}, // Valid private IPv4 + } + ip := GetValidLocalIP(addrs) + assert.Equal(t, "192.168.1.100", ip) + }) + + t.Run("non_ipnet_addr_type", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + // Use a different Addr type that's not *net.IPNet + addrs := []net.Addr{ + &net.TCPAddr{IP: net.IPv4(192, 168, 1, 1), Port: 8080}, + } + ip := GetValidLocalIP(addrs) + // Non-IPNet types should be skipped + assert.Equal(t, "", ip) + }) + + t.Run("link_local_ipv6_lowest_priority", func(t *testing.T) { + setPreferIPv6ForTest(t, false) + + addrs := []net.Addr{ + &net.IPNet{IP: net.ParseIP("fe80::1"), Mask: net.CIDRMask(64, 128)}, // Link-local IPv6 + } + ip := GetValidLocalIP(addrs) + // Link-local should be selected when it's the only option + assert.Equal(t, "[fe80::1]", ip) + }) + + t.Run("link_local_vs_public_ipv6", func(t *testing.T) { + setPreferIPv6ForTest(t, true) + + addrs := []net.Addr{ + &net.IPNet{IP: net.ParseIP("fe80::1"), Mask: net.CIDRMask(64, 128)}, // Link-local IPv6 + &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)}, // Public IPv6 + } + ip := GetValidLocalIP(addrs) + // Public IPv6 should be preferred over link-local + assert.Equal(t, "[2001:db8::1]", ip) + }) +} + +func Test_FormatLocalIP(t *testing.T) { + t.Run("ipv4_no_brackets", func(t *testing.T) { + ip := net.IPv4(192, 168, 1, 1) + result := formatLocalIP(ip) + assert.Equal(t, "192.168.1.1", result) + }) + + t.Run("ipv6_with_brackets", func(t *testing.T) { + ip := net.ParseIP("2001:db8::1") + result := formatLocalIP(ip) + assert.Equal(t, "[2001:db8::1]", result) + }) + + t.Run("ipv6_ula_with_brackets", func(t *testing.T) { + ip := net.ParseIP("fd12::1") + result := formatLocalIP(ip) + assert.Equal(t, "[fd12::1]", result) + }) + + t.Run("ipv6_link_local_with_brackets", func(t *testing.T) { + ip := net.ParseIP("fe80::1") + result := formatLocalIP(ip) + assert.Equal(t, "[fe80::1]", result) + }) + + t.Run("ipv4_mapped_ipv6_no_brackets", func(t *testing.T) { + // IPv4-mapped IPv6 should be formatted as IPv4 (no brackets) + ip := net.ParseIP("::ffff:192.168.1.1") + result := formatLocalIP(ip) + assert.Equal(t, "192.168.1.1", result) + }) +} diff --git a/pkg/util/paramtable/component_param.go b/pkg/util/paramtable/component_param.go index 26a8620c41..19805b857d 100644 --- a/pkg/util/paramtable/component_param.go +++ b/pkg/util/paramtable/component_param.go @@ -32,6 +32,7 @@ import ( "github.com/milvus-io/milvus/pkg/v2/config" "github.com/milvus-io/milvus/pkg/v2/log" + "github.com/milvus-io/milvus/pkg/v2/util/funcutil" "github.com/milvus-io/milvus/pkg/v2/util/hardware" "github.com/milvus-io/milvus/pkg/v2/util/metricsinfo" "github.com/milvus-io/milvus/pkg/v2/util/typeutil" @@ -323,6 +324,8 @@ type commonConfig struct { // Local RPC enabled for milvus internal communication when mix or standalone mode. LocalRPCEnabled ParamItem `refreshable:"false"` + PreferIPv6LocalIP ParamItem `refreshable:"false"` + SyncTaskPoolReleaseTimeoutSeconds ParamItem `refreshable:"true"` EnabledOptimizeExpr ParamItem `refreshable:"true"` @@ -1219,6 +1222,17 @@ The default value is 1, which is enough for most cases.`, } p.LocalRPCEnabled.Init(base.mgr) + p.PreferIPv6LocalIP = ParamItem{ + Key: "common.preferIPv6", + Version: "2.5.10", + DefaultValue: "false", + Doc: `Prefer IPv6 addresses when automatically selecting the local IP. +If enabled, IPv6 ULA/global addresses will be prioritized ahead of IPv4.`, + Export: true, + } + p.PreferIPv6LocalIP.Init(base.mgr) + funcutil.PreferIPv6LocalIP.Store(p.PreferIPv6LocalIP.GetAsBool()) + p.SyncTaskPoolReleaseTimeoutSeconds = ParamItem{ Key: "common.sync.taskPoolReleaseTimeoutSeconds", DefaultValue: "60",