Skip to content

Commit 4f5ccce

Browse files
committed
Fix get nexthop IP address for OVN mgmt interface
The getNextHopOnK8sMgmtIntf() function would return Dst.IP when no explicit gateway was set on a route. However, Dst.IP is the network address (the base of the CIDR), not a valid host IP. Fix: Added validation to skip routes where Dst.IP equals the network address (IP & Mask == IP), but allow host routes (/32 or /128) where Dst.IP is a valid host address. Tests: Added test cases to verify: 1. Network addresses are rejected when route order changes 2. Host routes (/32 or /128) with Gw=nil are accepted Addresses CodeRabbit review comment about preserving /32 and /128 routes. Fixes: #4121 Signed-off-by: Yossi Boaron <yboaron@redhat.com>
1 parent a5d2aba commit 4f5ccce

2 files changed

Lines changed: 163 additions & 0 deletions

File tree

pkg/routeagent_driver/handlers/ovn/handler_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,155 @@ func (t *handlerTestDriver) testOVNMgmtInterfaceAddressChange() {
818818
t.netLink.AwaitGwRoutes(0, constants.RouteAgentInterClusterNetworkTableID, t.OVNK8sMgmntIntGw)
819819
t.netLink.AwaitGwRoutes(0, constants.RouteAgentHostNetworkTableID, t.OVNK8sMgmntIntGw)
820820
})
821+
822+
It("should skip network address routes and use valid gateway when route order changes", func(ctx context.Context) {
823+
// This reproduces issue #4121 where after ROKS node reboot, route ordering changes
824+
// and Submariner picks the wrong route (network address instead of valid gateway)
825+
//
826+
// Scenario:
827+
// - Multiple routes exist for the same destination (cluster CIDR)
828+
// - One route has Gw=nil (would use Dst.IP = network address like 171.0.1.0)
829+
// - Another route has valid Gw=171.0.1.1
830+
// - After reboot, the bad route (Gw=nil) comes FIRST in iteration order
831+
// - Bug: Code returns first match (network address 171.0.1.0)
832+
// - Fix: Code skips network addresses and continues to find valid gateway
833+
link, err := t.netLink.LinkByName(ovn.OVNK8sMgmntIntfName)
834+
Expect(err).To(Succeed())
835+
836+
// Remove existing routes
837+
routes, err := t.netLink.RouteList(link, t.ipFamily)
838+
Expect(err).To(Succeed())
839+
840+
for i := range routes {
841+
if routes[i].Dst != nil && routes[i].Dst.String() == t.clusterCIDR {
842+
Expect(t.netLink.RouteDel(&routes[i])).To(Succeed())
843+
}
844+
}
845+
846+
// Simulate "after reboot" scenario: Add routes in the order that causes the bug
847+
// Route 1 (comes FIRST): Dst=cluster CIDR, Gw=nil (will use network address)
848+
networkAddr := toIPNet(t.clusterCIDR)
849+
Expect(t.netLink.RouteAdd(&netlink.Route{
850+
LinkIndex: OVNK8sMgmntIntIndex,
851+
Family: netlinkAPI.ToNetlinkFamily(t.ipFamily),
852+
Dst: networkAddr,
853+
Gw: nil, // No gateway - Dst.IP is network address (e.g., 171.0.1.0)
854+
})).To(Succeed())
855+
856+
// Route 2 (comes SECOND): Dst=cluster CIDR, Gw=valid gateway IP
857+
validGateway := net.ParseIP(t.OVNK8sMgmntIntGw)
858+
Expect(t.netLink.RouteAdd(&netlink.Route{
859+
LinkIndex: OVNK8sMgmntIntIndex,
860+
Family: netlinkAPI.ToNetlinkFamily(t.ipFamily),
861+
Dst: networkAddr,
862+
Gw: validGateway, // Valid gateway (e.g., 171.0.1.1)
863+
})).To(Succeed())
864+
865+
// Create a remote endpoint to trigger updateHostNetworkDataplane
866+
endpoint := t.createEndpoint(ctx, ipv4Subnets[0])
867+
868+
// With the bug: Would use the first route's network address (171.0.1.0)
869+
// With the fix: Should skip first route and use second route's valid gateway (171.0.1.1)
870+
t.netLink.AwaitGwRoutes(0, constants.RouteAgentHostNetworkTableID, t.OVNK8sMgmntIntGw)
871+
872+
// Verify the correct gateway was chosen
873+
routes150, err := t.netLink.RouteList(nil, t.ipFamily)
874+
Expect(err).To(Succeed())
875+
876+
foundValidRoute := false
877+
878+
for i := range routes150 {
879+
if routes150[i].Table == constants.RouteAgentHostNetworkTableID && routes150[i].Gw != nil {
880+
foundValidRoute = true
881+
882+
// Must use the valid gateway, not the network address
883+
Expect(routes150[i].Gw.String()).To(Equal(t.OVNK8sMgmntIntGw),
884+
"Should use valid gateway %s, not network address", t.OVNK8sMgmntIntGw)
885+
886+
// Double-check: Gateway should NOT be a network address (last octet != 0)
887+
gwBytes := routes150[i].Gw.To4()
888+
if gwBytes != nil {
889+
Expect(gwBytes[3]).NotTo(Equal(byte(0)),
890+
"Gateway %s is a network address (ends in .0)", routes150[i].Gw.String())
891+
}
892+
}
893+
}
894+
895+
Expect(foundValidRoute).To(BeTrue(), "Should have created route with valid gateway")
896+
897+
t.DeleteEndpoint(ctx, endpoint.Name)
898+
})
899+
900+
It("should accept host routes (/32 or /128) with Gw=nil", func(ctx context.Context) {
901+
// This tests that we don't reject valid host routes (/32 IPv4 or /128 IPv6)
902+
// when Gw=nil. A host route's Dst.IP is a valid host address even though
903+
// it equals Dst.IP.Mask(Dst.Mask) (because mask is all 1s).
904+
link, err := t.netLink.LinkByName(ovn.OVNK8sMgmntIntfName)
905+
Expect(err).To(Succeed())
906+
907+
// Remove existing routes
908+
routes, err := t.netLink.RouteList(link, t.ipFamily)
909+
Expect(err).To(Succeed())
910+
911+
for i := range routes {
912+
if routes[i].Dst != nil && routes[i].Dst.String() == t.clusterCIDR {
913+
Expect(t.netLink.RouteDel(&routes[i])).To(Succeed())
914+
}
915+
}
916+
917+
// Add a network address route (should be skipped)
918+
networkAddr := toIPNet(t.clusterCIDR)
919+
Expect(t.netLink.RouteAdd(&netlink.Route{
920+
LinkIndex: OVNK8sMgmntIntIndex,
921+
Family: netlinkAPI.ToNetlinkFamily(t.ipFamily),
922+
Dst: networkAddr,
923+
Gw: nil, // Network address - should be skipped
924+
})).To(Succeed())
925+
926+
// Add a /32 host route with Gw=nil (should be accepted)
927+
hostIP := net.ParseIP(t.OVNK8sMgmntIntGw)
928+
hostRoute := &net.IPNet{
929+
IP: hostIP,
930+
Mask: net.CIDRMask(32, 32), // /32 for IPv4 or /128 for IPv6
931+
}
932+
933+
if t.ipFamily == k8snet.IPv6 {
934+
hostRoute.Mask = net.CIDRMask(128, 128)
935+
}
936+
937+
Expect(t.netLink.RouteAdd(&netlink.Route{
938+
LinkIndex: OVNK8sMgmntIntIndex,
939+
Family: netlinkAPI.ToNetlinkFamily(t.ipFamily),
940+
Dst: hostRoute,
941+
Gw: nil, // No gateway - will use Dst.IP which is a valid host IP
942+
})).To(Succeed())
943+
944+
// Create a remote endpoint to trigger updateHostNetworkDataplane
945+
endpoint := t.createEndpoint(ctx, ipv4Subnets[0])
946+
947+
// Should use the host route's Dst.IP, not the network address
948+
t.netLink.AwaitGwRoutes(0, constants.RouteAgentHostNetworkTableID, t.OVNK8sMgmntIntGw)
949+
950+
// Verify the correct IP was chosen
951+
routes150, err := t.netLink.RouteList(nil, t.ipFamily)
952+
Expect(err).To(Succeed())
953+
954+
foundValidRoute := false
955+
956+
for i := range routes150 {
957+
if routes150[i].Table == constants.RouteAgentHostNetworkTableID && routes150[i].Gw != nil {
958+
foundValidRoute = true
959+
960+
// Must use the host route's IP
961+
Expect(routes150[i].Gw.String()).To(Equal(t.OVNK8sMgmntIntGw),
962+
"Should use host route IP %s", t.OVNK8sMgmntIntGw)
963+
}
964+
}
965+
966+
Expect(foundValidRoute).To(BeTrue(), "Should have created route with host route IP")
967+
968+
t.DeleteEndpoint(ctx, endpoint.Name)
969+
})
821970
}
822971

823972
func (t *handlerTestDriver) testUninstall() {

pkg/routeagent_driver/handlers/ovn/host_networking.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ func (ovn *Handler) getNextHopOnK8sMgmtIntf() (*net.IP, error) {
167167
logger.V(log.TRACE).Info("Processing route", "Dst", routes[i].Dst.String(), "Gw", routes[i].Gw.String())
168168

169169
for _, cidrNet := range parsedClusterCIDRs {
170+
//nolint:gocritic // Pre-existing nesting, not introduced by this fix
170171
if routes[i].Dst.String() == cidrNet.String() ||
171172
cidrNet.Contains(routes[i].Dst.IP) ||
172173
routes[i].Dst.Contains(cidrNet.IP) {
@@ -176,6 +177,19 @@ func (ovn *Handler) getNextHopOnK8sMgmtIntf() (*net.IP, error) {
176177
return &routes[i].Gw, nil
177178
}
178179

180+
// If no explicit gateway, validate that Dst.IP is a valid host address, not a network address.
181+
// A network address (e.g., 172.22.0.0/16) should not be used as a gateway.
182+
// Skip routes where Dst.IP is the network address, but allow host routes (/32 or /128).
183+
ones, bits := routes[i].Dst.Mask.Size()
184+
isHostRoute := ones == bits // /32 for IPv4 or /128 for IPv6
185+
186+
if !isHostRoute && routes[i].Dst.IP.Equal(routes[i].Dst.IP.Mask(routes[i].Dst.Mask)) {
187+
logger.V(log.DEBUG).Info("Skipping route with network address",
188+
"Dst", routes[i].Dst.String(), "IP", routes[i].Dst.IP.String())
189+
190+
continue
191+
}
192+
179193
localIP := routes[i].Dst.IP
180194

181195
return &localIP, nil

0 commit comments

Comments
 (0)