Skip to content

Commit c132ec7

Browse files
committed
Change NVE multicast groups to accept CIDR format
Accept multicast network prefixes (e.g., 239.1.0.0/16) instead of host addresses. The webhook validates no host bits are set, and the provider extracts the base IP before sending to the device.
1 parent 228c305 commit c132ec7

9 files changed

Lines changed: 86 additions & 36 deletions

File tree

api/core/v1alpha1/nve_types.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,11 @@ const (
7272
type MulticastGroups struct {
7373
// L2 is the multicast group for Layer 2 VNIs (BUM traffic in bridged VLANs).
7474
// +optional
75-
// +kubebuilder:validation:Format=ipv4
76-
L2 string `json:"l2,omitempty"`
75+
L2 *IPPrefix `json:"l2,omitempty"`
7776

7877
// L3 is the multicast group for Layer 3 VNIs (BUM traffic in routed VRFs).
7978
// +optional
80-
// +kubebuilder:validation:Format=ipv4
81-
L3 string `json:"l3,omitempty"`
79+
L3 *IPPrefix `json:"l3,omitempty"`
8280
}
8381

8482
// AnycastGateway defines distributed anycast gateway configuration.

api/core/v1alpha1/zz_generated.deepcopy.go

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/networking.metal.ironcore.dev_networkvirtualizationedges.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,12 @@ spec:
146146
l2:
147147
description: L2 is the multicast group for Layer 2 VNIs (BUM traffic
148148
in bridged VLANs).
149-
format: ipv4
149+
format: cidr
150150
type: string
151151
l3:
152152
description: L3 is the multicast group for Layer 3 VNIs (BUM traffic
153153
in routed VRFs).
154-
format: ipv4
154+
format: cidr
155155
type: string
156156
type: object
157157
providerConfigRef:

config/samples/v1alpha1_nve.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ spec:
2121
anycastSourceInterfaceRef:
2222
name: lo1
2323
multicastGroups:
24-
l2: 224.0.0.2
24+
l2: 239.1.1.0/24
2525
anycastGateway:
2626
virtualMAC: 00:00:11:11:22:22

docs/api-reference/index.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,7 @@ _Validation:_
11281128
_Appears in:_
11291129
- [ACLEntry](#aclentry)
11301130
- [InterfaceIPv4](#interfaceipv4)
1131+
- [MulticastGroups](#multicastgroups)
11311132
- [PrefixEntry](#prefixentry)
11321133
- [RendezvousPoint](#rendezvouspoint)
11331134

@@ -1559,8 +1560,8 @@ _Appears in:_
15591560

15601561
| Field | Description | Default | Validation |
15611562
| --- | --- | --- | --- |
1562-
| `l2` _string_ | L2 is the multicast group for Layer 2 VNIs (BUM traffic in bridged VLANs). | | Format: ipv4 <br />Optional: \{\} <br /> |
1563-
| `l3` _string_ | L3 is the multicast group for Layer 3 VNIs (BUM traffic in routed VRFs). | | Format: ipv4 <br />Optional: \{\} <br /> |
1563+
| `l2` _[IPPrefix](#ipprefix)_ | L2 is the multicast group for Layer 2 VNIs (BUM traffic in bridged VLANs). | | Format: cidr <br />Type: string <br />Optional: \{\} <br /> |
1564+
| `l3` _[IPPrefix](#ipprefix)_ | L3 is the multicast group for Layer 3 VNIs (BUM traffic in routed VRFs). | | Format: cidr <br />Type: string <br />Optional: \{\} <br /> |
15641565

15651566

15661567
#### NTP
@@ -3186,7 +3187,8 @@ _Appears in:_
31863187
| --- | --- | --- | --- |
31873188
| `destination` _string_ | Destination is the destination IP address of the vPC's domain peer keepalive interface.<br />This is the IP address the local switch will send keepalive messages to. | | Format: ipv4 <br />Required: \{\} <br /> |
31883189
| `source` _string_ | Source is the source IP address for keepalive messages.<br />This is the local IP address used to send keepalive packets to the peer. | | Format: ipv4 <br />Required: \{\} <br /> |
3189-
| `vrfRef` _[LocalObjectReference](#localobjectreference)_ | VRFRef is an optional reference to a VRF resource, e.g., the management VRF.<br />If specified, the switch sends keepalive packets throughout this VRF.<br />If omitted, the management VRF is used. | | Optional: \{\} <br /> |
3190+
| `vrfName` _string_ | The name of the vrf used to send keepalive packets to the peer.<br />Mutually exclusive with VrfRef. | | MaxLength: 63 <br />MinLength: 1 <br />Optional: \{\} <br /> |
3191+
| `vrfRef` _[LocalObjectReference](#localobjectreference)_ | The reference to a VRF resource used to send keepalive packets to the peer.<br />Mutually exclusive with VrfName. | | Optional: \{\} <br /> |
31903192

31913193

31923194
#### ManagementAccessConfig

internal/controller/core/nve_controller_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,14 @@ var _ = Describe("NVE Controller", func() {
134134
ensureInterfaces(deviceName, interfaceNames, v1alpha1.InterfaceTypeLoopback)
135135

136136
By("Creating the custom resource for the Kind NVE")
137+
l2Prefix := v1alpha1.MustParsePrefix("234.0.0.0/8")
137138
nve = ensureNVE(nveKey, v1alpha1.NetworkVirtualizationEdgeSpec{
138139
DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName},
139140
SuppressARP: true,
140141
HostReachability: "BGP",
141142
SourceInterfaceRef: v1alpha1.LocalObjectReference{Name: interfaceNames[0]},
142143
AnycastSourceInterfaceRef: &v1alpha1.LocalObjectReference{Name: interfaceNames[1]},
143-
MulticastGroups: &v1alpha1.MulticastGroups{L2: "234.0.0.1"},
144+
MulticastGroups: &v1alpha1.MulticastGroups{L2: &l2Prefix},
144145
AdminState: v1alpha1.AdminStateUp,
145146
})
146147
})
@@ -190,7 +191,7 @@ var _ = Describe("NVE Controller", func() {
190191
g.Expect(testProvider.NVE.Spec.HostReachability).To(BeEquivalentTo("BGP"), "Provider NVE hostreachability should be BGP")
191192
g.Expect(testProvider.NVE.Spec.SourceInterfaceRef.Name).To(Equal("lo0"), "Provider NVE primary interface should be lo0")
192193
g.Expect(testProvider.NVE.Spec.MulticastGroups).ToNot(BeNil(), "Provider NVE multicast group should not be nil")
193-
g.Expect(testProvider.NVE.Spec.MulticastGroups.L2).To(Equal("234.0.0.1"), "Provider NVE multicast group prefix should be seet")
194+
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")
194195
}).Should(Succeed())
195196

196197
By("Verifying referenced interfaces exist and are loopbacks")
@@ -250,6 +251,7 @@ var _ = Describe("NVE Controller", func() {
250251
By("Creating the custom resource for the Kind NVE")
251252
nve = &v1alpha1.NetworkVirtualizationEdge{}
252253
if err := k8sClient.Get(ctx, nveKey, nve); errors.IsNotFound(err) {
254+
l2Prefix := v1alpha1.MustParsePrefix("234.0.0.0/8")
253255
nve = &v1alpha1.NetworkVirtualizationEdge{
254256
ObjectMeta: metav1.ObjectMeta{
255257
Name: nveName,
@@ -261,7 +263,7 @@ var _ = Describe("NVE Controller", func() {
261263
HostReachability: "BGP",
262264
SourceInterfaceRef: v1alpha1.LocalObjectReference{Name: interfaceNames[0]},
263265
MulticastGroups: &v1alpha1.MulticastGroups{
264-
L2: "234.0.0.1",
266+
L2: &l2Prefix,
265267
},
266268
AdminState: v1alpha1.AdminStateUp,
267269
},
@@ -477,6 +479,7 @@ var _ = Describe("NVE Controller", func() {
477479
By("Creating the custom resource for the Kind NetworkVirtualizationEdge")
478480
nve = &v1alpha1.NetworkVirtualizationEdge{}
479481
if err := k8sClient.Get(ctx, nveKey, nve); errors.IsNotFound(err) {
482+
l2Prefix := v1alpha1.MustParsePrefix("234.0.0.0/8")
480483
nve = &v1alpha1.NetworkVirtualizationEdge{
481484
ObjectMeta: metav1.ObjectMeta{
482485
Name: nveName,
@@ -489,7 +492,7 @@ var _ = Describe("NVE Controller", func() {
489492
SourceInterfaceRef: v1alpha1.LocalObjectReference{Name: interfaceNames[0]},
490493
AnycastSourceInterfaceRef: &v1alpha1.LocalObjectReference{Name: interfaceNames[1]},
491494
MulticastGroups: &v1alpha1.MulticastGroups{
492-
L2: "234.0.0.1",
495+
L2: &l2Prefix,
493496
},
494497
AdminState: v1alpha1.AdminStateUp,
495498
},
@@ -549,6 +552,7 @@ var _ = Describe("NVE Controller", func() {
549552
By("Creating the custom resource for the Kind NetworkVirtualizationEdge")
550553
nve = &v1alpha1.NetworkVirtualizationEdge{}
551554
if err := k8sClient.Get(ctx, nveKey, nve); errors.IsNotFound(err) {
555+
l2Prefix := v1alpha1.MustParsePrefix("234.0.0.0/8")
552556
nve = &v1alpha1.NetworkVirtualizationEdge{
553557
ObjectMeta: metav1.ObjectMeta{
554558
Name: nveName,
@@ -561,7 +565,7 @@ var _ = Describe("NVE Controller", func() {
561565
SourceInterfaceRef: v1alpha1.LocalObjectReference{Name: interfaceNames[0]},
562566
AnycastSourceInterfaceRef: &v1alpha1.LocalObjectReference{Name: interfaceNames[1]},
563567
MulticastGroups: &v1alpha1.MulticastGroups{
564-
L2: "234.0.0.1",
568+
L2: &l2Prefix,
565569
},
566570
AdminState: v1alpha1.AdminStateUp,
567571
},

internal/provider/cisco/nxos/provider.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2532,11 +2532,11 @@ func (p *Provider) EnsureNVE(ctx context.Context, req *provider.NVERequest) erro
25322532
if req.AnycastSourceInterface != nil {
25332533
n.AnycastInterface = NewOption(req.AnycastSourceInterface.Spec.Name)
25342534
}
2535-
if req.NVE.Spec.MulticastGroups != nil && req.NVE.Spec.MulticastGroups.L2 != "" {
2536-
n.McastGroupL2 = NewOption(req.NVE.Spec.MulticastGroups.L2)
2535+
if req.NVE.Spec.MulticastGroups != nil && req.NVE.Spec.MulticastGroups.L2 != nil {
2536+
n.McastGroupL2 = NewOption(req.NVE.Spec.MulticastGroups.L2.Addr().String())
25372537
}
2538-
if req.NVE.Spec.MulticastGroups != nil && req.NVE.Spec.MulticastGroups.L3 != "" {
2539-
n.McastGroupL3 = NewOption(req.NVE.Spec.MulticastGroups.L3)
2538+
if req.NVE.Spec.MulticastGroups != nil && req.NVE.Spec.MulticastGroups.L3 != nil {
2539+
n.McastGroupL3 = NewOption(req.NVE.Spec.MulticastGroups.L3.Addr().String())
25402540
}
25412541

25422542
n.SuppressARP = req.NVE.Spec.SuppressARP

internal/webhook/core/v1alpha1/nve_webhook.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package v1alpha1
55

66
import (
77
"context"
8+
"errors"
89
"fmt"
910
"net/netip"
1011

@@ -75,23 +76,30 @@ func (v *NetworkVirtualizationEdgeCustomValidator) validateNetworkVirtualization
7576
if nve.Spec.MulticastGroups == nil {
7677
return nil
7778
}
78-
if nve.Spec.MulticastGroups.L2 != "" {
79-
if ok, err := v.isMulticast(nve.Spec.MulticastGroups.L2); err != nil || !ok {
80-
return fmt.Errorf("%q is not a multicast address", nve.Spec.MulticastGroups.L2)
79+
if nve.Spec.MulticastGroups.L2 != nil {
80+
if !v.validateMulticastAddr(nve.Spec.MulticastGroups.L2.Prefix) {
81+
return errors.New("invalid L2 multicast group: must be a valid IPv4 multicast CIDR with no host bits set")
8182
}
8283
}
83-
if nve.Spec.MulticastGroups.L3 != "" {
84-
if ok, err := v.isMulticast(nve.Spec.MulticastGroups.L3); err != nil || !ok {
85-
return fmt.Errorf("%q is not a multicast address", nve.Spec.MulticastGroups.L3)
84+
if nve.Spec.MulticastGroups.L3 != nil {
85+
if !v.validateMulticastAddr(nve.Spec.MulticastGroups.L3.Prefix) {
86+
return errors.New("invalid L3 multicast group: must be a valid IPv4 multicast CIDR with no host bits set")
8687
}
8788
}
8889
return nil
8990
}
9091

91-
func (*NetworkVirtualizationEdgeCustomValidator) isMulticast(s string) (bool, error) {
92-
addr, err := netip.ParseAddr(s)
93-
if err != nil || !addr.IsValid() {
94-
return false, fmt.Errorf("%q is not a valid IP addr: %w", s, err)
92+
// validateMulticastAddr checks if the provided prefix is a valid multicast address with no host bits set.
93+
func (*NetworkVirtualizationEdgeCustomValidator) validateMulticastAddr(pfx netip.Prefix) bool {
94+
// Check it's a multicast address
95+
if !pfx.Addr().Is4() || !pfx.Addr().IsMulticast() {
96+
return false
9597
}
96-
return addr.IsMulticast(), nil
98+
99+
// Check no host bits are set (canonical form)
100+
if pfx.Masked().Addr() != pfx.Addr() {
101+
return false
102+
}
103+
104+
return true
97105
}

internal/webhook/core/v1alpha1/nve_webhook_test.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,46 @@ var _ = Describe("NetworkVirtualizationEdge Webhook", func() {
4242
Expect(err).ToNot(HaveOccurred())
4343
})
4444

45-
It("accepts valid IPv4 multicast address", func() {
45+
It("accepts valid IPv4 multicast CIDR", func() {
46+
l2Prefix := corev1alpha1.MustParsePrefix("239.1.0.0/16")
4647
obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{
47-
L2: "239.1.1.1",
48+
L2: &l2Prefix,
4849
}
4950
_, err := validator.ValidateCreate(ctx, obj)
5051
Expect(err).ToNot(HaveOccurred())
5152
})
5253

53-
It("rejects non-multicast IPv4 address", func() {
54+
It("accepts valid IPv4 multicast CIDR for L3", func() {
55+
l3Prefix := corev1alpha1.MustParsePrefix("239.2.0.0/16")
5456
obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{
55-
L3: "10.0.0.1",
57+
L3: &l3Prefix,
58+
}
59+
_, err := validator.ValidateCreate(ctx, obj)
60+
Expect(err).ToNot(HaveOccurred())
61+
})
62+
63+
It("accepts /32 CIDR for single multicast IP", func() {
64+
l2Prefix := corev1alpha1.MustParsePrefix("239.1.1.1/32")
65+
obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{
66+
L2: &l2Prefix,
67+
}
68+
_, err := validator.ValidateCreate(ctx, obj)
69+
Expect(err).ToNot(HaveOccurred())
70+
})
71+
72+
It("rejects non-multicast IPv4 CIDR", func() {
73+
l3Prefix := corev1alpha1.MustParsePrefix("10.0.0.0/8")
74+
obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{
75+
L3: &l3Prefix,
76+
}
77+
_, err := validator.ValidateCreate(ctx, obj)
78+
Expect(err).To(HaveOccurred())
79+
})
80+
81+
It("rejects multicast CIDR with host bits set", func() {
82+
l2Prefix := corev1alpha1.MustParsePrefix("239.1.1.1/16")
83+
obj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{
84+
L2: &l2Prefix,
5685
}
5786
_, err := validator.ValidateCreate(ctx, obj)
5887
Expect(err).To(HaveOccurred())
@@ -62,8 +91,9 @@ var _ = Describe("NetworkVirtualizationEdge Webhook", func() {
6291
Context("Validate Update MulticastGroup IPv4 prefix", func() {
6392
It("allows unchanged valid multicastGroup", func() {
6493
oldObj := obj.DeepCopy()
94+
l2Prefix := corev1alpha1.MustParsePrefix("239.10.0.0/16")
6595
oldObj.Spec.MulticastGroups = &corev1alpha1.MulticastGroups{
66-
L2: "239.10.10.1",
96+
L2: &l2Prefix,
6797
}
6898
newObj := oldObj.DeepCopy()
6999
_, err := validator.ValidateUpdate(ctx, oldObj, newObj)

0 commit comments

Comments
 (0)