Commit f131090af4 for frr
commit f131090af4557bb9f8c5f89d6c8f17211d3be0d7
Author: Olasupo Okunaiya <olasupo.o@gmail.com>
Date: Mon Sep 21 02:11:39 2026 +0100
ospf6d: re-originate Link-LSAs when zebra changes the router-id
An explicit "ospf6 router-id" resets the process, so the self-originated
LSAs are flushed and originated again under the new router-id. A
router-id learnt from zebra did neither, ospf6d only adopted the new
value. The Link-LSA advertised under the previous router-id is then
never flushed: it stays in the link-scoped database, and in the database
of every neighbour that has learnt it, until it reaches MaxAge. Lookups
there are keyed on advertising router and link state id, so a neighbour
resolving an IPv6 nexthop over that link can still be holding a Link-LSA
for a router-id that no longer exists. This shows up at startup, where
OSPFv3 adopts an auto-detected router-id, originates its Link-LSA, and
only then receives the configured "ip router-id".
Reset the process when zebra changes the router-id, and flush the
self-originated LSAs under the router-id they were advertised with. Both
the flush and the lookup that finds a previous LSA are keyed on the
advertising router, so flushing once the new router-id is in place
misses them. The explicit "ospf6 router-id" path had the same ordering
problem and is corrected with it. Add a topotest covering the change.
A zebra router-id change is accepted while there are no full neighbours,
which includes graceful restart recovery, so end the restart before
resetting. A graceful restart cannot outlive the router-id it was
started under, and leaving it in progress would keep suppressing route
updates to zebra after the database has been rebuilt.
Fixes #20856
Signed-off-by: Olasupo Okunaiya <olasupo.o@gmail.com>
diff --git a/ospf6d/ospf6_gr.c b/ospf6d/ospf6_gr.c
index efb6134309..385f3ca838 100644
--- a/ospf6d/ospf6_gr.c
+++ b/ospf6d/ospf6_gr.c
@@ -159,7 +159,7 @@ static void ospf6_gr_flush_grace_lsas(struct ospf6 *ospf6)
}
/* Exit from the Graceful Restart mode. */
-static void ospf6_gr_restart_exit(struct ospf6 *ospf6, const char *reason)
+void ospf6_gr_restart_exit(struct ospf6 *ospf6, const char *reason)
{
struct ospf6_area *area;
struct listnode *onode, *anode;
diff --git a/ospf6d/ospf6_gr.h b/ospf6d/ospf6_gr.h
index 7399c5aed1..22ad2f1c21 100644
--- a/ospf6d/ospf6_gr.h
+++ b/ospf6d/ospf6_gr.h
@@ -115,6 +115,7 @@ extern int config_write_ospf6_gr_helper(struct vty *vty, struct ospf6 *ospf6);
extern int config_write_ospf6_debug_gr_helper(struct vty *vty);
extern void ospf6_gr_iface_send_grace_lsa(struct event *event);
+extern void ospf6_gr_restart_exit(struct ospf6 *ospf6, const char *reason);
extern void ospf6_gr_restart_enter(struct ospf6 *ospf6,
enum ospf6_gr_restart_reason reason,
time_t timestamp);
diff --git a/ospf6d/ospf6_lsa.c b/ospf6d/ospf6_lsa.c
index 3f034ba4cd..632c1bb28d 100644
--- a/ospf6d/ospf6_lsa.c
+++ b/ospf6d/ospf6_lsa.c
@@ -967,21 +967,20 @@ void ospf6_lsa_refresh(struct event *event)
ospf6_flood(NULL, new);
}
-void ospf6_flush_self_originated_lsas_now(struct ospf6 *ospf6)
+void ospf6_flush_self_originated_lsas_now(struct ospf6 *ospf6, in_addr_t adv_router)
{
struct listnode *node;
struct listnode *if_node, *if_nnode;
struct ospf6_area *oa;
struct ospf6_lsa *lsa;
const struct route_node *end = NULL;
- uint32_t type, adv_router;
+ uint32_t type;
struct ospf6_interface *oi;
ospf6->inst_shutdown = 1;
for (ALL_LIST_ELEMENTS_RO(ospf6->area_list, node, oa)) {
- end = ospf6_lsdb_head(oa->lsdb_self, 0, 0, ospf6->router_id,
- &lsa);
+ end = ospf6_lsdb_head(oa->lsdb_self, 0, 0, adv_router, &lsa);
while (lsa) {
/* RFC 2328 (14.1): Set MAXAGE */
lsa->header->age = htons(OSPF_LSA_MAXAGE);
@@ -992,8 +991,7 @@ void ospf6_flush_self_originated_lsas_now(struct ospf6 *ospf6)
}
for (ALL_LIST_ELEMENTS(oa->if_list, if_node, if_nnode, oi)) {
- end = ospf6_lsdb_head(oi->lsdb_self, 0, 0,
- ospf6->router_id, &lsa);
+ end = ospf6_lsdb_head(oi->lsdb_self, 0, 0, adv_router, &lsa);
while (lsa) {
/* RFC 2328 (14.1): Set MAXAGE */
lsa->header->age = htons(OSPF_LSA_MAXAGE);
@@ -1006,7 +1004,6 @@ void ospf6_flush_self_originated_lsas_now(struct ospf6 *ospf6)
}
type = htons(OSPF6_LSTYPE_AS_EXTERNAL);
- adv_router = ospf6->router_id;
for (ALL_LSDB_TYPED_ADVRTR(ospf6->lsdb, type, adv_router, lsa)) {
/* RFC 2328 (14.1): Set MAXAGE */
lsa->header->age = htons(OSPF_LSA_MAXAGE);
diff --git a/ospf6d/ospf6_lsa.h b/ospf6d/ospf6_lsa.h
index d882874d6a..397f9beb1b 100644
--- a/ospf6d/ospf6_lsa.h
+++ b/ospf6d/ospf6_lsa.h
@@ -367,7 +367,7 @@ extern void ospf6_lsa_terminate(void);
extern int config_write_ospf6_debug_lsa(struct vty *vty);
extern void install_element_ospf6_debug_lsa(void);
extern void ospf6_lsa_age_set(struct ospf6_lsa *lsa);
-extern void ospf6_flush_self_originated_lsas_now(struct ospf6 *ospf6);
+extern void ospf6_flush_self_originated_lsas_now(struct ospf6 *ospf6, in_addr_t adv_router);
extern struct ospf6 *ospf6_get_by_lsdb(struct ospf6_lsa *lsa);
struct ospf6_lsa *ospf6_find_external_lsa(struct ospf6 *ospf6,
struct prefix *p);
diff --git a/ospf6d/ospf6_top.c b/ospf6d/ospf6_top.c
index 3fdd89906b..d29ec20518 100644
--- a/ospf6d/ospf6_top.c
+++ b/ospf6d/ospf6_top.c
@@ -569,7 +569,7 @@ void ospf6_delete(struct ospf6 **po)
ospf6_gr_helper_deinit(o);
if (!o->gr_info.prepare_in_progress)
- ospf6_flush_self_originated_lsas_now(o);
+ ospf6_flush_self_originated_lsas_now(o, o->router_id);
XFREE(MTYPE_TMP, o->gr_info.exit_reason);
ospf6_disable(o);
ospf6_del(o);
@@ -833,13 +833,21 @@ static void ospf6_db_clear(struct ospf6 *ospf6)
ospf6_route_remove_all(ospf6->brouter_table);
}
-static void ospf6_process_reset(struct ospf6 *ospf6)
+/*
+ * Reset the process, flushing the self-originated LSAs that were advertised
+ * under old_router_id. A router-id change has to flush the LSAs it already
+ * flooded under the previous value, because both the flush and the lookup
+ * that finds a previous LSA are keyed on the advertising router. Flushing
+ * after the new router-id is in place misses them and the neighbours keep
+ * them until MaxAge.
+ */
+void ospf6_process_reset_old_router_id(struct ospf6 *ospf6, in_addr_t old_router_id)
{
struct interface *ifp;
struct vrf *vrf = vrf_lookup_by_id(ospf6->vrf_id);
ospf6_unset_all_aggr_flag(ospf6);
- ospf6_flush_self_originated_lsas_now(ospf6);
+ ospf6_flush_self_originated_lsas_now(ospf6, old_router_id);
ospf6->inst_shutdown = 0;
ospf6_db_clear(ospf6);
@@ -848,6 +856,11 @@ static void ospf6_process_reset(struct ospf6 *ospf6)
ospf6_interface_clear(ifp);
}
+static void ospf6_process_reset(struct ospf6 *ospf6)
+{
+ ospf6_process_reset_old_router_id(ospf6, ospf6->router_id);
+}
+
DEFPY (clear_router_ospf6,
clear_router_ospf6_cmd,
"clear ipv6 ospf6 process [vrf NAME$name]",
@@ -887,6 +900,7 @@ DEFUN(ospf6_router_id,
int ret;
const char *router_id_str;
uint32_t router_id;
+ in_addr_t old_router_id = o->router_id;
argv_find(argv, argc, "A.B.C.D", &idx);
router_id_str = argv[idx]->arg;
@@ -900,7 +914,7 @@ DEFUN(ospf6_router_id,
o->router_id_static = router_id;
if (ospf6_router_id_update(o, false))
- ospf6_process_reset(o);
+ ospf6_process_reset_old_router_id(o, old_router_id);
else
vty_out(vty,
"For this router-id change to take effect run the \"clear ipv6 ospf6 process\" command\n");
@@ -916,12 +930,13 @@ DEFUN(no_ospf6_router_id,
V4NOTATION_STR)
{
VTY_DECLVAR_CONTEXT(ospf6, o);
+ in_addr_t old_router_id = o->router_id;
o->router_id_static = 0;
if (ospf6_router_id_update(o, false))
- ospf6_process_reset(o);
+ ospf6_process_reset_old_router_id(o, old_router_id);
else
vty_out(vty,
"For this router-id change to take effect run the \"clear ipv6 ospf6 process\" command\n");
diff --git a/ospf6d/ospf6_top.h b/ospf6d/ospf6_top.h
index 50016bdc6f..cb3653d51a 100644
--- a/ospf6d/ospf6_top.h
+++ b/ospf6d/ospf6_top.h
@@ -243,6 +243,7 @@ extern void install_element_ospf6_clear_process(void);
extern void ospf6_top_init(void);
extern void ospf6_delete(struct ospf6 **o);
extern bool ospf6_router_id_update(struct ospf6 *ospf6, bool init);
+extern void ospf6_process_reset_old_router_id(struct ospf6 *ospf6, in_addr_t old_router_id);
void ospf6_restart_spf(struct ospf6 *ospf6);
extern void ospf6_maxage_remove(struct ospf6 *o);
diff --git a/ospf6d/ospf6_zebra.c b/ospf6d/ospf6_zebra.c
index ce9348d4bf..0d2985a4b8 100644
--- a/ospf6d/ospf6_zebra.c
+++ b/ospf6d/ospf6_zebra.c
@@ -76,6 +76,7 @@ static int ospf6_router_id_update_zebra(ZAPI_CALLBACK_ARGS)
{
struct prefix router_id;
struct ospf6 *o;
+ in_addr_t old_router_id;
zebra_router_id_update_read(zclient->ibuf, &router_id);
@@ -88,9 +89,36 @@ static int ospf6_router_id_update_zebra(ZAPI_CALLBACK_ARGS)
if (o == NULL)
return 0;
+ old_router_id = o->router_id;
o->router_id_zebra = router_id.u.prefix4.s_addr;
- ospf6_router_id_update(o, false);
+ if (!ospf6_router_id_update(o, false))
+ return 0;
+
+ /*
+ * A router-id learnt from zebra has to restart the process the same
+ * way an explicit "ospf6 router-id" does. Self-originated LSAs carry
+ * the router-id as their advertising router, so without the restart
+ * the LSAs originated under the previous router-id are neither
+ * refreshed nor flushed. A Link-LSA is the visible casualty: it is
+ * only originated when the interface comes up, so it keeps the old
+ * advertising router and the neighbour's lookup, keyed on advertising
+ * router and link state id, no longer finds it. IPv6 nexthop
+ * resolution over that link fails until the LSA reaches MaxAge.
+ */
+ if (old_router_id != INADDR_ANY && o->router_id != old_router_id) {
+ /*
+ * A graceful restart cannot outlive the router-id it was
+ * started under, because the helping neighbours are holding
+ * our routes against the old identity. Leaving the restart in
+ * progress would also keep suppressing route updates to zebra
+ * after the database has been rebuilt under the new router-id.
+ */
+ if (o->gr_info.restart_in_progress)
+ ospf6_gr_restart_exit(o, "router-id changed");
+
+ ospf6_process_reset_old_router_id(o, old_router_id);
+ }
return 0;
}
diff --git a/tests/topotests/ospf6_router_id_link_lsa/r1/frr.conf b/tests/topotests/ospf6_router_id_link_lsa/r1/frr.conf
new file mode 100644
index 0000000000..9c00864d12
--- /dev/null
+++ b/tests/topotests/ospf6_router_id_link_lsa/r1/frr.conf
@@ -0,0 +1,15 @@
+hostname r1
+!
+ipv6 forwarding
+!
+ip router-id 10.0.0.1
+!
+interface r1-eth0
+ ipv6 address 2001:db8:12::1/64
+ ipv6 ospf6 area 0
+ ipv6 ospf6 network point-to-point
+ ipv6 ospf6 hello-interval 2
+ ipv6 ospf6 dead-interval 10
+!
+router ospf6
+!
diff --git a/tests/topotests/ospf6_router_id_link_lsa/r2/frr.conf b/tests/topotests/ospf6_router_id_link_lsa/r2/frr.conf
new file mode 100644
index 0000000000..fbf4578164
--- /dev/null
+++ b/tests/topotests/ospf6_router_id_link_lsa/r2/frr.conf
@@ -0,0 +1,13 @@
+hostname r2
+!
+ipv6 forwarding
+!
+interface r2-eth0
+ ipv6 address 2001:db8:12::2/64
+ ipv6 ospf6 network point-to-point
+ ipv6 ospf6 hello-interval 2
+ ipv6 ospf6 dead-interval 10
+!
+router ospf6
+ ospf6 router-id 10.0.0.2
+!
diff --git a/tests/topotests/ospf6_router_id_link_lsa/test_ospf6_router_id_link_lsa.py b/tests/topotests/ospf6_router_id_link_lsa/test_ospf6_router_id_link_lsa.py
new file mode 100644
index 0000000000..e7720b8518
--- /dev/null
+++ b/tests/topotests/ospf6_router_id_link_lsa/test_ospf6_router_id_link_lsa.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: ISC
+
+#
+# test_ospf6_router_id_link_lsa.py
+# Part of NetDEF Topology Tests
+#
+# Copyright (c) 2026 by Olasupo Okunaiya
+#
+
+"""
+test_ospf6_router_id_link_lsa.py: Test that OSPFv3 re-originates its
+Link-LSAs when zebra hands it a new router-id.
+
+An explicit "ospf6 router-id" restarts the process, so the self-originated
+LSAs are flushed and originated again under the new router-id. A router-id
+learnt from zebra did neither: ospf6d simply adopted the new value.
+
+Router-LSAs and Intra-Area-Prefix-LSAs survive that, because later area and
+interface events originate them again anyway. A Link-LSA does not. It is
+originated when the interface comes up and nothing schedules it afterwards,
+so it keeps the old advertising router. The neighbour's link-scoped lookup
+is keyed on advertising router and link state id, so it never finds a
+Link-LSA for the correct router-id and IPv6 nexthop resolution over that
+link fails until the stale LSA reaches MaxAge.
+
+This test drives the same code path deterministically: the router-id is
+changed while r1 has no adjacency, which is the window in which ospf6d
+accepts a router-id change at all.
+
+Topology:
+
+ r1 ------------- r2
+
+r2 starts with OSPFv3 not enabled on the link, so r1 has no neighbour while
+its router-id changes. OSPFv3 is enabled on r2 afterwards, and r2's view of
+the link-scoped database is what gets checked.
+"""
+
+import os
+import sys
+from functools import partial
+import pytest
+
+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
+
+pytestmark = [pytest.mark.ospf6d]
+
+OLD_ROUTER_ID = "10.0.0.1"
+NEW_ROUTER_ID = "10.0.0.9"
+
+
+def build_topo(tgen):
+ "Build function"
+
+ for routern in range(1, 3):
+ tgen.add_router("r{}".format(routern))
+
+ tgen.gears["r1"].add_link(tgen.gears["r2"])
+
+
+def setup_module(mod):
+ "Sets up the pytest environment"
+ tgen = Topogen(build_topo, mod.__name__)
+ tgen.start_topology()
+
+ for router in tgen.routers().values():
+ router.load_frr_config()
+
+ tgen.start_router()
+
+
+def teardown_module(_mod):
+ "Teardown the pytest environment"
+ tgen = get_topogen()
+ tgen.stop_topology()
+
+
+def _link_lsa_adv_routers(router):
+ """Return the advertising routers of the Link-LSAs in `router`'s
+ link-scoped database, without duplicates.
+
+ A Link-LSA is listed once per payload item (its link-local address and
+ then each prefix), so the same advertising router appears on several
+ rows of the table."""
+ tgen = get_topogen()
+ output = tgen.gears[router].vtysh_cmd("show ipv6 ospf6 database link")
+ adv_routers = []
+ for line in output.splitlines():
+ fields = line.split()
+ # Type LSId AdvRouter Age SeqNum Payload
+ # Lnk 0.0.0.2 10.0.0.9 9 80000001 ...
+ if len(fields) >= 5 and fields[0] == "Lnk":
+ if fields[2] not in adv_routers:
+ adv_routers.append(fields[2])
+ return adv_routers
+
+
+def _expect_link_lsa(router, router_id):
+ "Return None once `router` holds a Link-LSA advertised by `router_id`."
+ adv_routers = _link_lsa_adv_routers(router)
+ if router_id in adv_routers:
+ return None
+ return "Link-LSAs present: {}".format(adv_routers or "none")
+
+
+def _r1_has_full_neighbor():
+ "Return None once r1 has an adjacency in Full state."
+ tgen = get_topogen()
+ output = tgen.gears["r1"].vtysh_cmd("show ipv6 ospf6 neighbor json", isjson=True)
+ for neighbor in output.get("neighbors", []):
+ if neighbor.get("state") == "Full":
+ return None
+ return "no Full adjacency"
+
+
+def test_ospf6_link_lsa_originated():
+ "r1 must originate a Link-LSA under the router-id zebra gave it."
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ logger.info("waiting for r1 to originate its Link-LSA as %s", OLD_ROUTER_ID)
+ test_func = partial(_expect_link_lsa, "r1", OLD_ROUTER_ID)
+ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
+ assert result is None, "r1 did not originate a Link-LSA as {}: {}".format(
+ OLD_ROUTER_ID, result
+ )
+
+
+def test_ospf6_link_lsa_reoriginated_on_router_id_change():
+ "Changing the zebra router-id must re-originate r1's Link-LSA."
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ # r1 has no neighbour yet, so ospf6d accepts the router-id change.
+ tgen.gears["r1"].vtysh_cmd(
+ "configure terminal\nip router-id {}\n".format(NEW_ROUTER_ID)
+ )
+
+ logger.info("waiting for r1 to re-originate its Link-LSA as %s", NEW_ROUTER_ID)
+ test_func = partial(_expect_link_lsa, "r1", NEW_ROUTER_ID)
+ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
+ assert result is None, (
+ "r1 did not re-originate its Link-LSA after the router-id changed "
+ "to {}: {}".format(NEW_ROUTER_ID, result)
+ )
+
+ # The Link-LSA advertised under the old router-id must be gone, not left
+ # behind for the neighbours to trip over.
+ adv_routers = _link_lsa_adv_routers("r1")
+ assert OLD_ROUTER_ID not in adv_routers, (
+ "r1 still holds a Link-LSA advertised by the old router-id {}: "
+ "{}".format(OLD_ROUTER_ID, adv_routers)
+ )
+
+
+def test_ospf6_neighbor_sees_new_link_lsa():
+ "The neighbour must learn the Link-LSA of the new router-id only."
+ tgen = get_topogen()
+ if tgen.routers_have_failure():
+ pytest.skip(tgen.errors)
+
+ # Bring OSPFv3 up on r2 now that r1's router-id has settled.
+ tgen.gears["r2"].vtysh_cmd(
+ "configure terminal\ninterface r2-eth0\nipv6 ospf6 area 0\n"
+ )
+
+ _, result = topotest.run_and_expect(_r1_has_full_neighbor, None, count=60, wait=1)
+ assert result is None, "OSPFv3 adjacency did not come up: {}".format(result)
+
+ logger.info("checking r2 learns r1's Link-LSA as %s", NEW_ROUTER_ID)
+ test_func = partial(_expect_link_lsa, "r2", NEW_ROUTER_ID)
+ _, result = topotest.run_and_expect(test_func, None, count=60, wait=1)
+ assert result is None, (
+ "r2 never learnt a Link-LSA from r1 under its current router-id "
+ "{}: {}".format(NEW_ROUTER_ID, result)
+ )
+
+ adv_routers = _link_lsa_adv_routers("r2")
+ assert OLD_ROUTER_ID not in adv_routers, (
+ "r2 holds a stale Link-LSA advertised by r1's old router-id {}: "
+ "{}".format(OLD_ROUTER_ID, adv_routers)
+ )
+
+
+if __name__ == "__main__":
+ args = ["-s"] + sys.argv[1:]
+ sys.exit(pytest.main(args))