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)