From 65e5b3ffb12cbe19757316290b85ae40fe442ed1 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Sun, 28 Jan 2024 23:18:20 +0530 Subject: [PATCH] pkg/controller/externalippool: enhance validation * Validate if any IPRange don't overlap with another IPRange of current or existing pool * Validate if IPRange.Start <= IPRange.End * Validate if IPRange.Start and IPRange.End belong to same IP family Signed-off-by: Daman Arora --- pkg/controller/externalippool/validate.go | 154 ++++++-- .../externalippool/validate_test.go | 363 +++++++++++++++--- 2 files changed, 425 insertions(+), 92 deletions(-) diff --git a/pkg/controller/externalippool/validate.go b/pkg/controller/externalippool/validate.go index 6a5719f2e11..c62abb8da9d 100644 --- a/pkg/controller/externalippool/validate.go +++ b/pkg/controller/externalippool/validate.go @@ -17,15 +17,16 @@ package externalippool import ( "encoding/json" "fmt" - "net" + "net/netip" admv1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" crdv1beta1 "antrea.io/antrea/pkg/apis/crd/v1beta1" - "antrea.io/antrea/pkg/util/ip" + utilip "antrea.io/antrea/pkg/util/ip" ) func (c *ExternalIPPoolController) ValidateExternalIPPool(review *admv1.AdmissionReview) *admv1.AdmissionResponse { @@ -48,15 +49,21 @@ func (c *ExternalIPPoolController) ValidateExternalIPPool(review *admv1.Admissio } } + externalIPPools, err := c.externalIPPoolLister.List(labels.Everything()) + if err != nil { + klog.ErrorS(err, "Error listing ExternalIPPools") + return newAdmissionResponseForErr(err) + } + switch review.Request.Operation { case admv1.Create: klog.V(2).Info("Validating CREATE request for ExternalIPPool") - if msg, allowed = validateIPRangesAndSubnetInfo(newObj.Spec.IPRanges, newObj.Spec.SubnetInfo); !allowed { + if msg, allowed = validateIPRangesAndSubnetInfo(newObj.Spec.IPRanges, newObj.Spec.SubnetInfo, externalIPPools); !allowed { break } case admv1.Update: klog.V(2).Info("Validating UPDATE request for ExternalIPPool") - if msg, allowed = validateIPRangesAndSubnetInfo(newObj.Spec.IPRanges, newObj.Spec.SubnetInfo); !allowed { + if msg, allowed = validateIPRangesAndSubnetInfo(newObj.Spec.IPRanges, newObj.Spec.SubnetInfo, externalIPPools); !allowed { break } oldIPRangeSet := getIPRangeSet(oldObj.Spec.IPRanges) @@ -83,47 +90,132 @@ func (c *ExternalIPPoolController) ValidateExternalIPPool(review *admv1.Admissio } } -func validateIPRangesAndSubnetInfo(ipRanges []crdv1beta1.IPRange, subnetInfo *crdv1beta1.SubnetInfo) (string, bool) { - if subnetInfo == nil { - return "", true - } - gatewayIP := net.ParseIP(subnetInfo.Gateway) - var mask net.IPMask - if gatewayIP.To4() != nil { - if subnetInfo.PrefixLength <= 0 || subnetInfo.PrefixLength >= 32 { - return fmt.Sprintf("invalid prefixLength %d", subnetInfo.PrefixLength), false +func validateIPRangesAndSubnetInfo(ipRanges []crdv1beta1.IPRange, subnetInfo *crdv1beta1.SubnetInfo, existingExternalIPPools []*crdv1beta1.ExternalIPPool) (string, bool) { + var subnet *netip.Prefix + if subnetInfo != nil { + gatewayAddr, err := netip.ParseAddr(subnetInfo.Gateway) + if err != nil { + return fmt.Sprintf("invalid gateway address %s", subnetInfo.Gateway), false } - mask = net.CIDRMask(int(subnetInfo.PrefixLength), 32) - } else { - if subnetInfo.PrefixLength <= 0 || subnetInfo.PrefixLength >= 128 { - return fmt.Sprintf("invalid prefixLength %d", subnetInfo.PrefixLength), false + + if gatewayAddr.Is4() { + if subnetInfo.PrefixLength <= 0 || subnetInfo.PrefixLength >= 32 { + return fmt.Sprintf("invalid prefixLength %d", subnetInfo.PrefixLength), false + } + } else { + if subnetInfo.PrefixLength <= 0 || subnetInfo.PrefixLength >= 128 { + return fmt.Sprintf("invalid prefixLength %d", subnetInfo.PrefixLength), false + } } - mask = net.CIDRMask(int(subnetInfo.PrefixLength), 128) + prefix := netip.PrefixFrom(gatewayAddr, int(subnetInfo.PrefixLength)).Masked() + subnet = &prefix } - subnet := &net.IPNet{ - IP: gatewayIP.Mask(mask), - Mask: mask, + + // combinedRanges combines both CIDR and start-end style range together mapped to start and end + // address of the range. We populate the map with ranges of existing pools and incorporate + // the ranges from the current pool as we iterate over them. The map's key is utilized to preserve + // the original user-specified input for formatting validation error, if it occurs. + combinedRanges := make(map[string][2]netip.Addr) + for _, externalIPPool := range existingExternalIPPools { + for _, ipRange := range externalIPPool.Spec.IPRanges { + var key string + var start, end netip.Addr + + if ipRange.CIDR != "" { + key = fmt.Sprintf("range [%s] of pool %s", ipRange.CIDR, externalIPPool.Name) + cidr, _ := parseIPRangeCIDR(ipRange.CIDR) + start, end = utilip.GetStartAndEndOfPrefix(cidr) + + } else { + key = fmt.Sprintf("range [%s-%s] of pool %s", ipRange.Start, ipRange.End, externalIPPool.Name) + start, end, _ = parseIPRangeStartEnd(ipRange.Start, ipRange.End) + + } + combinedRanges[key] = [2]netip.Addr{start, end} + } } + for _, ipRange := range ipRanges { + var key string + var start, end netip.Addr + if ipRange.CIDR != "" { - _, cidr, err := net.ParseCIDR(ipRange.CIDR) - if err != nil { - return err.Error(), false - } - if !ip.IPNetContains(subnet, cidr) { - return fmt.Sprintf("cidr %s must be a strict subset of the subnet", ipRange.CIDR), false + key = fmt.Sprintf("range [%s]", ipRange.CIDR) + cidr, errMsg := parseIPRangeCIDR(ipRange.CIDR) + if errMsg != "" { + return errMsg, false } + start, end = utilip.GetStartAndEndOfPrefix(cidr) + } else { - start := net.ParseIP(ipRange.Start) - end := net.ParseIP(ipRange.End) - if !subnet.Contains(start) || !subnet.Contains(end) { - return fmt.Sprintf("IP range %s-%s must be a strict subset of the subnet", ipRange.Start, ipRange.End), false + key = fmt.Sprintf("range [%s-%s]", ipRange.Start, ipRange.End) + + var errMsg string + start, end, errMsg = parseIPRangeStartEnd(ipRange.Start, ipRange.End) + if errMsg != "" { + return errMsg, false + } + + // validate if start and end belong to same ip family + if start.Is4() != end.Is4() { + return fmt.Sprintf("range start %s and range end %s should belong to same family", + ipRange.Start, ipRange.End), false + } + + // validate if start address <= end address + if start.Compare(end) == 1 { + return fmt.Sprintf("range start %s should not be greater than range end %s", + ipRange.Start, ipRange.End), false + } + } + + // validate if range is subset of given subnet info + if subnet != nil && !(subnet.Contains(start) && subnet.Contains(end)) { + return fmt.Sprintf("%s must be a strict subset of the subnet %s/%d", + key, subnetInfo.Gateway, subnetInfo.PrefixLength), false + } + + // validate if the range overlaps with ranges of any existing pool or already processed + // range of current pool. + for combinedKey, combinedRange := range combinedRanges { + if !(start.Compare(combinedRange[1]) == 1 || end.Compare(combinedRange[0]) == -1) { + return fmt.Sprintf("%s overlaps with %s", key, combinedKey), false } } + + combinedRanges[key] = [2]netip.Addr{start, end} } return "", true } +func parseIPRangeCIDR(cidrStr string) (netip.Prefix, string) { + var cidr netip.Prefix + var err error + + cidr, err = netip.ParsePrefix(cidrStr) + if err != nil { + return cidr, fmt.Sprintf("invalid cidr %s", cidrStr) + } + cidr = cidr.Masked() + return cidr, "" +} + +func parseIPRangeStartEnd(startStr, endStr string) (netip.Addr, netip.Addr, string) { + var start, end netip.Addr + var err error + + start, err = netip.ParseAddr(startStr) + if err != nil { + return start, end, fmt.Sprintf("invalid start ip address %s", startStr) + } + + end, err = netip.ParseAddr(endStr) + if err != nil { + return start, end, fmt.Sprintf("invalid end ip address %s", endStr) + } + return start, end, "" +} + func getIPRangeSet(ipRanges []crdv1beta1.IPRange) sets.Set[string] { set := sets.New[string]() for _, ipRange := range ipRanges { diff --git a/pkg/controller/externalippool/validate_test.go b/pkg/controller/externalippool/validate_test.go index 13422bb6b48..b463449502b 100644 --- a/pkg/controller/externalippool/validate_test.go +++ b/pkg/controller/externalippool/validate_test.go @@ -68,46 +68,6 @@ func TestControllerValidateExternalIPPool(t *testing.T) { }, expectedResponse: &admv1.AdmissionResponse{Allowed: true}, }, - { - name: "CREATE operation with invalid SubnetInfo should not be allowed", - request: &admv1.AdmissionRequest{ - Name: "foo", - Operation: "CREATE", - Object: runtime.RawExtension{Raw: marshal(mutateExternalIPPool(newExternalIPPool("foo", "10.10.10.0/24", "", ""), func(pool *crdv1b1.ExternalIPPool) { - pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ - Gateway: "10.10.11.1", - PrefixLength: 64, - VLAN: 2, - } - }))}, - }, - expectedResponse: &admv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Message: "invalid prefixLength 64", - }, - }, - }, - { - name: "CREATE operation with unmatched SubnetInfo should not be allowed", - request: &admv1.AdmissionRequest{ - Name: "foo", - Operation: "CREATE", - Object: runtime.RawExtension{Raw: marshal(mutateExternalIPPool(newExternalIPPool("foo", "10.10.10.0/24", "", ""), func(pool *crdv1b1.ExternalIPPool) { - pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ - Gateway: "10.10.11.1", - PrefixLength: 24, - VLAN: 2, - } - }))}, - }, - expectedResponse: &admv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Message: "cidr 10.10.10.0/24 must be a strict subset of the subnet", - }, - }, - }, { name: "Adding matched SubnetInfo should be allowed", request: &admv1.AdmissionRequest{ @@ -124,27 +84,6 @@ func TestControllerValidateExternalIPPool(t *testing.T) { }, expectedResponse: &admv1.AdmissionResponse{Allowed: true}, }, - { - name: "Adding unmatched SubnetInfo should not be allowed", - request: &admv1.AdmissionRequest{ - Name: "foo", - Operation: "UPDATE", - OldObject: runtime.RawExtension{Raw: marshal(newExternalIPPool("foo", "10.10.10.0/24", "10.10.20.1", "10.10.20.2"))}, - Object: runtime.RawExtension{Raw: marshal(mutateExternalIPPool(newExternalIPPool("foo", "10.10.10.0/24", "10.10.20.1", "10.10.20.2"), func(pool *crdv1b1.ExternalIPPool) { - pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ - Gateway: "10.10.10.1", - PrefixLength: 24, - VLAN: 2, - } - }))}, - }, - expectedResponse: &admv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Message: "IP range 10.10.20.1-10.10.20.2 must be a strict subset of the subnet", - }, - }, - }, { name: "Deleting IPRange should not be allowed", request: &admv1.AdmissionRequest{ @@ -197,3 +136,305 @@ func TestControllerValidateExternalIPPool(t *testing.T) { }) } } + +func TestValidateIPRangesAndSubnetInfo(t *testing.T) { + testCases := []struct { + name string + externalIPPool *crdv1b1.ExternalIPPool + existingExternalIPPools []*crdv1b1.ExternalIPPool + errMsg string + }{ + { + name: "invalid gateway address", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "10.10.10.0/24", "10.10.20.1", "10.10.20.2"), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ + Gateway: "10.10.0", + PrefixLength: 16, + VLAN: 2, + } + }), + errMsg: "invalid gateway address 10.10.0", + }, + { + name: "invalid ipv4 prefix", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "10.10.10.0/24", "", ""), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ + Gateway: "10.10.0.1", + PrefixLength: 42, + VLAN: 2, + } + }), + errMsg: "invalid prefixLength 42", + }, + { + name: "invalid ipv6 prefix", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "10.10.10.0/24", "", ""), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ + Gateway: "2001:d00::", + PrefixLength: 130, + VLAN: 2, + } + }), + errMsg: "invalid prefixLength 130", + }, + { + name: "range start greater than end", + externalIPPool: newExternalIPPool("foo", "", "10.10.20.0", "10.10.10.0"), + errMsg: "range start 10.10.20.0 should not be greater than range end 10.10.10.0", + }, + { + name: "start-end must belong to same ip family", + externalIPPool: newExternalIPPool("foo", "", "10.10.20.0", "2001:d00::"), + errMsg: "range start 10.10.20.0 and range end 2001:d00:: should belong to same family", + }, + { + name: "start-end range must be within subnet info", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "", "10.10.20.10", "10.10.20.40"), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ + Gateway: "10.10.10.0", + PrefixLength: 24, + VLAN: 2, + } + }), + errMsg: "range [10.10.20.10-10.10.20.40] must be a strict subset of the subnet 10.10.10.0/24", + }, + { + name: "cidr must be within subnet info", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "10.20.0.0/16", "", ""), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ + Gateway: "10.20.0.0", + PrefixLength: 24, + VLAN: 2, + } + }), + errMsg: "range [10.20.0.0/16] must be a strict subset of the subnet 10.20.0.0/24", + }, + { + name: "valid subnet info 1", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "", "10.10.20.10", "10.10.20.20"), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ + Gateway: "10.10.20.0", + PrefixLength: 24, + VLAN: 2, + } + }), + }, + { + name: "valid subnet info 2", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "fd00:10:96::/112", "", ""), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.SubnetInfo = &crdv1b1.SubnetInfo{ + Gateway: "fd00:10:96::", + PrefixLength: 96, + VLAN: 2, + } + }), + }, + + // test cases for cidr range overlap + { + name: "cidr must not overlap with any existing cidr", + externalIPPool: newExternalIPPool("foo", "10.20.30.0/24", "", ""), + existingExternalIPPools: []*crdv1b1.ExternalIPPool{ + newExternalIPPool("bar", "10.10.10.0/24", "", ""), + newExternalIPPool("baz", "10.10.20.0/24", "", ""), + newExternalIPPool("qux", "10.20.0.0/16", "", ""), + }, + errMsg: "range [10.20.30.0/24] overlaps with range [10.20.0.0/16] of pool qux", + }, + { + name: "cidr must not overlap with any existing start-end range", + externalIPPool: newExternalIPPool("foo", "10.20.30.0/24", "", ""), + existingExternalIPPools: []*crdv1b1.ExternalIPPool{ + newExternalIPPool("bar", "", "10.20.30.10", "10.20.30.50"), + }, + errMsg: "range [10.20.30.0/24] overlaps with range [10.20.30.10-10.20.30.50] of pool bar", + }, + { + name: "cidr must not overlap with any cidr", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "10.10.10.0/24", "", ""), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{CIDR: "10.30.20.0/24"}) + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{CIDR: "10.10.0.0/16"}) + }), + errMsg: "range [10.10.0.0/16] overlaps with range [10.10.10.0/24]", + }, + { + name: "cidr must not overlap with any start-end range", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "", "10.10.20.20", "10.10.20.50"), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{CIDR: "10.10.20.0/24"}) + }), + errMsg: "range [10.10.20.0/24] overlaps with range [10.10.20.20-10.10.20.50]", + }, + { + name: "valid non overlapping cidr", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "10.10.20.0/24", "", ""), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{CIDR: "10.10.30.0/24"}) + }), + existingExternalIPPools: []*crdv1b1.ExternalIPPool{ + newExternalIPPool("bar", "", "10.10.40.10", "10.10.40.80"), + newExternalIPPool("baz", "10.10.40.0/24", "", ""), + newExternalIPPool("qux", "10.20.0.0/16", "", ""), + }, + }, + + // test cases for start-end range overlap + { + name: "start-end range must not overlap with any existing cidr", + externalIPPool: newExternalIPPool("foo", "", "10.30.10.0", "10.30.20.0"), + existingExternalIPPools: []*crdv1b1.ExternalIPPool{ + newExternalIPPool("bar", "", "10.10.10.0", "10.10.20.0"), + newExternalIPPool("baz", "10.20.0.0/16", "", ""), + newExternalIPPool("qux", "10.30.0.0/20", "", ""), + }, + errMsg: "range [10.30.10.0-10.30.20.0] overlaps with range [10.30.0.0/20] of pool qux", + }, + { + name: "start-end range must not overlap with any existing start-end range", + externalIPPool: newExternalIPPool("foo", "", "10.30.10.0", "10.30.20.0"), + existingExternalIPPools: []*crdv1b1.ExternalIPPool{ + newExternalIPPool("bar", "10.10.0.0/16", "", ""), + newExternalIPPool("baz", "", "10.30.20.0", "10.30.40.0"), + }, + errMsg: "range [10.30.10.0-10.30.20.0] overlaps with range [10.30.20.0-10.30.40.0] of pool baz", + }, + { + name: "start-end range must not overlap with any cidr", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "10.30.0.0/16", "10.30.40.50", "10.30.40.80"), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{CIDR: "10.30.0.0/16"}) + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{Start: "10.30.40.50", End: "10.30.40.80"}) + }), + errMsg: "range [10.30.40.50-10.30.40.80] overlaps with range [10.30.0.0/16]", + }, + { + name: "start-end range must not overlap with any start-end range", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "", "10.30.40.50", "10.30.40.80"), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{CIDR: "10.30.50.0/24"}) + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{Start: "10.30.40.10", End: "10.30.40.90"}) + }), + errMsg: "range [10.30.40.10-10.30.40.90] overlaps with range [10.30.40.50-10.30.40.80]", + }, + { + name: "valid non overlapping start-end range", + externalIPPool: mutateExternalIPPool(newExternalIPPool("foo", "", "10.30.10.0", "10.30.20.0"), func(pool *crdv1b1.ExternalIPPool) { + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{CIDR: "10.30.50.0/24"}) + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{CIDR: "10.50.0.0/16"}) + pool.Spec.IPRanges = append(pool.Spec.IPRanges, crdv1b1.IPRange{Start: "10.30.20.1", End: "10.30.40.10"}) + }), + existingExternalIPPools: []*crdv1b1.ExternalIPPool{ + newExternalIPPool("bar", "", "10.10.10.0", "10.10.20.0"), + newExternalIPPool("baz", "10.20.0.0/16", "", ""), + newExternalIPPool("baz", "10.40.0.0/16", "", ""), + newExternalIPPool("bar", "", "10.10.10.0", "10.10.20.0"), + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + errMsg, result := validateIPRangesAndSubnetInfo( + testCase.externalIPPool.Spec.IPRanges, + testCase.externalIPPool.Spec.SubnetInfo, + testCase.existingExternalIPPools, + ) + + if testCase.errMsg == "" { + assert.Empty(t, errMsg) + } else { + + assert.Equal(t, testCase.errMsg, errMsg) + if testCase.errMsg != "" { + assert.False(t, result) + } else { + assert.True(t, result) + } + + // test if same message is returned by ValidateExternalIPPool + var fakeObjects []runtime.Object + for _, existingExternalIPPool := range testCase.existingExternalIPPools { + fakeObjects = append(fakeObjects, existingExternalIPPool) + } + + c := newController(fakeObjects) + stopCh := make(chan struct{}) + defer close(stopCh) + c.crdInformerFactory.Start(stopCh) + c.crdInformerFactory.WaitForCacheSync(stopCh) + go c.Run(stopCh) + require.True(t, cache.WaitForCacheSync(stopCh, c.HasSynced)) + review := &admv1.AdmissionReview{ + Request: &admv1.AdmissionRequest{ + Name: testCase.externalIPPool.Name, + Operation: "CREATE", + Object: runtime.RawExtension{Raw: marshal(testCase.externalIPPool)}, + }, + } + response := c.ValidateExternalIPPool(review) + assert.False(t, response.Allowed) + assert.NotNil(t, response.Result) + assert.Equal(t, testCase.errMsg, response.Result.Message) + } + }) + } +} + +func TestParseIPRangeCIDR(t *testing.T) { + testCases := []struct { + name string + cidr string + errMsg string + }{ + { + name: "valid", + cidr: "10.96.10.10/20", + }, + { + name: "invalid ipv4 cidr", + cidr: "10.96.40.50/36", + errMsg: "invalid cidr 10.96.40.50/36", + }, + { + name: "invalid ipv6 cidr", + cidr: "2001:d00::/132", + errMsg: "invalid cidr 2001:d00::/132", + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + // discard parsed net.IPNet, we only need to make assertions on errMsg. + _, errMsg := parseIPRangeCIDR(testCase.cidr) + assert.Equal(t, testCase.errMsg, errMsg) + }) + } +} + +func TestParseIPRangeStartEnd(t *testing.T) { + testCases := []struct { + name string + start string + end string + errMsg string + }{ + { + name: "valid", + start: "10.96.10.10", + end: "10.96.10.20", + }, + { + name: "invalid start ip", + start: "10.96.10.1000", + end: "10.96.10.20", + errMsg: "invalid start ip address 10.96.10.1000", + }, + { + name: "invalid end ip", + start: "2001:d00::", + end: "2001:g00::", + errMsg: "invalid end ip address 2001:g00::", + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + // discard parsed net.IP, we only need to make assertions on errMsg. + _, _, errMsg := parseIPRangeStartEnd(testCase.start, testCase.end) + assert.Equal(t, testCase.errMsg, errMsg) + }) + } +}