diff --git a/go.mod b/go.mod index 604e09521..fe28f6838 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/samber/lo v1.47.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.10.0 + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.30.7 k8s.io/apiextensions-apiserver v0.30.7 diff --git a/go.sum b/go.sum index eced290d7..ce092af91 100644 --- a/go.sum +++ b/go.sum @@ -314,6 +314,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/pkg/helpers/helpers.go b/pkg/helpers/helpers.go index 9abde3f04..c7f3e8997 100644 --- a/pkg/helpers/helpers.go +++ b/pkg/helpers/helpers.go @@ -5,22 +5,39 @@ package helpers import ( "fmt" "net/netip" + "strings" + + "go4.org/netipx" ) -// IsIPInRange checks if the target IP falls within the start and end IP range (inclusive). -func IsIPInRange(startIP, endIP, targetIP string) (bool, error) { - start, err := netip.ParseAddr(startIP) - if err != nil { - return false, fmt.Errorf("invalid start IP: %w", err) - } - end, err := netip.ParseAddr(endIP) - if err != nil { - return false, fmt.Errorf("invalid end IP: %w", err) - } - target, err := netip.ParseAddr(targetIP) +// IsIPInRange returns true if target IP falls within the IP range(inclusive of start and end IP), +// CIDR(inclusive of start and end IP), single IP. +func IsIPInRange(ipAddr, targetIP string) (bool, error) { + parsedTargetIP, err := netip.ParseAddr(targetIP) if err != nil { - return false, fmt.Errorf("invalid target IP: %w", err) + return false, fmt.Errorf("failed to parse target IP %q: %v", targetIP, err) } - return start.Compare(target) <= 0 && end.Compare(target) >= 0, nil + switch { + case strings.Contains(ipAddr, "-"): + ipRange, err := netipx.ParseIPRange(ipAddr) + if err != nil { + return false, fmt.Errorf("failed to parse IP range %q: %v", ipAddr, err) + } + return ipRange.Contains(parsedTargetIP), nil + + case strings.Contains(ipAddr, "/"): + prefix, err := netip.ParsePrefix(ipAddr) + if err != nil { + return false, fmt.Errorf("failed to parse IP prefix %q: %v", ipAddr, err) + } + return prefix.Contains(parsedTargetIP), nil + + default: + parsedIP, err := netip.ParseAddr(ipAddr) + if err != nil { + return false, fmt.Errorf("failed to parse IP address %q: %v", ipAddr, err) + } + return parsedIP.Compare(parsedTargetIP) == 0, nil + } } diff --git a/pkg/helpers/helpers_test.go b/pkg/helpers/helpers_test.go index 360cd1dee..dde168dfc 100644 --- a/pkg/helpers/helpers_test.go +++ b/pkg/helpers/helpers_test.go @@ -13,98 +13,144 @@ import ( func TestIsIPInRange(t *testing.T) { tests := []struct { name string - startIP string - endIP string + ipRange string targetIP string expectedInRange bool expectedErr error }{ { name: "Valid range - target within range", - startIP: "192.168.1.1", - endIP: "192.168.1.10", + ipRange: "192.168.1.1-192.168.1.10", targetIP: "192.168.1.5", expectedInRange: true, expectedErr: nil, }, { name: "Valid range - target same as start IP", - startIP: "192.168.1.1", - endIP: "192.168.1.10", + ipRange: "192.168.1.1-192.168.1.10", targetIP: "192.168.1.1", expectedInRange: true, expectedErr: nil, }, { name: "Valid range - target same as end IP", - startIP: "192.168.1.1", - endIP: "192.168.1.10", + ipRange: "192.168.1.1-192.168.1.10", targetIP: "192.168.1.10", expectedInRange: true, expectedErr: nil, }, { name: "Valid range - target outside range", - startIP: "192.168.1.1", - endIP: "192.168.1.10", + ipRange: "192.168.1.1-192.168.1.10", targetIP: "192.168.1.15", expectedInRange: false, expectedErr: nil, }, { name: "Invalid start IP", - startIP: "invalid-ip", - endIP: "192.168.1.10", + ipRange: "invalidIP-192.168.1.10", targetIP: "192.168.1.5", expectedInRange: false, expectedErr: fmt.Errorf( - "invalid start IP: ParseAddr(%q): unable to parse IP", - "invalid-ip", + "failed to parse IP range %q: invalid From IP %q in range %q", + "invalidIP-192.168.1.10", + "invalidIP", + "invalidIP-192.168.1.10", ), }, { name: "Invalid end IP", - startIP: "192.168.1.1", - endIP: "invalid-ip", + ipRange: "192.168.1.1-invalidIP", targetIP: "192.168.1.5", expectedInRange: false, expectedErr: fmt.Errorf( - "invalid end IP: ParseAddr(%q): unable to parse IP", - "invalid-ip", + "failed to parse IP range %q: invalid To IP %q in range %q", + "192.168.1.1-invalidIP", + "invalidIP", + "192.168.1.1-invalidIP", ), }, { name: "Invalid target IP", - startIP: "192.168.1.1", - endIP: "192.168.1.10", - targetIP: "invalid-ip", + ipRange: "192.168.1.1-192.168.1.10", + targetIP: "invalidIP", expectedInRange: false, expectedErr: fmt.Errorf( - "invalid target IP: ParseAddr(%q): unable to parse IP", - "invalid-ip", + "failed to parse target IP %q: ParseAddr(%q): unable to parse IP", + "invalidIP", + "invalidIP", ), }, { name: "IPv6 range - target within range", - startIP: "2001:db8::1", - endIP: "2001:db8::10", + ipRange: "2001:db8::1-2001:db8::10", targetIP: "2001:db8::5", expectedInRange: true, expectedErr: nil, }, { name: "IPv6 range - target outside range", - startIP: "2001:db8::1", - endIP: "2001:db8::10", + ipRange: "2001:db8::1-2001:db8::10", targetIP: "2001:db8::11", expectedInRange: false, expectedErr: nil, }, + { + name: "IP prefix - target IP inside range", + ipRange: "192.168.1.1/25", + targetIP: "192.168.1.1", + expectedInRange: true, + expectedErr: nil, + }, + { + name: "IP prefix - target IP outside range", + ipRange: "192.168.1.1/25", + targetIP: "192.168.1.251", + expectedInRange: false, + expectedErr: nil, + }, + { + name: "Invalid IP prefix", + ipRange: "192.168.1/25", + targetIP: "192.168.1.251", + expectedInRange: false, + expectedErr: fmt.Errorf( + "failed to parse IP prefix %q: netip.ParsePrefix(%q): ParseAddr(%q): IPv4 address too short", + "192.168.1/25", + "192.168.1/25", + "192.168.1", + ), + }, + { + name: "Single IP - same as target IP", + ipRange: "192.168.1.21", + targetIP: "192.168.1.21", + expectedInRange: true, + expectedErr: nil, + }, + { + name: "Single IP - different from target IP", + ipRange: "192.168.1.21", + targetIP: "192.168.1.211", + expectedInRange: false, + expectedErr: nil, + }, + { + name: "Invalid single IP", + ipRange: "192.168.1", + targetIP: "192.168.1.211", + expectedInRange: false, + expectedErr: fmt.Errorf( + "failed to parse IP address %q: ParseAddr(%q): IPv4 address too short", + "192.168.1", + "192.168.1", + ), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := IsIPInRange(tt.startIP, tt.endIP, tt.targetIP) + got, err := IsIPInRange(tt.ipRange, tt.targetIP) assert.Equal(t, tt.expectedInRange, got) if tt.expectedErr != nil { assert.EqualError(t, err, tt.expectedErr.Error()) diff --git a/pkg/webhook/cluster/nutanix_validator.go b/pkg/webhook/cluster/nutanix_validator.go index 3f3499a5e..845b05cf6 100644 --- a/pkg/webhook/cluster/nutanix_validator.go +++ b/pkg/webhook/cluster/nutanix_validator.go @@ -112,7 +112,7 @@ func validatePrismCentralIPNotInLoadBalancerIPRange( } for _, pool := range serviceLoadBalancerConfiguration.Configuration.AddressRanges { - isIPInRange, err := helpers.IsIPInRange(pool.Start, pool.End, pcIP.String()) + isIPInRange, err := helpers.IsIPInRange(pool.Start+"-"+pool.End, pcIP.String()) if err != nil { return fmt.Errorf( "failed to check if Prism Central IP %q is part of MetalLB address range %q-%q: %w",