@@ -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
823972func (t * handlerTestDriver ) testUninstall () {
0 commit comments