Skip to content

Commit 298c667

Browse files
committed
RouteAdvertisements: fail if DisableMP is unset
If MultiProtocol is enabled (default) then a BGP session carries prefixes of both IPv4 and IPv6 families. Our problem is that with an IPv4 session, FRR can incorrectly pick the masquerade IPv6 address (instead of the real address) as next hop for IPv6 prefixes and that won't work. Note that with a dedicated IPv6 session that can't happen since FRR will use the same address that was used to stablish the session. Let's enforce the use of DisableMP for now. Signed-off-by: Jaime Caamaño Ruiz <[email protected]>
1 parent 8b650b4 commit 298c667

File tree

2 files changed

+91
-13
lines changed

2 files changed

+91
-13
lines changed

go-controller/pkg/clustermanager/routeadvertisements/controller.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -506,13 +506,16 @@ func (c *Controller) generateFRRConfigurations(ra *ratypes.RouteAdvertisements)
506506
matchedNetworks := sets.New[string]()
507507
for _, frrConfig := range frrConfigs {
508508
// generate FRRConfiguration for each source FRRConfiguration/node combination
509-
new := c.generateFRRConfiguration(
509+
new, err := c.generateFRRConfiguration(
510510
ra,
511511
frrConfig,
512512
nodeName,
513513
selectedNetworks,
514514
matchedNetworks,
515515
)
516+
if err != nil {
517+
return nil, nil, err
518+
}
516519
if new == nil {
517520
// if we got nil, we didn't match any VRF
518521
return nil, nil, fmt.Errorf("%w: FRRConfiguration %q selected for node %q has no VRF matching the RouteAdvertisements target VRF or any selected network",
@@ -538,7 +541,7 @@ func (c *Controller) generateFRRConfiguration(
538541
nodeName string,
539542
selectedNetworks *selectedNetworks,
540543
matchedNetworks sets.Set[string],
541-
) *frrtypes.FRRConfiguration {
544+
) (*frrtypes.FRRConfiguration, error) {
542545
routers := []frrtypes.Router{}
543546
advertisements := sets.New(ra.Spec.Advertisements...)
544547

@@ -597,16 +600,30 @@ func (c *Controller) generateFRRConfiguration(
597600
targetRouter.Prefixes = advertisePrefixes
598601
targetRouter.Neighbors = make([]frrtypes.Neighbor, 0, len(source.Spec.BGP.Routers[i].Neighbors))
599602
for _, neighbor := range source.Spec.BGP.Routers[i].Neighbors {
600-
advertisePrefixes := advertisePrefixes
601-
receivePrefixes := receivePrefixes
602-
if neighbor.DisableMP {
603-
isIPV6 := utilnet.IsIPv6String(neighbor.Address)
604-
advertisePrefixes = util.MatchAllIPNetsStringFamily(isIPV6, advertisePrefixes)
605-
receivePrefixes = util.MatchAllIPNetsStringFamily(isIPV6, receivePrefixes)
603+
// If MultiProtocol is enabled (default) then a BGP session carries
604+
// prefixes of both IPv4 and IPv6 families. Our problem is that with
605+
// an IPv4 session, FRR can incorrectly pick the masquerade IPv6
606+
// address (instead of the real address) as next hop for IPv6
607+
// prefixes and that won't work. Note that with a dedicated IPv6
608+
// session that can't happen since FRR will use the same address
609+
// that was used to stablish the session. Let's enforce the use of
610+
// DisableMP for now.
611+
if !neighbor.DisableMP {
612+
return nil, fmt.Errorf("%w: DisableMP==false not supported, seen on FRRConfiguration %s/%s neighbor %s",
613+
errConfig,
614+
source.Namespace,
615+
source.Name,
616+
neighbor.Address,
617+
)
606618
}
619+
620+
isIPV6 := utilnet.IsIPv6String(neighbor.Address)
621+
advertisePrefixes := util.MatchAllIPNetsStringFamily(isIPV6, advertisePrefixes)
622+
receivePrefixes := util.MatchAllIPNetsStringFamily(isIPV6, receivePrefixes)
607623
if len(advertisePrefixes) == 0 {
608624
continue
609625
}
626+
610627
neighbor.ToAdvertise = frrtypes.Advertise{
611628
Allowed: frrtypes.AllowedOutPrefixes{
612629
Mode: frrtypes.AllowRestricted,
@@ -686,7 +703,7 @@ func (c *Controller) generateFRRConfiguration(
686703
}
687704
if len(routers) == 0 {
688705
// we ended up with no routers, bail out
689-
return nil
706+
return nil, nil
690707
}
691708

692709
new := &frrtypes.FRRConfiguration{}
@@ -712,7 +729,7 @@ func (c *Controller) generateFRRConfiguration(
712729
},
713730
}
714731

715-
return new
732+
return new, nil
716733
}
717734

718735
// updateFRRConfigurations updates the FRRConfigurations that apply for a

go-controller/pkg/clustermanager/routeadvertisements/controller_test.go

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
ctesting "k8s.io/client-go/testing"
2323
"k8s.io/client-go/tools/cache"
2424
"k8s.io/client-go/util/workqueue"
25+
"k8s.io/utils/ptr"
2526

2627
"github.com/ovn-org/ovn-kubernetes/go-controller/pkg/config"
2728
controllerutil "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/controller"
@@ -102,14 +103,16 @@ func (tn testNode) Node() *corev1.Node {
102103
type testNeighbor struct {
103104
ASN uint32
104105
Address string
106+
DisableMP *bool
105107
Receive []string
106108
Advertise []string
107109
}
108110

109111
func (tn testNeighbor) Neighbor() frrapi.Neighbor {
110112
n := frrapi.Neighbor{
111-
ASN: tn.ASN,
112-
Address: tn.Address,
113+
ASN: tn.ASN,
114+
Address: tn.Address,
115+
DisableMP: true,
113116
ToReceive: frrapi.Receive{
114117
Allowed: frrapi.AllowedInPrefixes{
115118
Mode: frrapi.AllowRestricted,
@@ -122,6 +125,9 @@ func (tn testNeighbor) Neighbor() frrapi.Neighbor {
122125
},
123126
},
124127
}
128+
if tn.DisableMP != nil {
129+
n.DisableMP = *tn.DisableMP
130+
}
125131
for _, receive := range tn.Receive {
126132
sep := strings.LastIndex(receive, "/")
127133
if sep == -1 {
@@ -365,6 +371,39 @@ func TestController_reconcile(t *testing.T) {
365371
},
366372
expectNADAnnotations: map[string]map[string]string{"default": {types.OvnRouteAdvertisementsKey: "[\"ra\"]"}},
367373
},
374+
{
375+
name: "reconciles dual-stack pod+eip RouteAdvertisement for a single FRR config, node and default network and target VRF",
376+
ra: &testRA{Name: "ra", AdvertisePods: true, AdvertiseEgressIPs: true},
377+
frrConfigs: []*testFRRConfig{
378+
{
379+
Name: "frrConfig",
380+
Namespace: frrNamespace,
381+
Routers: []*testRouter{
382+
{ASN: 1, Prefixes: []string{"1.1.1.0/24"}, Neighbors: []*testNeighbor{
383+
{ASN: 1, Address: "1.0.0.100"},
384+
{ASN: 1, Address: "fd02::ffff:100:64"},
385+
}},
386+
},
387+
},
388+
},
389+
nodes: []*testNode{{Name: "node", SubnetsAnnotation: "{\"default\":[\"1.1.0.0/24\",\"fd01::/64\"]}"}},
390+
eips: []*testEIP{{Name: "eipv4", EIPs: map[string]string{"node": "1.0.1.1"}}, {Name: "eipv6", EIPs: map[string]string{"node": "fd03::ffff:100:101"}}},
391+
reconcile: "ra",
392+
expectAcceptedStatus: metav1.ConditionTrue,
393+
expectFRRConfigs: []*testFRRConfig{
394+
{
395+
Labels: map[string]string{types.OvnRouteAdvertisementsKey: "ra"},
396+
Annotations: map[string]string{types.OvnRouteAdvertisementsKey: "ra/frrConfig/node"},
397+
NodeSelector: map[string]string{"kubernetes.io/hostname": "node"},
398+
Routers: []*testRouter{
399+
{ASN: 1, Prefixes: []string{"1.0.1.1/32", "1.1.0.0/24", "fd01::/64", "fd03::ffff:100:101/128"}, Neighbors: []*testNeighbor{
400+
{ASN: 1, Address: "1.0.0.100", Advertise: []string{"1.0.1.1/32", "1.1.0.0/24"}, Receive: []string{"1.1.0.0/16/24"}},
401+
{ASN: 1, Address: "fd02::ffff:100:64", Advertise: []string{"fd01::/64", "fd03::ffff:100:101/128"}, Receive: []string{"fd01::/48/64"}},
402+
}},
403+
}},
404+
},
405+
expectNADAnnotations: map[string]map[string]string{"default": {types.OvnRouteAdvertisementsKey: "[\"ra\"]"}},
406+
},
368407
{
369408
name: "reconciles pod RouteAdvertisement for a single FRR config, node, non default networks and default target VRF",
370409
ra: &testRA{Name: "ra", AdvertisePods: true, NetworkSelector: map[string]string{"selected": "true"}},
@@ -785,6 +824,24 @@ func TestController_reconcile(t *testing.T) {
785824
reconcile: "ra",
786825
expectAcceptedStatus: metav1.ConditionFalse,
787826
},
827+
{
828+
name: "fails to reconcile if DisableMP is unset",
829+
ra: &testRA{Name: "ra", AdvertisePods: true},
830+
frrConfigs: []*testFRRConfig{
831+
{
832+
Name: "frrConfig",
833+
Namespace: frrNamespace,
834+
Routers: []*testRouter{
835+
{ASN: 1, Prefixes: []string{"1.1.1.0/24"}, Neighbors: []*testNeighbor{
836+
{ASN: 1, Address: "1.0.0.100", DisableMP: ptr.To(false)},
837+
}},
838+
},
839+
},
840+
},
841+
nodes: []*testNode{{Name: "node", SubnetsAnnotation: "{\"default\":\"1.1.0.0/24\"}"}},
842+
reconcile: "ra",
843+
expectAcceptedStatus: metav1.ConditionFalse,
844+
},
788845
}
789846
for _, tt := range tests {
790847
t.Run(tt.name, func(t *testing.T) {
@@ -798,6 +855,10 @@ func TestController_reconcile(t *testing.T) {
798855
CIDR: ovntest.MustParseIPNet("1.1.0.0/16"),
799856
HostSubnetLength: 24,
800857
},
858+
{
859+
CIDR: ovntest.MustParseIPNet("fd01::/48"),
860+
HostSubnetLength: 64,
861+
},
801862
}
802863
config.OVNKubernetesFeature.EnableMultiNetwork = true
803864
config.OVNKubernetesFeature.EnableRouteAdvertisements = true
@@ -885,7 +946,7 @@ func TestController_reconcile(t *testing.T) {
885946
g.Expect(err).ToNot(gomega.HaveOccurred())
886947
accepted := meta.FindStatusCondition(ra.Status.Conditions, "Accepted")
887948
g.Expect(accepted).NotTo(gomega.BeNil())
888-
g.Expect(accepted.Status).To(gomega.Equal(tt.expectAcceptedStatus))
949+
g.Expect(accepted.Status).To(gomega.Equal(tt.expectAcceptedStatus), accepted.Message)
889950
}
890951

891952
// verify FRRConfigurations have been created/updated/deleted as expected

0 commit comments

Comments
 (0)