diff --git a/api/core/v1alpha1/nve_types.go b/api/core/v1alpha1/nve_types.go index 42c6d4ab..ad68f170 100644 --- a/api/core/v1alpha1/nve_types.go +++ b/api/core/v1alpha1/nve_types.go @@ -72,13 +72,11 @@ const ( type MulticastGroups struct { // L2 is the multicast group for Layer 2 VNIs (BUM traffic in bridged VLANs). // +optional - // +kubebuilder:validation:Format=ipv4 - L2 string `json:"l2,omitempty"` + L2 *IPPrefix `json:"l2,omitempty"` // L3 is the multicast group for Layer 3 VNIs (BUM traffic in routed VRFs). // +optional - // +kubebuilder:validation:Format=ipv4 - L3 string `json:"l3,omitempty"` + L3 *IPPrefix `json:"l3,omitempty"` } // AnycastGateway defines distributed anycast gateway configuration. diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index 025027bd..e7f24171 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -1870,6 +1870,14 @@ func (in *MultiChassis) DeepCopy() *MultiChassis { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MulticastGroups) DeepCopyInto(out *MulticastGroups) { *out = *in + if in.L2 != nil { + in, out := &in.L2, &out.L2 + *out = (*in).DeepCopy() + } + if in.L3 != nil { + in, out := &in.L3, &out.L3 + *out = (*in).DeepCopy() + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MulticastGroups. @@ -2096,7 +2104,7 @@ func (in *NetworkVirtualizationEdgeSpec) DeepCopyInto(out *NetworkVirtualization if in.MulticastGroups != nil { in, out := &in.MulticastGroups, &out.MulticastGroups *out = new(MulticastGroups) - **out = **in + (*in).DeepCopyInto(*out) } if in.AnycastGateway != nil { in, out := &in.AnycastGateway, &out.AnycastGateway diff --git a/charts/network-operator/templates/crd/networkvirtualizationedges.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/networkvirtualizationedges.networking.metal.ironcore.dev.yaml index 7c6d6634..24758fc0 100644 --- a/charts/network-operator/templates/crd/networkvirtualizationedges.networking.metal.ironcore.dev.yaml +++ b/charts/network-operator/templates/crd/networkvirtualizationedges.networking.metal.ironcore.dev.yaml @@ -149,12 +149,12 @@ spec: l2: description: L2 is the multicast group for Layer 2 VNIs (BUM traffic in bridged VLANs). - format: ipv4 + format: cidr type: string l3: description: L3 is the multicast group for Layer 3 VNIs (BUM traffic in routed VRFs). - format: ipv4 + format: cidr type: string type: object providerConfigRef: diff --git a/config/crd/bases/networking.metal.ironcore.dev_networkvirtualizationedges.yaml b/config/crd/bases/networking.metal.ironcore.dev_networkvirtualizationedges.yaml index 8fa1d84e..3699e34f 100644 --- a/config/crd/bases/networking.metal.ironcore.dev_networkvirtualizationedges.yaml +++ b/config/crd/bases/networking.metal.ironcore.dev_networkvirtualizationedges.yaml @@ -146,12 +146,12 @@ spec: l2: description: L2 is the multicast group for Layer 2 VNIs (BUM traffic in bridged VLANs). - format: ipv4 + format: cidr type: string l3: description: L3 is the multicast group for Layer 3 VNIs (BUM traffic in routed VRFs). - format: ipv4 + format: cidr type: string type: object providerConfigRef: diff --git a/config/samples/v1alpha1_nve.yaml b/config/samples/v1alpha1_nve.yaml index dd2dc4c1..61a53aa6 100644 --- a/config/samples/v1alpha1_nve.yaml +++ b/config/samples/v1alpha1_nve.yaml @@ -21,6 +21,6 @@ spec: anycastSourceInterfaceRef: name: lo1 multicastGroups: - l2: 224.0.0.2 + l2: 239.1.1.0/24 anycastGateway: virtualMAC: 00:00:11:11:22:22 diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index 73685c9d..33b3ba14 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -1128,6 +1128,7 @@ _Validation:_ _Appears in:_ - [ACLEntry](#aclentry) - [InterfaceIPv4](#interfaceipv4) +- [MulticastGroups](#multicastgroups) - [PrefixEntry](#prefixentry) - [RendezvousPoint](#rendezvouspoint) @@ -1559,8 +1560,8 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `l2` _string_ | L2 is the multicast group for Layer 2 VNIs (BUM traffic in bridged VLANs). | | Format: ipv4
Optional: \{\}
| -| `l3` _string_ | L3 is the multicast group for Layer 3 VNIs (BUM traffic in routed VRFs). | | Format: ipv4
Optional: \{\}
| +| `l2` _[IPPrefix](#ipprefix)_ | L2 is the multicast group for Layer 2 VNIs (BUM traffic in bridged VLANs). | | Format: cidr
Type: string
Optional: \{\}
| +| `l3` _[IPPrefix](#ipprefix)_ | L3 is the multicast group for Layer 3 VNIs (BUM traffic in routed VRFs). | | Format: cidr
Type: string
Optional: \{\}
| #### NTP @@ -3186,7 +3187,8 @@ _Appears in:_ | --- | --- | --- | --- | | `destination` _string_ | Destination is the destination IP address of the vPC's domain peer keepalive interface.
This is the IP address the local switch will send keepalive messages to. | | Format: ipv4
Required: \{\}
| | `source` _string_ | Source is the source IP address for keepalive messages.
This is the local IP address used to send keepalive packets to the peer. | | Format: ipv4
Required: \{\}
| -| `vrfRef` _[LocalObjectReference](#localobjectreference)_ | VRFRef is an optional reference to a VRF resource, e.g., the management VRF.
If specified, the switch sends keepalive packets throughout this VRF.
If omitted, the management VRF is used. | | Optional: \{\}
| +| `vrfName` _string_ | The name of the vrf used to send keepalive packets to the peer.
Mutually exclusive with VrfRef. | | MaxLength: 63
MinLength: 1
Optional: \{\}
| +| `vrfRef` _[LocalObjectReference](#localobjectreference)_ | The reference to a VRF resource used to send keepalive packets to the peer.
Mutually exclusive with VrfName. | | Optional: \{\}
| #### ManagementAccessConfig diff --git a/internal/controller/core/nve_controller_test.go b/internal/controller/core/nve_controller_test.go index 6a708b5b..1436e14e 100644 --- a/internal/controller/core/nve_controller_test.go +++ b/internal/controller/core/nve_controller_test.go @@ -134,13 +134,14 @@ var _ = Describe("NVE Controller", func() { ensureInterfaces(deviceName, interfaceNames, v1alpha1.InterfaceTypeLoopback) By("Creating the custom resource for the Kind NVE") + l2Prefix := v1alpha1.MustParsePrefix("234.0.0.0/8") nve = ensureNVE(nveKey, v1alpha1.NetworkVirtualizationEdgeSpec{ DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, SuppressARP: true, HostReachability: "BGP", SourceInterfaceRef: v1alpha1.LocalObjectReference{Name: interfaceNames[0]}, AnycastSourceInterfaceRef: &v1alpha1.LocalObjectReference{Name: interfaceNames[1]}, - MulticastGroups: &v1alpha1.MulticastGroups{L2: "234.0.0.1"}, + MulticastGroups: &v1alpha1.MulticastGroups{L2: &l2Prefix}, AdminState: v1alpha1.AdminStateUp, }) }) @@ -190,7 +191,7 @@ var _ = Describe("NVE Controller", func() { g.Expect(testProvider.NVE.Spec.HostReachability).To(BeEquivalentTo("BGP"), "Provider NVE hostreachability should be BGP") g.Expect(testProvider.NVE.Spec.SourceInterfaceRef.Name).To(Equal("lo0"), "Provider NVE primary interface should be lo0") g.Expect(testProvider.NVE.Spec.MulticastGroups).ToNot(BeNil(), "Provider NVE multicast group should not be nil") - g.Expect(testProvider.NVE.Spec.MulticastGroups.L2).To(Equal("234.0.0.1"), "Provider NVE multicast group prefix should be seet") + g.Expect(testProvider.NVE.Spec.MulticastGroups.L2).To(HaveValue(Equal(v1alpha1.MustParsePrefix("234.0.0.0/8"))), "Provider NVE multicast group prefix should be set") }).Should(Succeed()) By("Verifying referenced interfaces exist and are loopbacks") @@ -250,6 +251,7 @@ var _ = Describe("NVE Controller", func() { By("Creating the custom resource for the Kind NVE") nve = &v1alpha1.NetworkVirtualizationEdge{} if err := k8sClient.Get(ctx, nveKey, nve); errors.IsNotFound(err) { + l2Prefix := v1alpha1.MustParsePrefix("234.0.0.0/8") nve = &v1alpha1.NetworkVirtualizationEdge{ ObjectMeta: metav1.ObjectMeta{ Name: nveName, @@ -261,7 +263,7 @@ var _ = Describe("NVE Controller", func() { HostReachability: "BGP", SourceInterfaceRef: v1alpha1.LocalObjectReference{Name: interfaceNames[0]}, MulticastGroups: &v1alpha1.MulticastGroups{ - L2: "234.0.0.1", + L2: &l2Prefix, }, AdminState: v1alpha1.AdminStateUp, }, @@ -477,6 +479,7 @@ var _ = Describe("NVE Controller", func() { By("Creating the custom resource for the Kind NetworkVirtualizationEdge") nve = &v1alpha1.NetworkVirtualizationEdge{} if err := k8sClient.Get(ctx, nveKey, nve); errors.IsNotFound(err) { + l2Prefix := v1alpha1.MustParsePrefix("234.0.0.0/8") nve = &v1alpha1.NetworkVirtualizationEdge{ ObjectMeta: metav1.ObjectMeta{ Name: nveName, @@ -489,7 +492,7 @@ var _ = Describe("NVE Controller", func() { SourceInterfaceRef: v1alpha1.LocalObjectReference{Name: interfaceNames[0]}, AnycastSourceInterfaceRef: &v1alpha1.LocalObjectReference{Name: interfaceNames[1]}, MulticastGroups: &v1alpha1.MulticastGroups{ - L2: "234.0.0.1", + L2: &l2Prefix, }, AdminState: v1alpha1.AdminStateUp, }, @@ -549,6 +552,7 @@ var _ = Describe("NVE Controller", func() { By("Creating the custom resource for the Kind NetworkVirtualizationEdge") nve = &v1alpha1.NetworkVirtualizationEdge{} if err := k8sClient.Get(ctx, nveKey, nve); errors.IsNotFound(err) { + l2Prefix := v1alpha1.MustParsePrefix("234.0.0.0/8") nve = &v1alpha1.NetworkVirtualizationEdge{ ObjectMeta: metav1.ObjectMeta{ Name: nveName, @@ -561,7 +565,7 @@ var _ = Describe("NVE Controller", func() { SourceInterfaceRef: v1alpha1.LocalObjectReference{Name: interfaceNames[0]}, AnycastSourceInterfaceRef: &v1alpha1.LocalObjectReference{Name: interfaceNames[1]}, MulticastGroups: &v1alpha1.MulticastGroups{ - L2: "234.0.0.1", + L2: &l2Prefix, }, AdminState: v1alpha1.AdminStateUp, }, diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index fb2c78bb..7a1d8d05 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -2532,11 +2532,11 @@ func (p *Provider) EnsureNVE(ctx context.Context, req *provider.NVERequest) erro if req.AnycastSourceInterface != nil { n.AnycastInterface = NewOption(req.AnycastSourceInterface.Spec.Name) } - if req.NVE.Spec.MulticastGroups != nil && req.NVE.Spec.MulticastGroups.L2 != "" { - n.McastGroupL2 = NewOption(req.NVE.Spec.MulticastGroups.L2) + if req.NVE.Spec.MulticastGroups != nil && req.NVE.Spec.MulticastGroups.L2 != nil { + n.McastGroupL2 = NewOption(req.NVE.Spec.MulticastGroups.L2.Addr().String()) } - if req.NVE.Spec.MulticastGroups != nil && req.NVE.Spec.MulticastGroups.L3 != "" { - n.McastGroupL3 = NewOption(req.NVE.Spec.MulticastGroups.L3) + if req.NVE.Spec.MulticastGroups != nil && req.NVE.Spec.MulticastGroups.L3 != nil { + n.McastGroupL3 = NewOption(req.NVE.Spec.MulticastGroups.L3.Addr().String()) } n.SuppressARP = req.NVE.Spec.SuppressARP diff --git a/internal/webhook/core/v1alpha1/nve_webhook.go b/internal/webhook/core/v1alpha1/nve_webhook.go index 4c3f788a..62bbc1cb 100644 --- a/internal/webhook/core/v1alpha1/nve_webhook.go +++ b/internal/webhook/core/v1alpha1/nve_webhook.go @@ -5,6 +5,7 @@ package v1alpha1 import ( "context" + "errors" "fmt" "net/netip" @@ -75,23 +76,30 @@ func (v *NetworkVirtualizationEdgeCustomValidator) validateNetworkVirtualization if nve.Spec.MulticastGroups == nil { return nil } - if nve.Spec.MulticastGroups.L2 != "" { - if ok, err := v.isMulticast(nve.Spec.MulticastGroups.L2); err != nil || !ok { - return fmt.Errorf("%q is not a multicast address", nve.Spec.MulticastGroups.L2) + if nve.Spec.MulticastGroups.L2 != nil { + if !v.validateMulticastAddr(nve.Spec.MulticastGroups.L2.Prefix) { + return errors.New("invalid L2 multicast group: must be a valid IPv4 multicast CIDR with no host bits set") } } - if nve.Spec.MulticastGroups.L3 != "" { - if ok, err := v.isMulticast(nve.Spec.MulticastGroups.L3); err != nil || !ok { - return fmt.Errorf("%q is not a multicast address", nve.Spec.MulticastGroups.L3) + if nve.Spec.MulticastGroups.L3 != nil { + if !v.validateMulticastAddr(nve.Spec.MulticastGroups.L3.Prefix) { + return errors.New("invalid L3 multicast group: must be a valid IPv4 multicast CIDR with no host bits set") } } return nil } -func (*NetworkVirtualizationEdgeCustomValidator) isMulticast(s string) (bool, error) { - addr, err := netip.ParseAddr(s) - if err != nil || !addr.IsValid() { - return false, fmt.Errorf("%q is not a valid IP addr: %w", s, err) +// validateMulticastAddr checks if the provided prefix is a valid multicast address with no host bits set. +func (*NetworkVirtualizationEdgeCustomValidator) validateMulticastAddr(pfx netip.Prefix) bool { + // Check it's a multicast address + if !pfx.Addr().Is4() || !pfx.Addr().IsMulticast() { + return false } - return addr.IsMulticast(), nil + + // Check no host bits are set (canonical form) + if pfx.Masked().Addr() != pfx.Addr() { + return false + } + + return true } diff --git a/internal/webhook/core/v1alpha1/nve_webhook_test.go b/internal/webhook/core/v1alpha1/nve_webhook_test.go index f5fd1c78..4d933b97 100644 --- a/internal/webhook/core/v1alpha1/nve_webhook_test.go +++ b/internal/webhook/core/v1alpha1/nve_webhook_test.go @@ -42,17 +42,46 @@ var _ = Describe("NetworkVirtualizationEdge Webhook", func() { Expect(err).ToNot(HaveOccurred()) }) - It("accepts valid IPv4 multicast address", func() { + It("accepts valid IPv4 multicast CIDR", func() { + l2Prefix := corev1alpha1.MustParsePrefix("239.1.0.0/16") obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{ - L2: "239.1.1.1", + L2: &l2Prefix, } _, err := validator.ValidateCreate(ctx, obj) Expect(err).ToNot(HaveOccurred()) }) - It("rejects non-multicast IPv4 address", func() { + It("accepts valid IPv4 multicast CIDR for L3", func() { + l3Prefix := corev1alpha1.MustParsePrefix("239.2.0.0/16") obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{ - L3: "10.0.0.1", + L3: &l3Prefix, + } + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).ToNot(HaveOccurred()) + }) + + It("accepts /32 CIDR for single multicast IP", func() { + l2Prefix := corev1alpha1.MustParsePrefix("239.1.1.1/32") + obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{ + L2: &l2Prefix, + } + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).ToNot(HaveOccurred()) + }) + + It("rejects non-multicast IPv4 CIDR", func() { + l3Prefix := corev1alpha1.MustParsePrefix("10.0.0.0/8") + obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{ + L3: &l3Prefix, + } + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + }) + + It("rejects multicast CIDR with host bits set", func() { + l2Prefix := corev1alpha1.MustParsePrefix("239.1.1.1/16") + obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{ + L2: &l2Prefix, } _, err := validator.ValidateCreate(ctx, obj) Expect(err).To(HaveOccurred()) @@ -62,8 +91,9 @@ var _ = Describe("NetworkVirtualizationEdge Webhook", func() { Context("Validate Update MulticastGroup IPv4 prefix", func() { It("allows unchanged valid multicastGroup", func() { oldObj := obj.DeepCopy() + l2Prefix := corev1alpha1.MustParsePrefix("239.10.0.0/16") oldObj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{ - L2: "239.10.10.1", + L2: &l2Prefix, } newObj := oldObj.DeepCopy() _, err := validator.ValidateUpdate(ctx, oldObj, newObj)