diff --git a/cmd/antrea-controller/controller.go b/cmd/antrea-controller/controller.go index bdd0b79ed5e..36390568d09 100644 --- a/cmd/antrea-controller/controller.go +++ b/cmd/antrea-controller/controller.go @@ -301,6 +301,7 @@ func run(o *Options) error { networkPolicyController, networkPolicyStatusController, egressController, + externalIPPoolController, statsAggregator, bundleCollectionController, traceflowController, @@ -494,6 +495,7 @@ func createAPIServerConfig(kubeconfig string, npController *networkpolicy.NetworkPolicyController, networkPolicyStatusController *networkpolicy.StatusController, egressController *egress.EgressController, + externalIPPoolController *externalippool.ExternalIPPoolController, statsAggregator *stats.Aggregator, bundleCollectionStore *supportbundlecollection.Controller, traceflowController *traceflow.Controller, @@ -563,6 +565,7 @@ func createAPIServerConfig(kubeconfig string, endpointQuerier, npController, egressController, + externalIPPoolController, bundleCollectionStore, traceflowController), nil } diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f93a69aef96..bd61d934957 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -161,6 +161,7 @@ func NewConfig( endpointQuerier controllernetworkpolicy.EndpointQuerier, npController *controllernetworkpolicy.NetworkPolicyController, egressController *egress.EgressController, + externalIPPoolController *externalippool.ExternalIPPoolController, bundleCollectionController *controllerbundlecollection.Controller, traceflowController *traceflow.Controller) *Config { return &Config{ @@ -181,6 +182,7 @@ func NewConfig( networkPolicyController: npController, networkPolicyStatusController: networkPolicyStatusController, egressController: egressController, + externalIPPoolController: externalIPPoolController, bundleCollectionController: bundleCollectionController, traceflowController: traceflowController, }, diff --git a/pkg/controller/externalippool/validate.go b/pkg/controller/externalippool/validate.go index 6a5719f2e11..0c340676377 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, 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, externalIPPools); !allowed { break } oldIPRangeSet := getIPRangeSet(oldObj.Spec.IPRanges) @@ -83,47 +90,139 @@ 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(externalIPPool crdv1beta1.ExternalIPPool, existingExternalIPPools []*crdv1beta1.ExternalIPPool) (string, bool) { + subnetInfo := externalIPPool.Spec.SubnetInfo + ipRanges := externalIPPool.Spec.IPRanges + + 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 _, pool := range existingExternalIPPools { + // exclude existing ip ranges of the pool which is being updated + if pool.Name == externalIPPool.Name { + continue + } + for _, ipRange := range pool.Spec.IPRanges { + var key string + var start, end netip.Addr + + if ipRange.CIDR != "" { + key = fmt.Sprintf("range [%s] of pool %s", ipRange.CIDR, pool.Name) + cidr, _ := parseIPRangeCIDR(ipRange.CIDR) + start, end = utilip.GetStartAndEndOfPrefix(cidr) + + } else { + key = fmt.Sprintf("range [%s-%s] of pool %s", ipRange.Start, ipRange.End, pool.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..ffd3d64876b 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,304 @@ 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, + 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) + }) + } +} diff --git a/pkg/util/ip/ip.go b/pkg/util/ip/ip.go index 58563e2274c..7c5c7254041 100644 --- a/pkg/util/ip/ip.go +++ b/pkg/util/ip/ip.go @@ -19,6 +19,7 @@ import ( "encoding/binary" "fmt" "net" + "net/netip" "sort" utilnet "k8s.io/utils/net" @@ -277,3 +278,28 @@ func AppendPortIfMissing(addr, port string) string { return net.JoinHostPort(addr, port) } + +// GetStartAndEndOfPrefix retrieves the start and end addresses of a netip.Prefix. +// For example: 10.10.40.0/24 -> 10.10.40.0, 10.10.40.255 +func GetStartAndEndOfPrefix(prefix netip.Prefix) (netip.Addr, netip.Addr) { + var start, end netip.Addr + var mask net.IPMask + + if prefix.Addr().Is4() { + mask = net.CIDRMask(prefix.Bits(), 32) + } else { + mask = net.CIDRMask(prefix.Bits(), 128) + } + + // use gateway address, of canonical form of prefix, as start address. + start = prefix.Masked().Addr() + + // calculate the end address by performing bitwise OR with the complement of the mask. + slice := start.AsSlice() + for i := 0; i < len(slice); i++ { + slice[i] |= ^mask[i] + } + + end, _ = netip.AddrFromSlice(slice) + return start, end +} diff --git a/pkg/util/ip/ip_test.go b/pkg/util/ip/ip_test.go index caf12b50f52..3c8d12aa65f 100644 --- a/pkg/util/ip/ip_test.go +++ b/pkg/util/ip/ip_test.go @@ -16,6 +16,7 @@ package ip import ( "net" + "net/netip" "testing" "github.com/stretchr/testify/assert" @@ -329,3 +330,85 @@ func TestIPNetContains(t *testing.T) { }) } } + +func TestGetStartAndEndOfPrefix(t *testing.T) { + testCases := []struct { + prefix string + start string + end string + }{ + { + prefix: "10.20.30.0/26", + start: "10.20.30.0", + end: "10.20.30.63", + }, + { + prefix: "10.10.40.0/24", + start: "10.10.40.0", + end: "10.10.40.255", + }, + { + prefix: "10.20.30.0/20", + start: "10.20.16.0", + end: "10.20.31.255", + }, + { + prefix: "10.30.20.0/16", + start: "10.30.0.0", + end: "10.30.255.255", + }, + { + prefix: "10.10.10.10/12", + start: "10.0.0.0", + end: "10.15.255.255", + }, + { + prefix: "10.10.10.10/6", + start: "8.0.0.0", + end: "11.255.255.255", + }, + { + prefix: "2001:0db8::/42", + start: "2001:db8::", + end: "2001:db8:3f:ffff:ffff:ffff:ffff:ffff", + }, + { + prefix: "2001:4860:4860::8888/56", + start: "2001:4860:4860::", + end: "2001:4860:4860:ff:ffff:ffff:ffff:ffff", + }, + { + prefix: "2001:0db8::/64", + start: "2001:db8::", + end: "2001:db8::ffff:ffff:ffff:ffff", + }, + { + prefix: "2001:0db8::/84", + start: "2001:db8::", + end: "2001:db8::fff:ffff:ffff", + }, + { + prefix: "fd00:10:96::/100", + start: "fd00:10:96::", + end: "fd00:10:96::fff:ffff", + }, + { + prefix: "fd00:10:96::/112", + start: "fd00:10:96::", + end: "fd00:10:96::ffff", + }, + { + prefix: "2001:4860:4860::8888/124", + start: "2001:4860:4860::8880", + end: "2001:4860:4860::888f", + }, + } + + for _, tc := range testCases { + t.Run(tc.prefix, func(t *testing.T) { + start, end := GetStartAndEndOfPrefix(netip.MustParsePrefix(tc.prefix)) + assert.Equal(t, 0, netip.MustParseAddr(tc.start).Compare(start)) + assert.Equal(t, 0, netip.MustParseAddr(tc.end).Compare(end)) + }) + } +}