Commit a9e8fb48d6 for frr

commit a9e8fb48d625d5ff1adb6bec48d3eda5c9c6673c
Author: Adriano Cordova <adrianox@gmail.com>
Date:   Sun Sep 6 19:58:39 2026 -0300

    tests: add VRF variant of zebra reconnect route replay

    Run ospfd, ripd, isisd and bgpd inside a tenant VRF and verify each
    daemon replays its VRF-instance routes after a zebra restart.

    Signed-off-by: Adriano Cordova <adrianox@gmail.com>

diff --git a/tests/topotests/zebra_reconnect_route_replay/test_zebra_reconnect_route_replay_vrf.py b/tests/topotests/zebra_reconnect_route_replay/test_zebra_reconnect_route_replay_vrf.py
new file mode 100644
index 0000000000..909d452244
--- /dev/null
+++ b/tests/topotests/zebra_reconnect_route_replay/test_zebra_reconnect_route_replay_vrf.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: ISC
+
+#
+# test_zebra_reconnect_route_replay_vrf.py
+#
+# Copyright (c) 2026 by
+# Adriano Cordova <adrianox@gmail.com>
+#
+# */
+
+#
+# """ VRF counterpart of test_zebra_reconnect_route_replay.py:
+#
+# when zebra restarts while a routing daemon keeps running, the daemon must
+# replay its already computed/selected routes to zebra on reconnect. This
+# variant exercises every replay-capable daemon inside a tenant VRF:
+#
+# vr1 originates one distinct prefix into each of OSPF, RIP, IS-IS and BGP
+# inside VRF "blue" and advertises them to vr2's VRF-blue instances over a
+# shared link:
+#
+#     [ vr1 ] --- vr1-eth0(blue) --- sw0 --- vr2-eth0(blue) --- [ vr2 ]
+#
+#     vr1 stubs (vrf blue):  OSPF 198.51.100.0/24, RIP 198.51.101.0/24,
+#                            IS-IS 198.51.102.0/24
+#     vr1 BGP network (vrf blue): 203.0.113.0/24
+#
+# After vr2 has installed all four, only zebra is restarted on vr2. The test
+# verifies every prefix is reinstalled in vr2's VRF RIB after zebra comes
+# back, which only happens if each daemon replays its routes for the VRF
+# instance on the zebra reconnect.
+# """
+
+import os
+import sys
+import pytest
+from functools import partial
+
+CWD = os.path.dirname(os.path.realpath(__file__))
+sys.path.append(os.path.join(CWD, "../"))
+
+# pylint: disable=C0413
+from lib import topotest
+from lib.topogen import Topogen, get_topogen
+from lib.topolog import logger
+from lib.common_config import kill_router_daemons, start_router_daemons
+
+pytestmark = [
+    pytest.mark.bgpd,
+    pytest.mark.ospfd,
+    pytest.mark.ripd,
+    pytest.mark.isisd,
+]
+
+VRF = "blue"
+
+# prefix -> protocol that should install it on vr2 (in VRF blue)
+PREFIXES = {
+    "198.51.100.0/24": "ospf",
+    "198.51.101.0/24": "rip",
+    "198.51.102.0/24": "isis",
+    "203.0.113.0/24": "bgp",
+}
+
+
+def build_topo(tgen):
+    tgen.add_router("vr1")
+    tgen.add_router("vr2")
+
+    # Shared link carrying every protocol adjacency, both sides in VRF blue.
+    sw0 = tgen.add_switch("sw0")
+    sw0.add_link(tgen.gears["vr1"])
+    sw0.add_link(tgen.gears["vr2"])
+
+    # vr1 stub interfaces, one per IGP prefix (vr1-eth1/2/3), also in VRF blue.
+    for i in range(1, 4):
+        sw = tgen.add_switch("sw{}".format(i))
+        sw.add_link(tgen.gears["vr1"])
+
+
+VRF_L3MDEV_SYSCTL = "/proc/sys/net/ipv4/udp_l3mdev_accept"
+_udp_l3mdev_original = None
+
+
+def setup_module(module):
+    global _udp_l3mdev_original
+
+    tgen = Topogen(build_topo, module.__name__)
+    tgen.start_topology()
+
+    logger.info("Testing with Linux VRF support")
+    try:
+        with open(VRF_L3MDEV_SYSCTL, "r") as f:
+            _udp_l3mdev_original = f.read().strip()
+        with open(VRF_L3MDEV_SYSCTL, "w") as f:
+            f.write("0")
+    except OSError as e:
+        return pytest.skip("Skipping VRF test. Linux VRF not available on System: {}".format(e))
+
+    # Create the tenant VRF in each router's netns and move the peer interface
+    # and vr1's stub interfaces into it, so each daemon runs per-VRF.
+    for rname, router in tgen.routers().items():
+        router.net.add_l3vrf(VRF, 10)
+        router.net.attach_iface_to_l3vrf(rname + "-eth0", VRF)
+        if rname == "vr1":
+            for i in range(1, 4):
+                router.net.attach_iface_to_l3vrf(rname + "-eth{}".format(i), VRF)
+
+    for rname, router in tgen.routers().items():
+        router.load_frr_config(os.path.join(CWD, "{}/frr.conf".format(rname)))
+
+    tgen.start_router()
+
+
+def teardown_module(_mod):
+    if _udp_l3mdev_original is not None:
+        try:
+            with open(VRF_L3MDEV_SYSCTL, "w") as f:
+                f.write(_udp_l3mdev_original)
+        except OSError:
+            pass
+
+    tgen = get_topogen()
+    tgen.stop_topology()
+
+
+def _route_installed(router, prefix, proto):
+    """Return None when prefix is installed on router in VRF blue via proto with
+    a selected, active nexthop."""
+    output = router.vtysh_cmd(
+        "show ip route vrf {} {} json".format(VRF, prefix), isjson=True
+    )
+    if not output or prefix not in output:
+        return "{} missing from {} RIB".format(prefix, router.name)
+    for entry in output[prefix]:
+        if entry.get("protocol") != proto:
+            continue
+        if not entry.get("selected"):
+            continue
+        for nh in entry.get("nexthops", []):
+            if nh.get("ip") and nh.get("active"):
+                return None
+    return "{} on {} not installed via {}".format(prefix, router.name, proto)
+
+
+def _all_routes_installed(router):
+    for prefix, proto in PREFIXES.items():
+        err = _route_installed(router, prefix, proto)
+        if err:
+            return err
+    return None
+
+
+def test_converge():
+    "All four prefixes must be installed on vr2 in VRF blue via their protocols."
+    tgen = get_topogen()
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    vr2 = tgen.gears["vr2"]
+    _, result = topotest.run_and_expect(
+        partial(_all_routes_installed, vr2), None, count=120, wait=1
+    )
+    assert result is None, "vr2 did not install all prefixes initially: {}".format(result)
+    logger.info("vr2 initial routes:\n{}".format(vr2.vtysh_cmd("show ip route vrf {}".format(VRF))))
+
+
+def test_zebra_reconnect_replays_routes():
+    "After only zebra restarts on vr2, every protocol must replay its VRF routes."
+    tgen = get_topogen()
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    vr2 = tgen.gears["vr2"]
+
+    # Make sure we start from a fully converged state.
+    _, result = topotest.run_and_expect(
+        partial(_all_routes_installed, vr2), None, count=120, wait=1
+    )
+    assert result is None, "preconditions not met: {}".format(result)
+
+    logger.info("Restarting only zebra on vr2 (protocol daemons keep running)")
+    kill_router_daemons(tgen, "vr2", ["zebra"])
+    start_router_daemons(tgen, "vr2", ["zebra"])
+
+    # The protocol tables are stable, so each prefix is only reinstalled if the
+    # owning daemon replays its routes for the VRF instance on reconnect.
+    _, result = topotest.run_and_expect(
+        partial(_all_routes_installed, vr2), None, count=120, wait=1
+    )
+    assert result is None, "vr2 did not get routes replayed after zebra restart: {}".format(
+        result
+    )
+    logger.info("vr2 routes after zebra restart:\n{}".format(vr2.vtysh_cmd("show ip route vrf {}".format(VRF))))
+
+
+def test_shutdown_check_stderr():
+    if os.environ.get("TOPOTESTS_CHECK_STDERR") is None:
+        pytest.skip("Skipping test for Stderr output and memory leaks")
+
+    tgen = get_topogen()
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    for router in tgen.routers().values():
+        router.stop()
+        for daemon in ["zebra", "ospfd", "ripd", "isisd", "bgpd"]:
+            log = tgen.net[router.name].getStdErr(daemon)
+            if log:
+                logger.error("{} {} StdErr Log:\n{}".format(router.name, daemon, log))
+
+
+if __name__ == "__main__":
+    args = ["-s"] + sys.argv[1:]
+    sys.exit(pytest.main(args))
diff --git a/tests/topotests/zebra_reconnect_route_replay/vr1/frr.conf b/tests/topotests/zebra_reconnect_route_replay/vr1/frr.conf
new file mode 100644
index 0000000000..acba53e4d6
--- /dev/null
+++ b/tests/topotests/zebra_reconnect_route_replay/vr1/frr.conf
@@ -0,0 +1,48 @@
+hostname vr1
+service integrated-vtysh-config
+!
+interface vr1-eth0 vrf blue
+ ip address 192.0.2.1/24
+ ip router isis 1
+!
+interface vr1-eth1 vrf blue
+ ip address 198.51.100.1/24
+!
+interface vr1-eth2 vrf blue
+ ip address 198.51.101.1/24
+!
+interface vr1-eth3 vrf blue
+ ip address 198.51.102.1/24
+ ip router isis 1
+ isis passive
+!
+vrf blue
+ ip route 203.0.113.0/24 Null0
+!
+ip forwarding
+!
+router ospf vrf blue
+ ospf router-id 192.0.2.1
+ network 192.0.2.0/24 area 0
+ network 198.51.100.0/24 area 0
+ passive-interface vr1-eth1
+!
+router rip vrf blue
+ version 2
+ network 192.0.2.0/24
+ network 198.51.101.0/24
+ passive-interface vr1-eth2
+ timers basic 5 30 60
+!
+router isis 1 vrf blue
+ net 49.0001.0000.0000.0001.00
+ is-type level-2-only
+!
+router bgp 65001 vrf blue
+ no bgp ebgp-requires-policy
+ neighbor 192.0.2.2 remote-as 65002
+ address-family ipv4 unicast
+  network 203.0.113.0/24
+  neighbor 192.0.2.2 activate
+ exit-address-family
+!
diff --git a/tests/topotests/zebra_reconnect_route_replay/vr2/frr.conf b/tests/topotests/zebra_reconnect_route_replay/vr2/frr.conf
new file mode 100644
index 0000000000..e12e856412
--- /dev/null
+++ b/tests/topotests/zebra_reconnect_route_replay/vr2/frr.conf
@@ -0,0 +1,29 @@
+hostname vr2
+service integrated-vtysh-config
+!
+interface vr2-eth0 vrf blue
+ ip address 192.0.2.2/24
+ ip router isis 1
+!
+ip forwarding
+!
+router ospf vrf blue
+ ospf router-id 192.0.2.2
+ network 192.0.2.0/24 area 0
+!
+router rip vrf blue
+ version 2
+ network 192.0.2.0/24
+ timers basic 5 30 60
+!
+router isis 1 vrf blue
+ net 49.0001.0000.0000.0002.00
+ is-type level-2-only
+!
+router bgp 65002 vrf blue
+ no bgp ebgp-requires-policy
+ neighbor 192.0.2.1 remote-as 65001
+ address-family ipv4 unicast
+  neighbor 192.0.2.1 activate
+ exit-address-family
+!