Commit 8c18c5caea for frr
commit 8c18c5caeaa028bc0c5cb1fe8724803fa8a70624
Author: David Lamparter <equinox@diac24.net>
Date: Tue Nov 3 02:44:32 2026 +0100
Revert "ospfd/ospf6d: cache ASE forwarding-address locality check per calculation pass"
diff --git a/ospf6d/ospf6_area.c b/ospf6d/ospf6_area.c
index 73f368c959..606de46906 100644
--- a/ospf6d/ospf6_area.c
+++ b/ospf6d/ospf6_area.c
@@ -96,7 +96,7 @@ static void ospf6_area_lsdb_hook_add(struct ospf6_lsa *lsa)
break;
case OSPF6_LSTYPE_TYPE_7:
- ospf6_asbr_lsa_add(lsa, NULL);
+ ospf6_asbr_lsa_add(lsa);
break;
default:
diff --git a/ospf6d/ospf6_asbr.c b/ospf6d/ospf6_asbr.c
index dcc29da2b1..549910ca89 100644
--- a/ospf6d/ospf6_asbr.c
+++ b/ospf6d/ospf6_asbr.c
@@ -466,29 +466,13 @@ void ospf6_asbr_update_route_ecmp_path(struct ospf6_route *old,
/* Check if the forwarding address is local address */
static int ospf6_ase_forward_address_check(struct ospf6 *ospf6,
- struct in6_addr *fwd_addr,
- struct route_table *fwd_cache)
+ struct in6_addr *fwd_addr)
{
struct listnode *anode, *node;
struct ospf6_interface *oi;
struct ospf6_area *oa;
struct interface *ifp;
struct connected *c;
- struct route_node *rn;
- struct prefix p;
- int ret = 1;
-
- if (fwd_cache) {
- p.family = AF_INET6;
- p.prefixlen = IPV6_MAX_BITLEN;
- p.u.prefix6 = *fwd_addr;
- rn = route_node_lookup(fwd_cache, &p);
- if (rn) {
- ret = (int)((uintptr_t)rn->info - 1);
- route_unlock_node(rn);
- return ret;
- }
- }
for (ALL_LIST_ELEMENTS_RO(ospf6->area_list, anode, oa)) {
for (ALL_LIST_ELEMENTS_RO(oa->if_list, node, oi)) {
@@ -499,30 +483,16 @@ static int ospf6_ase_forward_address_check(struct ospf6 *ospf6,
ifp = oi->interface;
frr_each (if_connected, ifp->connected, c) {
if (IPV6_ADDR_SAME(&c->address->u.prefix6,
- fwd_addr)) {
- ret = 0;
- break;
- }
+ fwd_addr))
+ return 0;
}
- if (ret == 0)
- break;
}
- if (ret == 0)
- break;
}
- if (fwd_cache) {
- rn = route_node_get(fwd_cache, &p);
- if (rn->info == NULL)
- rn->info = (void *)(uintptr_t)(ret + 1);
- else
- route_unlock_node(rn);
- }
-
- return ret;
+ return 1;
}
-void ospf6_asbr_lsa_add(struct ospf6_lsa *lsa, struct route_table *fwd_cache)
+void ospf6_asbr_lsa_add(struct ospf6_lsa *lsa)
{
struct ospf6_as_external_lsa *external;
struct prefix asbr_id;
@@ -634,7 +604,7 @@ void ospf6_asbr_lsa_add(struct ospf6_lsa *lsa, struct route_table *fwd_cache)
if (!IN6_IS_ADDR_UNSPECIFIED(&fwd_addr.u.prefix6)) {
if (!ospf6_ase_forward_address_check(
- ospf6, &fwd_addr.u.prefix6, fwd_cache)) {
+ ospf6, &fwd_addr.u.prefix6)) {
if (IS_OSPF6_DEBUG_EXAMIN(AS_EXTERNAL))
zlog_debug("Fwd address (%s:%u:%u)%pFX is local address",
OSPF6_DEBUG_VRF(ospf6), &fwd_addr);
@@ -1021,7 +991,7 @@ void ospf6_asbr_lsentry_add(struct ospf6_route *asbr_entry, struct ospf6 *ospf6)
router = ospf6_linkstate_prefix_adv_router(&asbr_entry->prefix);
for (ALL_LSDB_TYPED_ADVRTR(ospf6->lsdb, type, router, lsa)) {
if (!OSPF6_LSA_IS_MAXAGE(lsa))
- ospf6_asbr_lsa_add(lsa, NULL);
+ ospf6_asbr_lsa_add(lsa);
}
}
@@ -1041,14 +1011,6 @@ void ospf6_asbr_lsentry_remove(struct ospf6_route *asbr_entry,
void ospf6_asbr_recalculate_external_routes(struct ospf6 *ospf6)
{
struct ospf6_lsa *lsa, *lsanext;
- struct route_table *fwd_cache;
-
- /* The forwarding-address locality check reads the area/interface and
- * connected lists, which cannot change while this loop runs. Cache
- * the verdict per address so that external LSAs carrying the same
- * forwarding address do not re-scan them.
- */
- fwd_cache = route_table_init();
for (ALL_LSDB(ospf6->lsdb, lsa, lsanext)) {
if (ntohs(lsa->header->type) != OSPF6_LSTYPE_AS_EXTERNAL ||
@@ -1061,10 +1023,8 @@ void ospf6_asbr_recalculate_external_routes(struct ospf6 *ospf6)
* installation in sync with current topology.
*/
ospf6_asbr_lsa_remove(lsa, NULL);
- ospf6_asbr_lsa_add(lsa, fwd_cache);
+ ospf6_asbr_lsa_add(lsa);
}
-
- route_table_finish(fwd_cache);
}
diff --git a/ospf6d/ospf6_asbr.h b/ospf6d/ospf6_asbr.h
index 7ac5a8c6cd..1f23fca975 100644
--- a/ospf6d/ospf6_asbr.h
+++ b/ospf6d/ospf6_asbr.h
@@ -92,8 +92,7 @@ struct ospf6_external_aggr_rt {
htonl(C); \
}
-extern void ospf6_asbr_lsa_add(struct ospf6_lsa *lsa,
- struct route_table *fwd_cache);
+extern void ospf6_asbr_lsa_add(struct ospf6_lsa *lsa);
extern void ospf6_asbr_lsa_remove(struct ospf6_lsa *lsa,
struct ospf6_route *asbr_entry);
diff --git a/ospf6d/ospf6_spf.c b/ospf6d/ospf6_spf.c
index a0e281e367..75de07c695 100644
--- a/ospf6d/ospf6_spf.c
+++ b/ospf6d/ospf6_spf.c
@@ -1106,8 +1106,7 @@ void ospf6_remove_temp_router_lsa(struct ospf6_area *area)
}
int ospf6_ase_calculate_route(struct ospf6 *ospf6, struct ospf6_lsa *lsa,
- struct ospf6_area *area,
- struct route_table *fwd_cache)
+ struct ospf6_area *area)
{
struct ospf6_route *route;
struct ospf6_as_external_lsa *external;
@@ -1144,7 +1143,7 @@ int ospf6_ase_calculate_route(struct ospf6 *ospf6, struct ospf6_lsa *lsa,
ospf6->route_table->hook_remove = NULL;
if (!OSPF6_LSA_IS_MAXAGE(lsa))
- ospf6_asbr_lsa_add(lsa, fwd_cache);
+ ospf6_asbr_lsa_add(lsa);
ospf6->route_table->hook_add = hook_add;
ospf6->route_table->hook_remove = hook_remove;
@@ -1181,7 +1180,7 @@ int ospf6_ase_calculate_route(struct ospf6 *ospf6, struct ospf6_lsa *lsa,
area->route_table->hook_remove = NULL;
if (!OSPF6_LSA_IS_MAXAGE(lsa))
- ospf6_asbr_lsa_add(lsa, fwd_cache);
+ ospf6_asbr_lsa_add(lsa);
area->route_table->hook_add = hook_add;
area->route_table->hook_remove = hook_remove;
@@ -1226,22 +1225,14 @@ static void ospf6_ase_calculate_timer(struct event *t)
struct ospf6_lsa *lsa;
struct listnode *node, *nnode;
struct ospf6_area *area;
- struct route_table *fwd_cache;
uint16_t type;
ospf6 = EVENT_ARG(t);
- /* The forwarding-address locality check reads the area/interface and
- * connected lists, which cannot change while this pass runs. Cache
- * the verdict per address so that a batch of external LSAs carrying
- * the same forwarding address does not re-scan them.
- */
- fwd_cache = route_table_init();
-
/* Calculate external route for each AS-external-LSA */
type = htons(OSPF6_LSTYPE_AS_EXTERNAL);
for (ALL_LSDB_TYPED(ospf6->lsdb, type, lsa))
- ospf6_ase_calculate_route(ospf6, lsa, NULL, fwd_cache);
+ ospf6_ase_calculate_route(ospf6, lsa, NULL);
/* This version simple adds to the table all NSSA areas */
if (ospf6->anyNSSA) {
@@ -1252,13 +1243,10 @@ static void ospf6_ase_calculate_timer(struct event *t)
type = htons(OSPF6_LSTYPE_TYPE_7);
for (ALL_LSDB_TYPED(area->lsdb, type, lsa))
- ospf6_ase_calculate_route(ospf6, lsa, area,
- fwd_cache);
+ ospf6_ase_calculate_route(ospf6, lsa, area);
}
}
- route_table_finish(fwd_cache);
-
if (ospf6->gr_info.finishing_restart) {
/*
* The routing table computation is complete. Uninstall remnant
diff --git a/ospf6d/ospf6_spf.h b/ospf6d/ospf6_spf.h
index 60b9aacb95..c2fd5d9ef1 100644
--- a/ospf6d/ospf6_spf.h
+++ b/ospf6d/ospf6_spf.h
@@ -151,8 +151,7 @@ extern struct ospf6_lsa *ospf6_create_single_router_lsa(struct ospf6_area *area,
extern void ospf6_remove_temp_router_lsa(struct ospf6_area *area);
extern void ospf6_ase_calculate_timer_add(struct ospf6 *ospf6);
extern int ospf6_ase_calculate_route(struct ospf6 *ospf6, struct ospf6_lsa *lsa,
- struct ospf6_area *area,
- struct route_table *fwd_cache);
+ struct ospf6_area *area);
extern bool
ospf6_merge_parents_nh_to_child(struct ospf6_vertex *v,
struct ospf6_route *route,
diff --git a/ospf6d/ospf6_top.c b/ospf6d/ospf6_top.c
index 1fa9f807fe..9f049afcbc 100644
--- a/ospf6d/ospf6_top.c
+++ b/ospf6d/ospf6_top.c
@@ -297,7 +297,7 @@ static void ospf6_top_lsdb_hook_add(struct ospf6_lsa *lsa)
{
switch (ntohs(lsa->header->type)) {
case OSPF6_LSTYPE_AS_EXTERNAL:
- ospf6_asbr_lsa_add(lsa, NULL);
+ ospf6_asbr_lsa_add(lsa);
break;
default:
diff --git a/ospfd/ospf_ase.c b/ospfd/ospf_ase.c
index 1f91295482..0267f26955 100644
--- a/ospfd/ospf_ase.c
+++ b/ospfd/ospf_ase.c
@@ -126,45 +126,19 @@ static void ospf_ase_complete_direct_routes(struct ospf_route *ro,
}
static int ospf_ase_forward_address_check(struct ospf *ospf,
- struct in_addr fwd_addr,
- struct route_table *fwd_cache)
+ struct in_addr fwd_addr)
{
struct listnode *ifn;
struct ospf_interface *oi;
- struct route_node *rn;
- struct prefix p;
- int ret = 1;
-
- if (fwd_cache) {
- p.family = AF_INET;
- p.prefixlen = IPV4_MAX_BITLEN;
- p.u.prefix4 = fwd_addr;
- rn = route_node_lookup(fwd_cache, &p);
- if (rn) {
- ret = (int)((uintptr_t)rn->info - 1);
- route_unlock_node(rn);
- return ret;
- }
- }
for (ALL_LIST_ELEMENTS_RO(ospf->oiflist, ifn, oi))
if (if_is_operative(oi->ifp))
if (oi->type != OSPF_IFTYPE_VIRTUALLINK)
if (IPV4_ADDR_SAME(&oi->address->u.prefix4,
- &fwd_addr)) {
- ret = 0;
- break;
- }
+ &fwd_addr))
+ return 0;
- if (fwd_cache) {
- rn = route_node_get(fwd_cache, &p);
- if (rn->info == NULL)
- rn->info = (void *)(uintptr_t)(ret + 1);
- else
- route_unlock_node(rn);
- }
-
- return ret;
+ return 1;
}
static struct ospf_route *
@@ -210,8 +184,7 @@ ospf_ase_calculate_new_route(struct ospf_lsa *lsa,
#define OSPF_ASE_CALC_INTERVAL 1
-int ospf_ase_calculate_route(struct ospf *ospf, struct ospf_lsa *lsa,
- struct route_table *fwd_cache)
+int ospf_ase_calculate_route(struct ospf *ospf, struct ospf_lsa *lsa)
{
uint32_t metric;
struct as_external_lsa *al;
@@ -332,8 +305,7 @@ int ospf_ase_calculate_route(struct ospf *ospf, struct ospf_lsa *lsa,
routing table entry must specify an intra-area or inter-area
path; if no such path exists, do nothing with the LSA and
consider the next in the list. */
- if (!ospf_ase_forward_address_check(ospf, al->e[0].fwd_addr,
- fwd_cache)) {
+ if (!ospf_ase_forward_address_check(ospf, al->e[0].fwd_addr)) {
if (IS_DEBUG_OSPF(lsa, LSA))
zlog_debug(
"Route[External]: Forwarding address is our router address");
@@ -593,7 +565,6 @@ static void ospf_ase_calculate_timer(struct event *t)
struct route_node *rn;
struct listnode *node;
struct ospf_area *area;
- struct route_table *fwd_cache;
struct timeval start_time, stop_time;
ospf = EVENT_ARG(t);
@@ -603,16 +574,9 @@ static void ospf_ase_calculate_timer(struct event *t)
monotime(&start_time);
- /* The forwarding-address locality check reads oiflist, which
- * cannot change while this pass runs. Cache the verdict per
- * address so that a batch of external LSAs carrying the same
- * forwarding address does not re-scan the interface list.
- */
- fwd_cache = route_table_init();
-
/* Calculate external route for each AS-external-LSA */
LSDB_LOOP (EXTERNAL_LSDB(ospf), rn, lsa)
- ospf_ase_calculate_route(ospf, lsa, fwd_cache);
+ ospf_ase_calculate_route(ospf, lsa);
/* This version simple adds to the table all NSSA areas */
if (ospf->anyNSSA)
@@ -624,14 +588,11 @@ static void ospf_ase_calculate_timer(struct event *t)
if (area->external_routing == OSPF_AREA_NSSA)
LSDB_LOOP (NSSA_LSDB(area), rn, lsa)
ospf_ase_calculate_route(ospf,
- lsa,
- fwd_cache);
+ lsa);
}
/* kevinm: And add the NSSA routes in ospf_top */
LSDB_LOOP (NSSA_LSDB(ospf), rn, lsa)
- ospf_ase_calculate_route(ospf, lsa, fwd_cache);
-
- route_table_finish(fwd_cache);
+ ospf_ase_calculate_route(ospf, lsa);
/* Compare old and new external routing table and install the
difference info zebra/kernel */
@@ -789,7 +750,7 @@ void ospf_ase_incremental_update(struct ospf *ospf, struct ospf_lsa *lsa)
route_unlock_node(rn);
for (ALL_LIST_ELEMENTS_RO(lsas, node, lsa))
- ospf_ase_calculate_route(ospf, lsa, NULL);
+ ospf_ase_calculate_route(ospf, lsa);
/* prepare temporary old routing table for compare */
tmp_old = route_table_init();
diff --git a/ospfd/ospf_ase.h b/ospfd/ospf_ase.h
index 3b568e3dd0..d444e160a8 100644
--- a/ospfd/ospf_ase.h
+++ b/ospfd/ospf_ase.h
@@ -13,8 +13,7 @@ extern struct ospf_route *ospf_find_asbr_route_through_area(struct route_table *
struct prefix_ipv4 *asbr,
struct ospf_area *area);
-extern int ospf_ase_calculate_route(struct ospf *ospf, struct ospf_lsa *lsa,
- struct route_table *fwd_cache);
+extern int ospf_ase_calculate_route(struct ospf *ospf, struct ospf_lsa *lsa);
extern void ospf_ase_calculate_schedule(struct ospf *ospf);
extern void ospf_ase_calculate_timer_add(struct ospf *ospf);
diff --git a/tests/topotests/ospf6_ase_forward_scan/__init__.py b/tests/topotests/ospf6_ase_forward_scan/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/topotests/ospf6_ase_forward_scan/inject_lsu.py b/tests/topotests/ospf6_ase_forward_scan/inject_lsu.py
deleted file mode 100644
index cc0302959f..0000000000
--- a/tests/topotests/ospf6_ase_forward_scan/inject_lsu.py
+++ /dev/null
@@ -1,171 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: ISC
-#
-# inject_lsu.py
-#
-# Copyright (c) 2026 ATCorp
-#
-"""Build and send raw OSPFv3 Link State Update packets.
-
-Used by test_ospf6_ase_forward_scan.py to inject AS-external LSAs with the
-F-bit set and a chosen forwarding address, bypassing the well-formedness
-of high-level packet helpers.
-"""
-
-import argparse
-import socket
-import struct
-import sys
-import time
-
-from scapy.all import Ether, IPv6, Raw, conf, sendp
-
-conf.verb = 0
-
-# 20-byte LSA header + 40-byte AS-external body (F-bit, /128 prefix).
-LSA_CHUNK = 16
-
-
-def lsa_id(index):
- return "198.18.%d.%d" % (index // 256, index % 256)
-
-
-def fletcher_checksum(lsa):
- data = bytearray(lsa[2:])
- data[14:16] = b"\x00\x00"
- c0 = 0
- c1 = 0
- for value in data:
- c0 = (c0 + value) % 255
- c1 = (c1 + c0) % 255
- x = ((len(data) - 14 - 1) * c0 - c1) % 255
- if x == 0:
- x = 255
- y = 510 - c0 - x
- if y > 255:
- y -= 255
- data[14:16] = bytes((x, y))
- valid = bytearray(data)
- c0 = 0
- c1 = 0
- for value in valid:
- c0 = (c0 + value) % 255
- c1 = (c1 + c0) % 255
- if (c1 << 8) + c0 != 0:
- raise ValueError("invalid generated LSA Fletcher checksum")
- return (x << 8) | y
-
-
-def internet_checksum(data):
- if len(data) % 2:
- data += b"\x00"
- total = sum(struct.unpack("!%dH" % (len(data) // 2), data))
- total = (total & 0xFFFF) + (total >> 16)
- total = (total & 0xFFFF) + (total >> 16)
- return (~total) & 0xFFFF
-
-
-def ipv6_ph_checksum(src, dst, payload):
- pseudo = (
- socket.inet_pton(socket.AF_INET6, src)
- + socket.inet_pton(socket.AF_INET6, dst)
- + struct.pack("!I", len(payload))
- + b"\x00\x00\x00"
- + bytes((89,))
- )
- return internet_checksum(pseudo + payload)
-
-
-def make_external_lsa(index, adv_router, fwd, corrupt):
- # bits_metric: F bit (wire value 0x02000000) + 24-bit metric 10.
- body = (
- struct.pack("!I", 0x0200000A)
- + bytes((128, 0))
- + struct.pack("!H", 0)
- + socket.inet_pton(socket.AF_INET6, "2001:db8:ff::%x" % index)
- + socket.inet_pton(socket.AF_INET6, fwd)
- )
- lsa = bytearray(
- struct.pack(
- "!HH4s4sIHH",
- 1,
- 0x4005,
- socket.inet_aton(lsa_id(index)),
- socket.inet_aton(adv_router),
- 0x80000001,
- 0,
- 20 + len(body),
- )
- + body
- )
- struct.pack_into("!H", lsa, 16, fletcher_checksum(lsa))
- if corrupt:
- lsa[17] ^= 0x5A
- return bytes(lsa)
-
-
-def make_lsu(lsa_blobs, src_rid, src_ip, dst_ip):
- body = struct.pack("!I", len(lsa_blobs)) + b"".join(lsa_blobs)
- packet = bytearray(
- struct.pack(
- "!BBH4s4sHBB",
- 3,
- 4,
- 16 + len(body),
- socket.inet_aton(src_rid),
- socket.inet_aton("0.0.0.0"),
- 0,
- 0,
- 0,
- )
- + body
- )
- checksum = ipv6_ph_checksum(src_ip, dst_ip, bytes(packet))
- struct.pack_into("!H", packet, 12, checksum)
- if ipv6_ph_checksum(src_ip, dst_ip, bytes(packet)) != 0:
- raise ValueError("invalid generated OSPFv3 pseudo-header checksum")
- return bytes(packet)
-
-
-def main():
- parser = argparse.ArgumentParser()
- parser.add_argument("--iface", required=True)
- parser.add_argument("--src-mac", required=True)
- parser.add_argument("--dst-mac", required=True)
- parser.add_argument("--src-ip", required=True)
- parser.add_argument("--dst-ip", required=True)
- parser.add_argument("--src-rid", required=True)
- parser.add_argument("--adv-router", required=True)
- parser.add_argument("--fwd", required=True)
- parser.add_argument("--count", type=int, default=16)
- parser.add_argument("--first-index", type=int, default=1)
- parser.add_argument("--corrupt", action="store_true")
- parser.add_argument("--spacing", type=float, default=0.2)
- args = parser.parse_args()
-
- blobs = [
- make_external_lsa(index, args.adv_router, args.fwd, args.corrupt)
- for index in range(args.first_index, args.first_index + args.count)
- ]
- packets = [
- make_lsu(blobs[off : off + LSA_CHUNK], args.src_rid, args.src_ip,
- args.dst_ip)
- for off in range(0, len(blobs), LSA_CHUNK)
- ]
- for index, payload in enumerate(packets):
- if index:
- time.sleep(args.spacing)
- frame = (
- Ether(src=args.src_mac, dst=args.dst_mac)
- / IPv6(src=args.src_ip, dst=args.dst_ip, nh=89, hlim=1)
- / Raw(payload)
- )
- sendp(frame, iface=args.iface, count=1, verbose=False)
- print(
- "OA2_INJECT packets=%d lsas=%d fwd=%s corrupt=%s first=%d"
- % (len(packets), args.count, args.fwd, args.corrupt, args.first_index)
- )
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/tests/topotests/ospf6_ase_forward_scan/r1/frr.conf b/tests/topotests/ospf6_ase_forward_scan/r1/frr.conf
deleted file mode 100644
index 8d7135660b..0000000000
--- a/tests/topotests/ospf6_ase_forward_scan/r1/frr.conf
+++ /dev/null
@@ -1,18 +0,0 @@
-!
-hostname r1
-!
-debug ospf6 lsa as-external examine
-!
-interface eth1
- ipv6 address 2001:db8::1/64
- ipv6 ospf6 area 0
-!
-interface eth2
- ipv6 address 2001:db8:1::1/64
- ipv6 ospf6 area 0
- ipv6 ospf6 passive
-!
-router ospf6
- ospf6 router-id 1.1.1.1
- log-adjacency-changes detail
-!
diff --git a/tests/topotests/ospf6_ase_forward_scan/r2/frr.conf b/tests/topotests/ospf6_ase_forward_scan/r2/frr.conf
deleted file mode 100644
index 917cdb4e8a..0000000000
--- a/tests/topotests/ospf6_ase_forward_scan/r2/frr.conf
+++ /dev/null
@@ -1,15 +0,0 @@
-!
-hostname r2
-!
-interface lo
- ipv6 address 2001:db8:ffff::9/128
-!
-interface eth1
- ipv6 address 2001:db8::2/64
- ipv6 ospf6 area 0
-!
-router ospf6
- ospf6 router-id 2.2.2.2
- redistribute connected
- log-adjacency-changes detail
-!
diff --git a/tests/topotests/ospf6_ase_forward_scan/test_ospf6_ase_forward_scan.py b/tests/topotests/ospf6_ase_forward_scan/test_ospf6_ase_forward_scan.py
deleted file mode 100644
index 9742ee2bc1..0000000000
--- a/tests/topotests/ospf6_ase_forward_scan/test_ospf6_ase_forward_scan.py
+++ /dev/null
@@ -1,449 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 eval: (blacken-mode 1) -*-
-# SPDX-License-Identifier: ISC
-#
-# test_ospf6_ase_forward_scan.py
-#
-# Copyright (c) 2026 ATCorp
-#
-
-"""
-test_ospf6_ase_forward_scan.py
-==============================
-
-Regression for per-LSA repeated forwarding-address locality scans in the
-OSPFv3 AS-external route calculation (RFC 5340 section 4.4.4, RFC 3101
-section 2.5).
-
-Every AS-external-LSA with the F-bit set and a non-zero forwarding address
-makes ospf6_asbr_lsa_add() call ospf6_ase_forward_address_check(), which
-walks every area, interface and connected address. A batch of M external
-LSAs that carry the same non-local forwarding address therefore re-scans
-the identical address M times per ASE calculation pass, and again on every
-later pass. The interface and connected lists cannot change while a pass
-runs, so the verdict must be cached per address for the duration of the
-pass.
-
-Topology
---------
-
- r1 (RID 1.1.1.1, victim) ---- s1 ---- r2 (RID 2.2.2.2, ASBR)
- | |
- h1 (scapy injector) (isolated after baseline)
- |
- s2 (stub, forces SPF passes via link toggle)
-
-Test plan
----------
-
-1. test_adjacency_full
- Baseline: LAN adjacency reaches Full and the ASBR route exists.
-
-2. test_same_fwd_scan_once_per_pass
- r2 is isolated (link down, within the raised dead interval) and h1
- injects 16 AS-external LSAs with distinct LS IDs, the F-bit set and
- the same non-local forwarding address. gdb counts the scan-loop
- iterations on r1 while an r1 link toggle forces ASE passes. Expect
- at most one interface scan per pass, not one per LSA.
-
-3. test_zero_fwd_and_corrupt_controls
- Same-shaped LSUs with the unspecified forwarding address (no scan,
- LSAs installed) and with a broken Fletcher checksum (rejected, no
- scan).
-
-The gdb-based scan counts are the primary observable; tests 2-3 skip when
-gdb or the required source lines are unavailable.
-"""
-
-import json
-import os
-import re
-import shlex
-import sys
-import time
-
-import pytest
-
-from lib.topogen import Topogen, get_topogen
-from lib.topolog import logger
-
-
-CWD = os.path.dirname(os.path.realpath(__file__))
-sys.path.append(os.path.join(CWD, "../"))
-
-pytestmark = [pytest.mark.ospf6d]
-
-R1_RID = "1.1.1.1"
-R2_RID = "2.2.2.2"
-R1_IP = "2001:db8::1"
-R2_IP = "2001:db8::2"
-H1_IP = "2001:db8::3"
-FWD = "2001:db8:99::1"
-LSA_ID_PREFIX = "198.18."
-BATCH = 16
-
-GDB_SCRIPT = """set pagination off
-set confirm off
-handle SIGINT stop print nopass
-set $calls = 0
-set $visits = 0
-set $pcalls = 0
-set $pvisits = 0
-break ospf6_asbr.c:%(call)d
-commands
- silent
- set $calls = $calls + 1
- set $pcalls = $pcalls + 1
- continue
-end
-break ospf6_asbr.c:%(loop)d
-commands
- silent
- set $visits = $visits + 1
- set $pvisits = $pvisits + 1
- continue
-end
-break ospf6_ase_calculate_timer
-commands
- silent
- set $pcalls = 0
- set $pvisits = 0
- continue
-end
-break ospf6_spf.c:%(exit)d
-commands
- silent
- printf "OA2_PASS calls=%%d visits=%%d\\n", $pcalls, $pvisits
- continue
-end
-continue
-printf "OA2_TOTAL calls=%%d visits=%%d\\n", $calls, $visits
-detach
-quit
-"""
-
-
-def lsa_id(index):
- return "198.18.%d.%d" % (index // 256, index % 256)
-
-
-def build_topo(tgen):
- r1 = tgen.add_router("r1")
- r2 = tgen.add_router("r2")
- h1 = tgen.add_host("h1", H1_IP + "/64", "via " + R1_IP)
- s1 = tgen.add_switch("s1")
- s1.add_link(r1, nodeif="eth1")
- s1.add_link(r2, nodeif="eth1")
- s1.add_link(h1, nodeif="eth1")
- s2 = tgen.add_switch("s2")
- s2.add_link(r1, nodeif="eth2")
-
-
-def setup_module(mod):
- 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():
- tgen = get_topogen()
- tgen.stop_topology()
-
-
-def _vty_json(router, command):
- return json.loads(router.vtysh_cmd(command))
-
-
-def _wait_until(func, timeout_s=20, interval_s=0.5):
- deadline = time.time() + timeout_s
- while time.time() < deadline:
- if func():
- return True
- time.sleep(interval_s)
- return bool(func())
-
-
-def _neighbor_full(router, rid):
- try:
- data = _vty_json(router, "show ipv6 ospf6 neighbor json")
- except (json.JSONDecodeError, ValueError):
- return False
-
- def search(value):
- if isinstance(value, dict):
- if value.get("neighborId") == rid and value.get("state") == "Full":
- return True
- for child in value.values():
- if search(child):
- return True
- elif isinstance(value, list):
- for child in value:
- if search(child):
- return True
- return False
-
- return search(data)
-
-
-def _asbr_route_present(router):
- return R2_RID in router.vtysh_cmd("show ipv6 ospf6 border-routers")
-
-
-def _external_ids(router):
- data = _vty_json(router, "show ipv6 ospf6 database as-external json")
- ids = set()
-
- def walk(value):
- if isinstance(value, dict):
- if (value.get("lsId", "").startswith(LSA_ID_PREFIX)
- and value.get("advRouter") == R2_RID):
- ids.add(value["lsId"])
- for child in value.values():
- walk(child)
- elif isinstance(value, list):
- for child in value:
- walk(child)
-
- walk(data)
- return ids
-
-
-def _mac(gear):
- return gear.cmd("cat /sys/class/net/eth1/address").strip()
-
-
-def _source_lines():
- """Locate the breakpoint lines at runtime, tolerant to line shifts."""
- asbr = os.path.join(CWD, "..", "..", "..", "ospf6d", "ospf6_asbr.c")
- spf = os.path.join(CWD, "..", "..", "..", "ospf6d", "ospf6_spf.c")
- patterns = {
- "call": (asbr, r"if \(!ospf6_ase_forward_address_check\("),
- "loop": (asbr, r"if \(!if_is_operative\(oi->interface\)"),
- "exit": (spf, r"if \(ospf6->gr_info\.finishing_restart\) \{"),
- }
- lines = {}
- for key, (source, pattern) in patterns.items():
- with open(source) as fp:
- content = fp.readlines()
- for lineno, text in enumerate(content, 1):
- if re.search(pattern, text):
- lines[key] = lineno
- break
- else:
- raise LookupError("pattern %r not found in %s" % (pattern, source))
- return lines
-
-
-def _gdb_usable(router):
- if not router.cmd("which gdb || true").strip():
- return False
- try:
- _source_lines()
- except (OSError, LookupError):
- return False
- return True
-
-
-def _ospf6d_pid(router):
- # pidof is ambiguous: all routers share the pid namespace and run
- # identical ospf6d cmdlines. The pid file lives in the router's own
- # mount namespace and names exactly this router's ospf6d.
- return router.cmd("cat /var/run/frr/ospf6d.pid").strip().splitlines()[-1]
-
-
-def _gdb_start(router, logdir):
- script = os.path.join(logdir, "oa2_count.gdb")
- log = os.path.join(logdir, "oa2_count.log")
- with open(script, "w") as fp:
- fp.write(GDB_SCRIPT % _source_lines())
- router.cmd("rm -f %s" % shlex.quote(log))
- pid = router.cmd(
- "nohup gdb -q -batch -x %s -p %s > %s 2>&1 </dev/null & echo $!"
- % (shlex.quote(script), _ospf6d_pid(router), shlex.quote(log))
- ).strip()
- pid = pid.splitlines()[-1]
- assert pid.isdigit(), "gdb did not start on %s (pid %r)" % (router.name, pid)
- assert _wait_until(
- lambda: os.path.exists(log)
- and "Breakpoint 4 at" in open(log, errors="replace").read()
- ), "gdb breakpoints not ready on %s" % router.name
- return pid, log
-
-
-def _gdb_stop(router, pid, log):
- router.cmd("kill -INT %s >/dev/null 2>&1 || true" % pid)
-
- def _totals():
- if not os.path.exists(log):
- return None
- match = re.search(
- r"OA2_TOTAL calls=(\d+) visits=(\d+)", open(log, errors="replace").read()
- )
- return (int(match.group(1)), int(match.group(2))) if match else None
-
- def _passes():
- if not os.path.exists(log):
- return []
- return [
- (int(m.group(1)), int(m.group(2)))
- for m in re.finditer(
- r"OA2_PASS calls=(\d+) visits=(\d+)",
- open(log, errors="replace").read(),
- )
- ]
-
- assert _wait_until(lambda: _totals() is not None, timeout_s=15), (
- "gdb did not report counts on %s" % router.name
- )
- # Safety net: never leave the daemon stopped.
- router.cmd("kill -CONT %s 2>/dev/null || true" % _ospf6d_pid(router))
- return _totals(), _passes()
-
-
-def _inject(h1, r1, r2, count, fwd, first_index, corrupt=False):
- h1.cmd_raises(
- "python3 %s/inject_lsu.py --iface eth1 --src-mac %s --dst-mac %s "
- "--src-ip %s --dst-ip %s --src-rid %s --adv-router %s --fwd %s "
- "--count %d --first-index %d %s"
- % (
- shlex.quote(CWD),
- _mac(r2),
- _mac(r1),
- R2_IP,
- R1_IP,
- R2_RID,
- R2_RID,
- fwd,
- count,
- first_index,
- "--corrupt" if corrupt else "",
- )
- )
-
-
-def _raise_dead_interval(router, iface, seconds=300):
- router.vtysh_cmd(
- "configure terminal\ninterface %s\nipv6 ospf6 dead-interval %d\nend"
- % (iface, seconds)
- )
-
-
-def test_adjacency_full():
- tgen = get_topogen()
- if tgen.routers_have_failure():
- pytest.skip(tgen.errors)
- r1 = tgen.gears["r1"]
- assert _wait_until(lambda: _neighbor_full(r1, R2_RID), timeout_s=90), (
- "r1-r2 adjacency did not reach Full"
- )
- assert _wait_until(lambda: _asbr_route_present(r1), timeout_s=30), (
- "r1 has no ASBR route to r2"
- )
-
-
-def test_same_fwd_scan_once_per_pass():
- """M same-fwd LSAs must cause at most one scan per ASE pass."""
- tgen = get_topogen()
- if tgen.routers_have_failure():
- pytest.skip(tgen.errors)
- r1 = tgen.gears["r1"]
- r2 = tgen.gears["r2"]
- h1 = tgen.gears["h1"]
- assert _wait_until(lambda: _neighbor_full(r1, R2_RID), timeout_s=90)
- assert _wait_until(lambda: _asbr_route_present(r1), timeout_s=30)
- if not _gdb_usable(r1):
- pytest.skip("gdb or the ospf6 source lines are unavailable")
-
- _raise_dead_interval(r1, "eth1")
- _raise_dead_interval(r2, "eth1")
- # Isolate r2 so it cannot fight back the forged LSAs; r1 keeps the
- # neighbor and the ASBR route inside the dead interval.
- r2.cmd_raises("ip link set dev eth1 down")
- assert _wait_until(lambda: _neighbor_full(r1, R2_RID), timeout_s=10), (
- "r1 lost the r2 neighbor after isolation"
- )
- assert _wait_until(lambda: _asbr_route_present(r1), timeout_s=10), (
- "r1 lost the ASBR route after isolation"
- )
-
- ids = [lsa_id(index) for index in range(1, BATCH + 1)]
- assert not any(lsa_id in _external_ids(r1) for lsa_id in ids)
- _inject(h1, r1, r2, BATCH, FWD, 1)
- assert _wait_until(
- lambda: all(ls_id in _external_ids(r1) for ls_id in ids), timeout_s=15
- ), "the 16 injected LSAs did not reach the r1 LSDB"
-
- gdb_pid, gdb_log = _gdb_start(r1, tgen.logdir)
- try:
- # Two link toggles force two SPF runs, hence two ASE passes.
- r1.cmd_raises("ip link set dev eth2 down")
- time.sleep(3)
- r1.cmd_raises("ip link set dev eth2 up")
- time.sleep(3)
- finally:
- (calls, visits), passes = _gdb_stop(r1, gdb_pid, gdb_log)
-
- logger.info(
- "forward-address check: calls=%d visits=%d passes=%s", calls, visits, passes
- )
- batch_passes = [p for p in passes if p[0] == BATCH]
- assert batch_passes, "no ASE pass processed the injected batch"
- # Each pass must scan the shared forwarding address at most once
- # (2 interfaces before the toggle, 1 while eth2 is down), not once
- # per LSA.
- for pass_calls, pass_visits in batch_passes:
- assert pass_visits <= 2, (
- "ASE pass scanned %d interface entries for %d same-address LSAs; "
- "the forwarding-address verdict must be cached per pass"
- % (pass_visits, pass_calls)
- )
- # All 16 LSAs were still processed exactly once per pass.
- assert calls >= BATCH
- assert _wait_until(
- lambda: all(ls_id in _external_ids(r1) for ls_id in ids), timeout_s=10
- ), "injected LSAs vanished from the LSDB"
-
-
-def test_zero_fwd_and_corrupt_controls():
- """Unspecified forwarding address and corrupt LSAs must not be scanned."""
- tgen = get_topogen()
- if tgen.routers_have_failure():
- pytest.skip(tgen.errors)
- r1 = tgen.gears["r1"]
- r2 = tgen.gears["r2"]
- h1 = tgen.gears["h1"]
- assert _wait_until(lambda: _neighbor_full(r1, R2_RID), timeout_s=90)
- if not _gdb_usable(r1):
- pytest.skip("gdb or the ospf6 source lines are unavailable")
-
- zero_ids = [lsa_id(index) for index in range(101, 109)]
- gdb_pid, gdb_log = _gdb_start(r1, tgen.logdir)
- try:
- _inject(h1, r1, r2, 8, "::", 101)
- time.sleep(3)
- finally:
- (calls, visits), _ = _gdb_stop(r1, gdb_pid, gdb_log)
- assert calls == 0 and visits == 0, (
- "unspecified forwarding address must skip the locality scan "
- "(calls=%d visits=%d)" % (calls, visits)
- )
- assert all(ls_id in _external_ids(r1) for ls_id in zero_ids), (
- "unspecified-fwd LSAs must be installed normally"
- )
-
- bad_id = lsa_id(201)
- gdb_pid, gdb_log = _gdb_start(r1, tgen.logdir)
- try:
- _inject(h1, r1, r2, 1, FWD, 201, corrupt=True)
- time.sleep(3)
- finally:
- (calls, visits), _ = _gdb_stop(r1, gdb_pid, gdb_log)
- assert calls == 0 and visits == 0, (
- "checksum-invalid LSA must be rejected before the scan (calls=%d visits=%d)"
- % (calls, visits)
- )
- assert bad_id not in _external_ids(r1)
diff --git a/tests/topotests/ospf_ase_forward_scan/__init__.py b/tests/topotests/ospf_ase_forward_scan/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/topotests/ospf_ase_forward_scan/inject_lsu.py b/tests/topotests/ospf_ase_forward_scan/inject_lsu.py
deleted file mode 100644
index 8c5e452352..0000000000
--- a/tests/topotests/ospf_ase_forward_scan/inject_lsu.py
+++ /dev/null
@@ -1,158 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: ISC
-#
-# inject_lsu.py
-#
-# Copyright (c) 2026 ATCorp
-#
-"""Build and send raw OSPF Link State Update packets.
-
-Used by test_ospf_ase_forward_scan.py to inject AS-external LSAs with a
-chosen forwarding address, bypassing the well-formedness of high-level
-packet helpers.
-"""
-
-import argparse
-import socket
-import struct
-import sys
-import time
-
-from scapy.all import Ether, IP, Raw, conf, sendp
-
-conf.verb = 0
-
-# 20-byte LSA header + 16-byte AS-external body; 16 LSAs stay under MTU 1500.
-LSA_CHUNK = 16
-
-
-def lsa_id(index):
- return "198.18.%d.%d" % (index // 256, index % 256)
-
-
-def fletcher_checksum(lsa):
- data = bytearray(lsa[2:])
- data[14:16] = b"\x00\x00"
- c0 = 0
- c1 = 0
- for value in data:
- c0 = (c0 + value) % 255
- c1 = (c1 + c0) % 255
- x = ((len(data) - 14 - 1) * c0 - c1) % 255
- if x == 0:
- x = 255
- y = 510 - c0 - x
- if y > 255:
- y -= 255
- data[14:16] = bytes((x, y))
- valid = bytearray(data)
- c0 = 0
- c1 = 0
- for value in valid:
- c0 = (c0 + value) % 255
- c1 = (c1 + c0) % 255
- if (c1 << 8) + c0 != 0:
- raise ValueError("invalid generated LSA Fletcher checksum")
- return (x << 8) | y
-
-
-def internet_checksum(data):
- if len(data) % 2:
- data += b"\x00"
- total = sum(struct.unpack("!%dH" % (len(data) // 2), data))
- total = (total & 0xFFFF) + (total >> 16)
- total = (total & 0xFFFF) + (total >> 16)
- return (~total) & 0xFFFF
-
-
-def make_external_lsa(index, adv_router, fwd, corrupt):
- body = (
- socket.inet_aton("255.255.255.255")
- + bytes((0x00,))
- + (10).to_bytes(3, "big")
- + socket.inet_aton(fwd)
- + (0).to_bytes(4, "big")
- )
- lsa = bytearray(
- struct.pack(
- "!HBB4s4sIHH",
- 1,
- 0x02,
- 5,
- socket.inet_aton(lsa_id(index)),
- socket.inet_aton(adv_router),
- 0x80000001,
- 0,
- 36,
- )
- + body
- )
- struct.pack_into("!H", lsa, 16, fletcher_checksum(lsa))
- if corrupt:
- lsa[17] ^= 0x5A
- return bytes(lsa)
-
-
-def make_lsu(lsa_blobs, src_rid):
- body = struct.pack("!I", len(lsa_blobs)) + b"".join(lsa_blobs)
- packet = bytearray(
- struct.pack(
- "!BBH4s4sHH8s",
- 2,
- 4,
- 24 + len(body),
- socket.inet_aton(src_rid),
- socket.inet_aton("0.0.0.0"),
- 0,
- 0,
- b"\x00" * 8,
- )
- + body
- )
- struct.pack_into("!H", packet, 12, internet_checksum(bytes(packet)))
- if internet_checksum(bytes(packet)) != 0:
- raise ValueError("invalid generated OSPF checksum")
- return bytes(packet)
-
-
-def main():
- parser = argparse.ArgumentParser()
- parser.add_argument("--iface", required=True)
- parser.add_argument("--src-mac", required=True)
- parser.add_argument("--dst-mac", required=True)
- parser.add_argument("--src-ip", required=True)
- parser.add_argument("--dst-ip", required=True)
- parser.add_argument("--src-rid", required=True)
- parser.add_argument("--adv-router", required=True)
- parser.add_argument("--fwd", required=True)
- parser.add_argument("--count", type=int, default=16)
- parser.add_argument("--first-index", type=int, default=1)
- parser.add_argument("--corrupt", action="store_true")
- parser.add_argument("--spacing", type=float, default=0.2)
- args = parser.parse_args()
-
- blobs = [
- make_external_lsa(index, args.adv_router, args.fwd, args.corrupt)
- for index in range(args.first_index, args.first_index + args.count)
- ]
- packets = [
- make_lsu(blobs[off : off + LSA_CHUNK], args.src_rid)
- for off in range(0, len(blobs), LSA_CHUNK)
- ]
- for index, payload in enumerate(packets):
- if index:
- time.sleep(args.spacing)
- frame = (
- Ether(src=args.src_mac, dst=args.dst_mac)
- / IP(src=args.src_ip, dst=args.dst_ip, ttl=1, proto=89)
- / Raw(payload)
- )
- sendp(frame, iface=args.iface, count=1, verbose=False)
- print(
- "OA2_INJECT packets=%d lsas=%d fwd=%s corrupt=%s first=%d"
- % (len(packets), args.count, args.fwd, args.corrupt, args.first_index)
- )
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/tests/topotests/ospf_ase_forward_scan/r1/frr.conf b/tests/topotests/ospf_ase_forward_scan/r1/frr.conf
deleted file mode 100644
index 56273548ea..0000000000
--- a/tests/topotests/ospf_ase_forward_scan/r1/frr.conf
+++ /dev/null
@@ -1,18 +0,0 @@
-!
-hostname r1
-!
-debug ospf lsa
-!
-interface eth1
- ip address 10.0.0.1/24
- ip ospf area 0
-!
-interface eth2
- ip address 10.0.1.1/24
- ip ospf area 0
- ip ospf passive
-!
-router ospf
- ospf router-id 1.1.1.1
- log-adjacency-changes detail
-!
diff --git a/tests/topotests/ospf_ase_forward_scan/r2/frr.conf b/tests/topotests/ospf_ase_forward_scan/r2/frr.conf
deleted file mode 100644
index d8f3674df1..0000000000
--- a/tests/topotests/ospf_ase_forward_scan/r2/frr.conf
+++ /dev/null
@@ -1,15 +0,0 @@
-!
-hostname r2
-!
-interface lo
- ip address 10.9.9.9/32
-!
-interface eth1
- ip address 10.0.0.2/24
- ip ospf area 0
-!
-router ospf
- ospf router-id 2.2.2.2
- redistribute connected metric 10 metric-type 1
- log-adjacency-changes detail
-!
diff --git a/tests/topotests/ospf_ase_forward_scan/test_ospf_ase_forward_scan.py b/tests/topotests/ospf_ase_forward_scan/test_ospf_ase_forward_scan.py
deleted file mode 100644
index 01a760d6f0..0000000000
--- a/tests/topotests/ospf_ase_forward_scan/test_ospf_ase_forward_scan.py
+++ /dev/null
@@ -1,441 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 eval: (blacken-mode 1) -*-
-# SPDX-License-Identifier: ISC
-#
-# test_ospf_ase_forward_scan.py
-#
-# Copyright (c) 2026 ATCorp
-#
-
-"""
-test_ospf_ase_forward_scan.py
-=============================
-
-Regression for per-LSA repeated forwarding-address locality scans in the
-OSPFv2 AS-external route calculation (RFC 2328 section 16.4).
-
-Every AS-external-LSA with a non-zero forwarding address makes
-ospf_ase_calculate_route() call ospf_ase_forward_address_check(), which
-walks the whole oiflist. A batch of M external LSAs that carry the same
-non-local forwarding address therefore re-scans the identical address M
-times per ASE calculation pass, and again on every later pass. The
-interface list cannot change while a pass runs, so the verdict must be
-cached per address for the duration of the pass.
-
-Topology
---------
-
- r1 (RID 1.1.1.1, victim) ---- s1 ---- r2 (RID 2.2.2.2, ASBR)
- | |
- h1 (scapy injector) (isolated after baseline)
- |
- s2 (stub, forces SPF passes via link toggle)
-
-Test plan
----------
-
-1. test_adjacency_full
- Baseline: LAN adjacency reaches Full and the ASBR route exists.
-
-2. test_same_fwd_scan_once_per_pass
- r2 is isolated (link down, within the raised dead interval) and h1
- injects 16 AS-external LSAs with distinct LS IDs and the same
- non-local forwarding address. gdb counts the scan-loop iterations
- on r1 while an r1 link toggle forces ASE passes. Expect at most
- one interface scan per pass, not one per LSA.
-
-3. test_zero_fwd_and_corrupt_controls
- Same-shaped LSUs with forwarding address 0.0.0.0 (no scan, LSAs
- installed) and with a broken Fletcher checksum (rejected, no scan).
-
-The gdb-based scan counts are the primary observable; tests 2-3 skip when
-gdb or the required source lines are unavailable.
-"""
-
-import json
-import os
-import re
-import shlex
-import sys
-import time
-from functools import partial
-
-import pytest
-
-from lib.topogen import Topogen, get_topogen
-from lib.topolog import logger
-
-
-CWD = os.path.dirname(os.path.realpath(__file__))
-sys.path.append(os.path.join(CWD, "../"))
-
-pytestmark = [pytest.mark.ospfd]
-
-R1_RID = "1.1.1.1"
-R2_RID = "2.2.2.2"
-R1_IP = "10.0.0.1"
-R2_IP = "10.0.0.2"
-H1_IP = "10.0.0.3"
-FWD = "10.99.99.99"
-LSA_ID_PREFIX = "198.18."
-BATCH = 16
-
-GDB_SCRIPT = """set pagination off
-set confirm off
-handle SIGINT stop print nopass
-set $calls = 0
-set $visits = 0
-set $pcalls = 0
-set $pvisits = 0
-break ospf_ase.c:%(call)d
-commands
- silent
- set $calls = $calls + 1
- set $pcalls = $pcalls + 1
- continue
-end
-break ospf_ase.c:%(loop)d
-commands
- silent
- set $visits = $visits + 1
- set $pvisits = $pvisits + 1
- continue
-end
-break ospf_ase_calculate_timer
-commands
- silent
- set $pcalls = 0
- set $pvisits = 0
- continue
-end
-break ospf_ase.c:%(exit)d
-commands
- silent
- printf "OA2_PASS calls=%%d visits=%%d\\n", $pcalls, $pvisits
- continue
-end
-continue
-printf "OA2_TOTAL calls=%%d visits=%%d\\n", $calls, $visits
-detach
-quit
-"""
-
-
-def lsa_id(index):
- return "198.18.%d.%d" % (index // 256, index % 256)
-
-
-def build_topo(tgen):
- r1 = tgen.add_router("r1")
- r2 = tgen.add_router("r2")
- h1 = tgen.add_host("h1", H1_IP + "/24", "via " + R1_IP)
- s1 = tgen.add_switch("s1")
- s1.add_link(r1, nodeif="eth1")
- s1.add_link(r2, nodeif="eth1")
- s1.add_link(h1, nodeif="eth1")
- s2 = tgen.add_switch("s2")
- s2.add_link(r1, nodeif="eth2")
-
-
-def setup_module(mod):
- 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():
- tgen = get_topogen()
- tgen.stop_topology()
-
-
-def _vty_json(router, command):
- return json.loads(router.vtysh_cmd(command))
-
-
-def _wait_until(func, timeout_s=20, interval_s=0.5):
- deadline = time.time() + timeout_s
- while time.time() < deadline:
- if func():
- return True
- time.sleep(interval_s)
- return bool(func())
-
-
-def _neighbor_full(router, rid):
- try:
- data = _vty_json(router, "show ip ospf neighbor json")
- except (json.JSONDecodeError, ValueError):
- return False
- entries = data.get("neighbors", {}).get(rid, [])
- return bool(entries) and entries[0].get("converged") == "Full"
-
-
-def _asbr_route_present(router):
- return R2_RID in router.vtysh_cmd("show ip ospf border-routers")
-
-
-def _external_ids(router):
- data = _vty_json(router, "show ip ospf database external json")
- ids = set()
-
- def walk(value):
- if isinstance(value, dict):
- if (value.get("linkStateId", "").startswith(LSA_ID_PREFIX)
- and value.get("advertisingRouter") == R2_RID):
- ids.add(value["linkStateId"])
- for child in value.values():
- walk(child)
- elif isinstance(value, list):
- for child in value:
- walk(child)
-
- walk(data)
- return ids
-
-
-def _mac(gear):
- return gear.cmd("cat /sys/class/net/eth1/address").strip()
-
-
-def _source_lines():
- """Locate the breakpoint lines in ospfd/ospf_ase.c at runtime.
-
- The scan-call site, the scan-loop body and the ASE-pass exit marker are
- found by pattern so the same test works on any tree that contains the
- check, including this fix (which shifts line numbers).
- """
- source = os.path.join(CWD, "..", "..", "..", "ospfd", "ospf_ase.c")
- patterns = {
- "call": r"if \(!ospf_ase_forward_address_check\(ospf,",
- "loop": r"if \(if_is_operative\(oi->ifp\)\)",
- "exit": r"if \(ospf->gr_info\.finishing_restart\) \{",
- }
- lines = {}
- with open(source) as fp:
- content = fp.readlines()
- for key, pattern in patterns.items():
- for lineno, text in enumerate(content, 1):
- if re.search(pattern, text):
- lines[key] = lineno
- break
- else:
- raise LookupError("pattern %r not found in ospf_ase.c" % pattern)
- return lines
-
-
-def _gdb_usable(router):
- if not router.cmd("which gdb || true").strip():
- return False
- try:
- _source_lines()
- except (OSError, LookupError):
- return False
- return True
-
-
-def _ospfd_pid(router):
- # pidof is ambiguous: all routers share the pid namespace and run
- # identical ospfd cmdlines. The pid file lives in the router's own
- # mount namespace and names exactly this router's ospfd.
- return router.cmd("cat /var/run/frr/ospfd.pid").strip().splitlines()[-1]
-
-
-def _gdb_start(router, logdir):
- script = os.path.join(logdir, "oa2_count.gdb")
- log = os.path.join(logdir, "oa2_count.log")
- with open(script, "w") as fp:
- fp.write(GDB_SCRIPT % _source_lines())
- router.cmd("rm -f %s" % shlex.quote(log))
- pid = router.cmd(
- "nohup gdb -q -batch -x %s -p %s > %s 2>&1 </dev/null & echo $!"
- % (shlex.quote(script), _ospfd_pid(router), shlex.quote(log))
- ).strip()
- pid = pid.splitlines()[-1]
- assert pid.isdigit(), "gdb did not start on %s (pid %r)" % (router.name, pid)
- assert _wait_until(
- lambda: os.path.exists(log)
- and "Breakpoint 4 at" in open(log, errors="replace").read()
- ), "gdb breakpoints not ready on %s" % router.name
- return pid, log
-
-
-def _gdb_stop(router, pid, log):
- router.cmd("kill -INT %s >/dev/null 2>&1 || true" % pid)
-
- def _totals():
- if not os.path.exists(log):
- return None
- match = re.search(
- r"OA2_TOTAL calls=(\d+) visits=(\d+)", open(log, errors="replace").read()
- )
- return (int(match.group(1)), int(match.group(2))) if match else None
-
- def _passes():
- if not os.path.exists(log):
- return []
- return [
- (int(m.group(1)), int(m.group(2)))
- for m in re.finditer(
- r"OA2_PASS calls=(\d+) visits=(\d+)",
- open(log, errors="replace").read(),
- )
- ]
-
- assert _wait_until(lambda: _totals() is not None, timeout_s=15), (
- "gdb did not report counts on %s" % router.name
- )
- # Safety net: never leave the daemon stopped.
- router.cmd("kill -CONT %s 2>/dev/null || true" % _ospfd_pid(router))
- return _totals(), _passes()
-
-
-def _inject(h1, r1, r2, count, fwd, first_index, corrupt=False):
- h1.cmd_raises(
- "python3 %s/inject_lsu.py --iface eth1 --src-mac %s --dst-mac %s "
- "--src-ip %s --dst-ip %s --src-rid %s --adv-router %s --fwd %s "
- "--count %d --first-index %d %s"
- % (
- shlex.quote(CWD),
- _mac(r2),
- _mac(r1),
- R2_IP,
- R1_IP,
- R2_RID,
- R2_RID,
- fwd,
- count,
- first_index,
- "--corrupt" if corrupt else "",
- )
- )
-
-
-def _raise_dead_interval(router, iface, seconds=300):
- router.vtysh_cmd(
- "configure terminal\ninterface %s\nip ospf dead-interval %d\nend"
- % (iface, seconds)
- )
-
-
-def test_adjacency_full():
- tgen = get_topogen()
- if tgen.routers_have_failure():
- pytest.skip(tgen.errors)
- r1 = tgen.gears["r1"]
- assert _wait_until(lambda: _neighbor_full(r1, R2_RID), timeout_s=90), (
- "r1-r2 adjacency did not reach Full"
- )
- assert _wait_until(lambda: _asbr_route_present(r1), timeout_s=30), (
- "r1 has no ASBR route to r2"
- )
-
-
-def test_same_fwd_scan_once_per_pass():
- """M same-fwd LSAs must cause at most one scan per ASE pass."""
- tgen = get_topogen()
- if tgen.routers_have_failure():
- pytest.skip(tgen.errors)
- r1 = tgen.gears["r1"]
- r2 = tgen.gears["r2"]
- h1 = tgen.gears["h1"]
- assert _wait_until(lambda: _neighbor_full(r1, R2_RID), timeout_s=90)
- assert _wait_until(lambda: _asbr_route_present(r1), timeout_s=30)
- if not _gdb_usable(r1):
- pytest.skip("gdb or the ospf_ase.c source lines are unavailable")
-
- _raise_dead_interval(r1, "eth1")
- _raise_dead_interval(r2, "eth1")
- # Isolate r2 so it cannot fight back the forged LSAs; r1 keeps the
- # neighbor and the ASBR route inside the dead interval.
- r2.cmd_raises("ip link set dev eth1 down")
- assert _wait_until(lambda: _neighbor_full(r1, R2_RID), timeout_s=10), (
- "r1 lost the r2 neighbor after isolation"
- )
- assert _wait_until(lambda: _asbr_route_present(r1), timeout_s=10), (
- "r1 lost the ASBR route after isolation"
- )
-
- ids = [lsa_id(index) for index in range(1, BATCH + 1)]
- assert not any(lsa_id in _external_ids(r1) for lsa_id in ids)
- _inject(h1, r1, r2, BATCH, FWD, 1)
- assert _wait_until(
- lambda: all(ls_id in _external_ids(r1) for ls_id in ids), timeout_s=15
- ), "the 16 injected LSAs did not reach the r1 LSDB"
-
- gdb_pid, gdb_log = _gdb_start(r1, tgen.logdir)
- try:
- # Two link toggles force two SPF runs, hence two ASE passes.
- r1.cmd_raises("ip link set dev eth2 down")
- time.sleep(3)
- r1.cmd_raises("ip link set dev eth2 up")
- time.sleep(3)
- finally:
- (calls, visits), passes = _gdb_stop(r1, gdb_pid, gdb_log)
-
- logger.info(
- "forward-address check: calls=%d visits=%d passes=%s", calls, visits, passes
- )
- batch_passes = [p for p in passes if p[0] == BATCH]
- assert batch_passes, "no ASE pass processed the injected batch"
- # Each pass must scan the shared forwarding address at most once
- # (2 interfaces before the toggle, 1 while eth2 is down), not once
- # per LSA.
- for pass_calls, pass_visits in batch_passes:
- assert pass_visits <= 2, (
- "ASE pass scanned %d interface entries for %d same-address LSAs; "
- "the forwarding-address verdict must be cached per pass"
- % (pass_visits, pass_calls)
- )
- # All 16 LSAs were still processed exactly once per pass.
- assert calls >= BATCH
- # The LSDB presence (asserted above) plus the per-pass call counts show
- # the target processed every injected LSA; the scan counts are the
- # evidence for the deduplication requirement.
- assert _wait_until(
- lambda: all(ls_id in _external_ids(r1) for ls_id in ids), timeout_s=10
- ), "injected LSAs vanished from the LSDB"
-
-
-def test_zero_fwd_and_corrupt_controls():
- """Zero forwarding address and corrupt LSAs must not reach the scan."""
- tgen = get_topogen()
- if tgen.routers_have_failure():
- pytest.skip(tgen.errors)
- r1 = tgen.gears["r1"]
- r2 = tgen.gears["r2"]
- h1 = tgen.gears["h1"]
- assert _wait_until(lambda: _neighbor_full(r1, R2_RID), timeout_s=90)
- if not _gdb_usable(r1):
- pytest.skip("gdb or the ospf_ase.c source lines are unavailable")
-
- zero_ids = [lsa_id(index) for index in range(101, 109)]
- gdb_pid, gdb_log = _gdb_start(r1, tgen.logdir)
- try:
- _inject(h1, r1, r2, 8, "0.0.0.0", 101)
- time.sleep(3)
- finally:
- (calls, visits), _ = _gdb_stop(r1, gdb_pid, gdb_log)
- assert calls == 0 and visits == 0, (
- "zero forwarding address must skip the locality scan (calls=%d visits=%d)"
- % (calls, visits)
- )
- assert all(ls_id in _external_ids(r1) for ls_id in zero_ids), (
- "zero-fwd LSAs must be installed normally"
- )
-
- bad_id = lsa_id(201)
- gdb_pid, gdb_log = _gdb_start(r1, tgen.logdir)
- try:
- _inject(h1, r1, r2, 1, FWD, 201, corrupt=True)
- time.sleep(3)
- finally:
- (calls, visits), _ = _gdb_stop(r1, gdb_pid, gdb_log)
- assert calls == 0 and visits == 0, (
- "checksum-invalid LSA must be rejected before the scan (calls=%d visits=%d)"
- % (calls, visits)
- )
- assert bad_id not in _external_ids(r1)