diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md
index 823d7b68a..69fba0ed1 100644
--- a/doc/ChangeLog.md
+++ b/doc/ChangeLog.md
@@ -10,6 +10,9 @@ All notable changes to the project are documented in this file.
- Upgrade Linux kernel to 6.18.49 (LTS)
- Upgrade FRR to 10.5.5
+- Add IPv6 dynamic routing: RIPng and OSPFv3. Both reuse the existing
+ ietf-rip and ietf-ospf models, selected per control-plane-protocol by the
+ `ripng`/`ospfv3` type and the IPv6 address-family
[v26.08.0][] - 2026-09-01
-------------------------
diff --git a/doc/routing.md b/doc/routing.md
index f8b99e588..de7f3efd8 100644
--- a/doc/routing.md
+++ b/doc/routing.md
@@ -7,8 +7,8 @@ Currently supported YANG models:
| ietf-routing | Base model for all other models |
| ietf-ipv4-unicast-routing | Static IPv4 unicast routing |
| ietf-ipv6-unicast-routing | Static IPv6 unicast routing |
-| ietf-ospf | OSPF routing |
-| ietf-rip | RIP routing |
+| ietf-ospf | OSPFv2 and OSPFv3 routing |
+| ietf-rip | RIPv2 and RIPng routing |
| infix-routing | Infix deviations and extensions |
The base model, ietf-routing, is where all the other models hook in. It
@@ -305,6 +305,60 @@ admin@example:/>
+## OSPFv3 Routing
+
+The system also supports OSPF for IPv6, i.e., OSPFv3. It uses the same
+`ietf-ospf` model as OSPFv2, selected by the control plane protocol type
+`ospfv3`. OSPFv3 has no IPv4 interface address to derive a router ID
+from, so an `explicit-router-id` must be set.
+
+
admin@example:/config/> edit routing control-plane-protocol ospfv3 name default ospf
+admin@example:/config/routing/…/ospf/> set explicit-router-id 1.1.1.1
+admin@example:/config/routing/…/ospf/> set area 0.0.0.0 interface e0 enabled
+admin@example:/config/routing/…/ospf/> leave
+admin@example:/>
+
+
+> [!TIP]
+> Remember to enable [IPv6 forwarding](ip.md#ipv6-forwarding) for all the
+> interfaces you want to route between.
+
+Areas, interface settings (cost, timers, BFD, `point-to-point`),
+redistribution and default route advertisement work the same way as for
+[OSPFv2](#ospfv2-routing), with the differences listed below.
+
+
+### Differences from OSPFv2
+
+The following OSPFv3 settings differ from OSPFv2:
+
+- OSPFv3 carries IPv6 routes only. IPv4 over OSPFv3 (RFC 5838) is not
+ supported, hence there is no `address-family` setting.
+- `explicit-router-id` is required, there is no IPv4 interface address to
+ derive a router ID from.
+- Interface types are `broadcast`, `point-to-point` and multicast
+ Point-to-Multipoint (`hybrid`). Non-broadcast interfaces and static
+ neighbors are not supported.
+- NSSA areas are regular NSSA, the `summary` flag applies to stub areas
+ only, where `summary false` gives a totally-stubby area.
+- Per-area `default-cost` is not available.
+
+
+### OSPFv3 status
+
+The CLI mirrors the OSPFv2 commands under `show ipv6 ospf`:
+
+
admin@example:/> show ipv6 ospf
+admin@example:/> show ipv6 ospf neighbor
+admin@example:/> show ipv6 ospf interface
+admin@example:/> show ipv6 ospf route
+
+
+Debug logging is configured under the `ospfv3` protocol `debug` container,
+analogous to OSPFv2. Available categories are `packet`, `ism`, `nsm` and
+`nssa`.
+
+
## RIP Routing
The system supports RIP dynamic routing for IPv4, i.e., RIPv2. To enable
@@ -425,6 +479,51 @@ admin@example:/>
+## RIPng Routing
+
+RIPng is RIP for IPv6. It uses the same `ietf-rip` model as RIPv2,
+selected by the control plane protocol type `ripng`:
+
+
admin@example:/config/> edit routing control-plane-protocol ripng name default rip
+admin@example:/config/routing/…/rip/> set interfaces interface e0
+admin@example:/config/routing/…/rip/> set interfaces interface e1
+admin@example:/config/routing/…/rip/> leave
+admin@example:/>
+
+
+> [!TIP]
+> Remember to enable [IPv6 forwarding](ip.md#ipv6-forwarding) for all the
+> interfaces you want to route between.
+
+Passive interfaces, redistribution (`set redistribute connected` /
+`static`) and timers are configured the same way as for
+[RIPv2](#rip-routing), with the differences below.
+
+
+### Differences from RIPv2
+
+RIPng runs on FRR's `ripngd`:
+
+- RIPng has no protocol version, so the per-interface `send-version` and
+ `receive-version` settings do not apply.
+- The global `distance` and unicast `neighbor` settings are not supported
+ (`ripngd` has no such commands).
+- `set redistribute ospf` redistributes OSPFv3, as there is no OSPFv2 in
+ an IPv6 domain.
+
+
+### RIPng status
+
+RIPng-learned routes appear in the [IPv6 routing table](#ipv6-routing-table):
+
+
admin@example:/> show ipv6 route
+
+
+Debug logging is configured under the `ripng` protocol `debug` container,
+analogous to RIPv2. Available categories are `events`, `packet` and
+`kernel`.
+
+
## View routing table
The routing table can be inspected from the operational datastore, XPath
@@ -500,7 +599,8 @@ Default distances used (lower numeric value wins):
| 0 | Kernel routes, i.e., connected routes |
| 1 | Static routes |
| 5 | DHCP routes |
-| 110 | OSPF |
+| 110 | OSPF (OSPFv2 and OSPFv3) |
+| 120 | RIP (RIPv2 and RIPng) |
| 254 | IPv4LL (ZeroConf) device routes |
| 255 | Route will not be used or redistributed |
@@ -527,6 +627,8 @@ The source protocol describes the origin of the route.
| kernel | Added when setting a subnet address on an interface |
| static | User created, learned from DHCP, or IPv4LL |
| ospfv2 | Routes learned from OSPFv2 |
+| ospfv3 | Routes learned from OSPFv3 |
+| rip | Routes learned from RIPv2 or RIPng |
The YANG model *ietf-routing* support multiple ribs but only two are
currently supported, namely `ipv4` and `ipv6`.
diff --git a/package/skeleton-init-finit/skeleton-init-finit.mk b/package/skeleton-init-finit/skeleton-init-finit.mk
index 8cc9f1000..036451228 100644
--- a/package/skeleton-init-finit/skeleton-init-finit.mk
+++ b/package/skeleton-init-finit/skeleton-init-finit.mk
@@ -92,7 +92,7 @@ define SKELETON_INIT_FINIT_SET_FRR
endef
else
define SKELETON_INIT_FINIT_SET_FRR
- for svc in babeld bfdd bgpd mgmtd eigrpd isisd ldpd ospfd ospf6d pathd ripd ripng staticd vrrpd zebra; do \
+ for svc in babeld bfdd bgpd mgmtd eigrpd isisd ldpd ospfd ospf6d pathd ripd ripngd staticd vrrpd zebra; do \
cp $(SKELETON_INIT_FINIT_AVAILABLE)/frr/$$svc.conf $(FINIT_D)/available/$$svc.conf; \
done
ln -sf ../available/zebra.conf $(FINIT_D)/enabled/zebra.conf
diff --git a/package/skeleton-init-finit/skeleton/etc/default/ospf6d b/package/skeleton-init-finit/skeleton/etc/default/ospf6d
new file mode 100644
index 000000000..e67c64a8b
--- /dev/null
+++ b/package/skeleton-init-finit/skeleton/etc/default/ospf6d
@@ -0,0 +1,2 @@
+# --log-level debug
+OSPF6D_ARGS="-A 127.0.0.1 -u frr -g frr -f /etc/frr/ospf6d.conf --log syslog"
diff --git a/package/skeleton-init-finit/skeleton/etc/default/ripngd b/package/skeleton-init-finit/skeleton/etc/default/ripngd
new file mode 100644
index 000000000..e61ff21d3
--- /dev/null
+++ b/package/skeleton-init-finit/skeleton/etc/default/ripngd
@@ -0,0 +1,2 @@
+# --log-level debug
+RIPNGD_ARGS="-A 127.0.0.1 -u frr -g frr --log syslog"
diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospf6d.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospf6d.conf
index a31eaf4cb..11dd09036 100644
--- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospf6d.conf
+++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospf6d.conf
@@ -1 +1,3 @@
-service [2345] log:null ospf6d -A 127.0.0.1 -u frr -g frr -- OSPF IPv6 daemon
+service pid:!/run/frr/ospf6d.pid env:-/etc/default/ospf6d \
+ [2345] ospf6d $OSPF6D_ARGS \
+ -- OSPF IPv6 daemon
diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripng.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripng.conf
deleted file mode 100644
index 5d80f4ca0..000000000
--- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripng.conf
+++ /dev/null
@@ -1 +0,0 @@
-service [2345] log:null ripngd -A 127.0.0.1 -u frr -g frr -- RIP IPv6 daemon
diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripngd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripngd.conf
new file mode 100644
index 000000000..57257f4ed
--- /dev/null
+++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripngd.conf
@@ -0,0 +1,3 @@
+service pid:!/run/frr/ripngd.pid env:-/etc/default/ripngd \
+ [2345] ripngd $RIPNGD_ARGS \
+ -- RIPng daemon
diff --git a/src/bin/show/__init__.py b/src/bin/show/__init__.py
index 4d240c4a9..b6d5b1e93 100755
--- a/src/bin/show/__init__.py
+++ b/src/bin/show/__init__.py
@@ -387,8 +387,10 @@ def bfd(args: List[str]) -> None:
print(f"Unknown BFD subcommand: {subcommand}")
-def ospf(args: List[str]) -> None:
- """Handle show ospf [subcommand] [ifname] [detail]
+def _ospf(args: List[str], afi: str) -> None:
+ """Handle show [ip|ipv6] ospf [subcommand] [ifname] [detail]
+
+ afi selects the address family: 'ipv4' (OSPFv2) or 'ipv6' (OSPFv3).
Subcommands:
(none) - General OSPF instance information
@@ -409,6 +411,9 @@ def ospf(args: List[str]) -> None:
print(json.dumps(data, indent=2))
return
+ # Tell the formatters which address family (OSPFv2 vs OSPFv3) to select.
+ data['_afi'] = afi
+
# Parse arguments: subcommand, optional interface name, optional detail flag
subcommand = args[0] if len(args) > 0 and args[0] else ""
@@ -454,6 +459,16 @@ def ospf(args: List[str]) -> None:
print(f"Unknown OSPF subcommand: {subcommand}")
+def ospf(args: List[str]) -> None:
+ """Show OSPFv2 (IPv4) operational status."""
+ _ospf(args, "ipv4")
+
+
+def ospf6(args: List[str]) -> None:
+ """Show OSPFv3 (IPv6) operational status."""
+ _ospf(args, "ipv6")
+
+
def rip(args: List[str]) -> None:
"""Handle show rip [subcommand] [ifname]
@@ -753,6 +768,7 @@ def execute_command(command: str, args: List[str]):
'nacm': nacm,
'ntp': ntp,
'ospf': ospf,
+ 'ospf6': ospf6,
'ptp': ptp,
'rip': rip,
'routes': routes,
diff --git a/src/confd/src/routing.c b/src/confd/src/routing.c
index c9b2a24d2..85d5845db 100644
--- a/src/confd/src/routing.c
+++ b/src/confd/src/routing.c
@@ -14,8 +14,13 @@
#define OSPFD_CONF "/etc/frr/ospfd.conf"
#define OSPFD_CONF_NEXT OSPFD_CONF "+"
#define OSPFD_CONF_PREV OSPFD_CONF "-"
+#define OSPF6D_CONF "/etc/frr/ospf6d.conf"
+#define OSPF6D_CONF_NEXT OSPF6D_CONF "+"
+#define OSPF6D_CONF_PREV OSPF6D_CONF "-"
#define RIPD_SIGNAL "/run/ripd_enabled"
#define RIPD_SIGNAL_NEXT RIPD_SIGNAL "+"
+#define RIPNGD_SIGNAL "/run/ripngd_enabled"
+#define RIPNGD_SIGNAL_NEXT RIPNGD_SIGNAL "+"
#define BFDD_SIGNAL "/run/bfd_enabled" /* Just signal that bfd should be enabled*/
#define BFDD_SIGNAL_NEXT BFDD_SIGNAL "+"
#define FRR_DAEMONS "/etc/frr/daemons"
@@ -29,14 +34,14 @@ no log unique-id\n\
log syslog warnings\n\
log facility local2\n"
-int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp)
+int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp, int ripng)
{
struct lyd_node *interfaces, *timers, *default_route, *interface, *tmp;
const char *default_metric, *distance;
int num_interfaces = 0;
- /* Generate libconfuse format for RIP */
- fputs("\nrip {\n", fp);
+ /* Generate libconfuse format for RIPv2 (rip) or RIPng (ripng) */
+ fputs(ripng ? "\nripng {\n" : "\nrip {\n", fp);
fputs("\tenabled = true\n", fp);
/* Global RIP parameters */
@@ -45,7 +50,7 @@ int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp)
fprintf(fp, "\tdefault-metric = %s\n", default_metric);
distance = lydx_get_cattr(rip, "distance");
- if (distance)
+ if (distance && !ripng) /* FRR ripngd has no 'distance' command */
fprintf(fp, "\tdistance = %s\n", distance);
/* Timers */
@@ -76,12 +81,14 @@ int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp)
/* Debug options - use system commands since FRR doesn't support via northbound */
struct lyd_node *debug = lydx_get_child(rip, "debug");
if (debug) {
+ const char *proto = ripng ? "ripng" : "rip";
+
if (lydx_get_bool(debug, "events"))
- fputs("\tsystem = \"vtysh -c 'debug rip events'\"\n", fp);
+ fprintf(fp, "\tsystem = \"vtysh -c 'debug %s events'\"\n", proto);
if (lydx_get_bool(debug, "packet"))
- fputs("\tsystem = \"vtysh -c 'debug rip packet'\"\n", fp);
+ fprintf(fp, "\tsystem = \"vtysh -c 'debug %s packet'\"\n", proto);
if (lydx_get_bool(debug, "kernel"))
- fputs("\tsystem = \"vtysh -c 'debug rip zebra'\"\n", fp);
+ fprintf(fp, "\tsystem = \"vtysh -c 'debug %s zebra'\"\n", proto);
}
/* Networks (interfaces) - output as list */
@@ -165,8 +172,13 @@ static const char *ospf_network_type(const char *yang_type)
return yang_type;
}
-int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp)
+/*
+ * Shared by OSPFv2 and OSPFv3, the only difference is the vtysh keyword
+ * prefix, 'ip ospf' vs. 'ipv6 ospf6'.
+ */
+static int parse_ospf_interfaces(struct lyd_node *areas, FILE *fp, int v3)
{
+ const char *ospf = v3 ? "ipv6 ospf6" : "ip ospf";
struct lyd_node *interface, *interfaces, *area;
int num_bfd_enabled = 0;
@@ -198,25 +210,28 @@ int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FIL
cost = lydx_get_cattr(interface, "cost");
priority = lydx_get_cattr(interface, "priority");
- fprintf(fp, " ip ospf area %s\n", area_id);
+ /* Set the network type before joining the area, ospf6d
+ * ignores a network type change on an interface that is
+ * already active in OSPF. */
+ if (interface_type)
+ fprintf(fp, " %s network %s\n", ospf, ospf_network_type(interface_type));
+ fprintf(fp, " %s area %s\n", ospf, area_id);
if (dead)
- fprintf(fp, " ip ospf dead-interval %s\n", dead);
+ fprintf(fp, " %s dead-interval %s\n", ospf, dead);
if (hello)
- fprintf(fp, " ip ospf hello-interval %s\n", hello);
+ fprintf(fp, " %s hello-interval %s\n", ospf, hello);
if (retransmit)
- fprintf(fp, " ip ospf retransmit-interval %s\n", retransmit);
+ fprintf(fp, " %s retransmit-interval %s\n", ospf, retransmit);
if (transmit)
- fprintf(fp, " ip ospf transmit-delay %s\n", transmit);
+ fprintf(fp, " %s transmit-delay %s\n", ospf, transmit);
if (priority)
- fprintf(fp, " ip ospf priority %s\n", priority);
+ fprintf(fp, " %s priority %s\n", ospf, priority);
if (bfd_enabled)
- fputs(" ip ospf bfd\n", fp);
+ fprintf(fp, " %s bfd\n", ospf);
if (passive)
- fputs(" ip ospf passive\n", fp);
- if (interface_type)
- fprintf(fp, " ip ospf network %s\n", ospf_network_type(interface_type));
+ fprintf(fp, " %s passive\n", ospf);
if (cost)
- fprintf(fp, " ip ospf cost %s\n", cost);
+ fprintf(fp, " %s cost %s\n", ospf, cost);
}
}
}
@@ -224,17 +239,21 @@ int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FIL
return num_bfd_enabled;
}
-int parse_ospf_redistribute(sr_session_ctx_t *session, struct lyd_node *redistributes, FILE *fp)
+static void parse_ospf_redistribute(struct lyd_node *redistributes, FILE *fp, int v3)
{
struct lyd_node *tmp;
LY_LIST_FOR(lyd_child(redistributes), tmp) {
const char *protocol = lydx_get_cattr(tmp, "protocol");
+ /* The redistribute enum is address family agnostic, for an IPv6
+ * IGP 'rip' means RIPng. Redistributing 'ospf' into OSPFv3 is
+ * rejected by YANG. */
+ if (v3 && !strcmp(protocol, "rip"))
+ protocol = "ripng";
+
fprintf(fp, " redistribute %s\n", protocol);
}
-
- return 0;
}
static void parse_ospf_static_neighbors(struct lyd_node *areas, FILE *fp)
@@ -344,14 +363,14 @@ int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf)
areas = lydx_get_child(ospf, "areas");
router_id = lydx_get_cattr(ospf, "explicit-router-id");
- bfd_enabled = parse_ospf_interfaces(session, areas, fp);
+ bfd_enabled = parse_ospf_interfaces(areas, fp, 0);
fputs("router ospf\n", fp);
num_areas = parse_ospf_areas(session, areas, fp);
- parse_ospf_redistribute(session, lydx_get_child(ospf, "redistribute"), fp);
+ parse_ospf_redistribute(lydx_get_child(ospf, "redistribute"), fp, 0);
parse_ospf_static_neighbors(areas, fp);
default_route = lydx_get_child(ospf, "default-route-advertise");
if (default_route) {
- /* enable is obsolete in favor for enabled. */
+ /* 'enable' is obsolete, superseded by 'enabled'. */
if ((lydx_get_child(default_route, "enable") && lydx_get_bool(default_route, "enable"))
|| lydx_get_bool(default_route, "enabled")) {
fputs(" default-information originate", fp);
@@ -370,10 +389,116 @@ int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf)
return 0;
}
+ /* Only ever set the shared BFDD signal here; routing_change resets it
+ * once per commit so multiple OSPF instances (v2 + v3) don't clobber
+ * each other's BFD request. */
+ if (bfd_enabled)
+ (void)touch(BFDD_SIGNAL_NEXT);
+
+ return 0;
+}
+
+static int parse_ospf6_areas(struct lyd_node *areas, FILE *fp)
+{
+ int areas_configured = 0;
+ struct lyd_node *area;
+
+ LY_LIST_FOR(lyd_child(areas), area) {
+ const char *area_id, *area_type;
+ int summary;
+
+ area_id = lydx_get_cattr(area, "area-id");
+ area_type = lydx_get_cattr(area, "area-type");
+ summary = lydx_get_bool(area, "summary");
+
+ if (area_type) {
+ /* ospf6d supports 'stub [no-summary]' and 'nssa'; it has
+ * no 'default-cost' and no totally-NSSA (nssa no-summary). */
+ if (!strcmp(area_type, "ietf-ospf:nssa-area"))
+ fprintf(fp, " area %s nssa\n", area_id);
+ else if (!strcmp(area_type, "ietf-ospf:stub-area"))
+ fprintf(fp, " area %s stub %s\n", area_id, !summary ? "no-summary" : "");
+ }
+ areas_configured++;
+ }
+
+ return areas_configured;
+}
+
+int parse_ospf6(sr_session_ctx_t *session, struct lyd_node *ospf)
+{
+ struct lyd_node *areas, *default_route, *debug;
+ const char *router_id;
+ int bfd_enabled = 0;
+ int num_areas = 0;
+ FILE *fp;
+
+ (void)session;
+
+ fp = fopen(OSPF6D_CONF_NEXT, "w");
+ if (!fp) {
+ ERRNO("Failed to open %s", OSPF6D_CONF_NEXT);
+ return SR_ERR_INTERNAL;
+ }
+
+ /* Handle OSPFv3 debug configuration. ospf6d debug categories differ
+ * from ospfd; bfd and default-information have no ospf6 equivalent. */
+ debug = lydx_get_child(ospf, "debug");
+ if (debug) {
+ int any_debug = 0;
+
+ if (lydx_get_bool(debug, "packet")) {
+ fputs("debug ospf6 message all\n", fp);
+ any_debug = 1;
+ }
+ if (lydx_get_bool(debug, "ism")) {
+ fputs("debug ospf6 interface\n", fp);
+ any_debug = 1;
+ }
+ if (lydx_get_bool(debug, "nsm")) {
+ fputs("debug ospf6 neighbor\n", fp);
+ any_debug = 1;
+ }
+ if (lydx_get_bool(debug, "nssa")) {
+ fputs("debug ospf6 nssa\n", fp);
+ any_debug = 1;
+ }
+
+ if (any_debug) {
+ fputs("log syslog debugging\n", fp);
+ fputs("!\n", fp);
+ }
+ }
+
+ areas = lydx_get_child(ospf, "areas");
+ router_id = lydx_get_cattr(ospf, "explicit-router-id");
+ bfd_enabled = parse_ospf_interfaces(areas, fp, 1);
+ fputs("router ospf6\n", fp);
+ num_areas = parse_ospf6_areas(areas, fp);
+ parse_ospf_redistribute(lydx_get_child(ospf, "redistribute"), fp, 1);
+ default_route = lydx_get_child(ospf, "default-route-advertise");
+ if (default_route) {
+ /* 'enable' is obsolete, superseded by 'enabled'. */
+ if ((lydx_get_child(default_route, "enable") && lydx_get_bool(default_route, "enable"))
+ || lydx_get_bool(default_route, "enabled")) {
+ fputs(" default-information originate", fp);
+ if (lydx_get_bool(default_route, "always"))
+ fputs(" always", fp);
+ fputs("\n", fp);
+ }
+ }
+
+ if (router_id)
+ fprintf(fp, " ospf6 router-id %s\n", router_id);
+ fclose(fp);
+
+ if (!num_areas) {
+ (void)remove(OSPF6D_CONF_NEXT);
+ return 0;
+ }
+
if (bfd_enabled)
(void)touch(BFDD_SIGNAL_NEXT);
- else
- (void)remove(BFDD_SIGNAL_NEXT);
return 0;
}
@@ -443,7 +568,7 @@ static int parse_static_routes(sr_session_ctx_t *session, struct lyd_node *paren
* Generate the complete /etc/frr/daemons file. Written atomically as a
* single unit so the file is always consistent and easy to read.
*/
-static void frr_daemons_write(int ospfd, int ripd, int bfdd)
+static void frr_daemons_write(int ospfd, int ospf6d, int ripd, int ripngd, int bfdd)
{
const char *next = FRR_DAEMONS "+";
FILE *fp;
@@ -459,11 +584,11 @@ static void frr_daemons_write(int ospfd, int ripd, int bfdd)
fprintf(fp,
"# Generated by Infix confd\n"
"ospfd=%s\n"
+ "ospf6d=%s\n"
"ripd=%s\n"
+ "ripngd=%s\n"
"bfdd=%s\n"
"bgpd=no\n"
- "ospf6d=no\n"
- "ripngd=no\n"
"isisd=no\n"
"pimd=no\n"
"pim6d=no\n"
@@ -473,9 +598,11 @@ static void frr_daemons_write(int ospfd, int ripd, int bfdd)
"vrrpd=no\n"
"pathd=no\n"
"\n",
- ospfd ? "yes" : "no",
- ripd ? "yes" : "no",
- bfdd ? "yes" : "no");
+ ospfd ? "yes" : "no",
+ ospf6d ? "yes" : "no",
+ ripd ? "yes" : "no",
+ ripngd ? "yes" : "no",
+ bfdd ? "yes" : "no");
/* Global settings and per-daemon options */
fputs(
@@ -484,7 +611,9 @@ static void frr_daemons_write(int ospfd, int ripd, int bfdd)
"zebra_options=\" -A 127.0.0.1 -s 90000000\"\n"
"mgmtd_options=\" -A 127.0.0.1\"\n"
"ospfd_options=\" -A 127.0.0.1\"\n"
+ "ospf6d_options=\" -A 127.0.0.1\"\n"
"ripd_options=\" -A 127.0.0.1\"\n"
+ "ripngd_options=\" -A 127.0.0.1\"\n"
"staticd_options=\"-A 127.0.0.1\"\n"
"bfdd_options=\" -A 127.0.0.1\"\n"
"\n"
@@ -498,7 +627,7 @@ static void frr_daemons_write(int ospfd, int ripd, int bfdd)
int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
- int netd_enabled = 0, ospfd_enabled = 0, bfdd_enabled = 0, ripd_enabled = 0;
+ int netd_enabled = 0, ospfd_enabled = 0, ospf6d_enabled = 0, bfdd_enabled = 0, ripd_enabled = 0, ripngd_enabled = 0;
struct lyd_node *cplane, *cplanes;
int rc = SR_ERR_OK;
FILE *fp;
@@ -521,14 +650,20 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly
/* Check if passed validation in previous event */
netd_enabled = fexist(NETD_CONF_NEXT);
ospfd_enabled = fexist(OSPFD_CONF_NEXT);
+ ospf6d_enabled = fexist(OSPF6D_CONF_NEXT);
bfdd_enabled = fexist(BFDD_SIGNAL_NEXT);
ripd_enabled = fexist(RIPD_SIGNAL_NEXT);
+ ripngd_enabled = fexist(RIPNGD_SIGNAL_NEXT);
goto activate;
default:
return SR_ERR_OK;
}
+ /* Reset the shared BFDD signal; parse_ospf/parse_ospf6 re-set it if any
+ * OSPF instance enables BFD this commit. */
+ (void)remove(BFDD_SIGNAL_NEXT);
+
cplanes = lydx_get_descendant(config, "routing", "control-plane-protocols", "control-plane-protocol", NULL);
/* Open netd config file for both static routes and RIP */
@@ -550,12 +685,20 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly
netd_enabled = 1;
} else if (!strcmp(type, "infix-routing:ospfv2")) {
parse_ospf(session, lydx_get_child(cplane, "ospf"));
+ } else if (!strcmp(type, "infix-routing:ospfv3")) {
+ parse_ospf6(session, lydx_get_child(cplane, "ospf"));
} else if (!strcmp(type, "infix-routing:ripv2")) {
- num = parse_rip(session, lydx_get_child(cplane, "rip"), fp);
+ num = parse_rip(session, lydx_get_child(cplane, "rip"), fp, 0);
if (num > 0) {
touch(RIPD_SIGNAL_NEXT);
netd_enabled = 1;
}
+ } else if (!strcmp(type, "infix-routing:ripng")) {
+ num = parse_rip(session, lydx_get_child(cplane, "rip"), fp, 1);
+ if (num > 0) {
+ touch(RIPNGD_SIGNAL_NEXT);
+ netd_enabled = 1;
+ }
}
}
@@ -569,12 +712,14 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly
/* For SR_EV_ENABLED we activate immediately (no SR_EV_DONE follows) */
netd_enabled = fexist(NETD_CONF_NEXT);
ospfd_enabled = fexist(OSPFD_CONF_NEXT);
+ ospf6d_enabled = fexist(OSPF6D_CONF_NEXT);
bfdd_enabled = fexist(BFDD_SIGNAL_NEXT);
ripd_enabled = fexist(RIPD_SIGNAL_NEXT);
+ ripngd_enabled = fexist(RIPNGD_SIGNAL_NEXT);
activate:
/* Generate complete /etc/frr/daemons (for watchfrr/frrinit.sh) */
- frr_daemons_write(ospfd_enabled, ripd_enabled, bfdd_enabled);
+ frr_daemons_write(ospfd_enabled, ospf6d_enabled, ripd_enabled, ripngd_enabled, bfdd_enabled);
if (bfdd_enabled)
(void)rename(BFDD_SIGNAL_NEXT, BFDD_SIGNAL);
@@ -589,11 +734,24 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly
(void)remove(OSPFD_CONF);
}
+ if (ospf6d_enabled) {
+ (void)remove(OSPF6D_CONF_PREV);
+ (void)rename(OSPF6D_CONF, OSPF6D_CONF_PREV);
+ (void)rename(OSPF6D_CONF_NEXT, OSPF6D_CONF);
+ } else {
+ (void)remove(OSPF6D_CONF);
+ }
+
if (ripd_enabled)
(void)rename(RIPD_SIGNAL_NEXT, RIPD_SIGNAL);
else
(void)remove(RIPD_SIGNAL);
+ if (ripngd_enabled)
+ (void)rename(RIPNGD_SIGNAL_NEXT, RIPNGD_SIGNAL);
+ else
+ (void)remove(RIPNGD_SIGNAL);
+
/* netd handles both static routes and RIP, assembles frr.conf */
if (netd_enabled) {
(void)remove(NETD_CONF_PREV);
@@ -608,8 +766,12 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly
ospfd_enabled ? finit_enable("ospfd") : finit_disable("ospfd");
if (ospfd_enabled)
finit_reload("ospfd");
- ripd_enabled ? finit_enable("ripd") : finit_disable("ripd");
- bfdd_enabled ? finit_enable("bfdd") : finit_disable("bfdd");
+ ospf6d_enabled ? finit_enable("ospf6d") : finit_disable("ospf6d");
+ if (ospf6d_enabled)
+ finit_reload("ospf6d");
+ ripd_enabled ? finit_enable("ripd") : finit_disable("ripd");
+ ripngd_enabled ? finit_enable("ripngd") : finit_disable("ripngd");
+ bfdd_enabled ? finit_enable("bfdd") : finit_disable("bfdd");
/*
* Signal netd to reload - it assembles /etc/frr/frr.conf and
diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc
index 88cada9d8..8a2fdbb72 100644
--- a/src/confd/yang/confd.inc
+++ b/src/confd/yang/confd.inc
@@ -31,7 +31,7 @@ MODULES=(
"ieee802-dot1q-types@2022-10-29.yang"
"infix-ip@2026-04-28.yang"
"infix-if-type@2026-01-07.yang"
- "infix-routing@2026-03-11.yang"
+ "infix-routing@2026-07-22.yang"
"ieee802-dot1ab-lldp@2022-03-15.yang"
"infix-lldp@2025-05-05.yang"
"infix-dhcp-common@2025-12-21.yang"
diff --git a/src/confd/yang/confd/infix-routing.yang b/src/confd/yang/confd/infix-routing.yang
index 535d0f1e3..4090dd3b8 100644
--- a/src/confd/yang/confd/infix-routing.yang
+++ b/src/confd/yang/confd/infix-routing.yang
@@ -26,6 +26,19 @@ module infix-routing {
contact "kernelkit@googlegroups.com";
description "Deviations and augments for ietf-routing, ietf-ospf, and ietf-rip.";
+ revision 2026-07-22 {
+ description "Add IPv6 dynamic routing support (RIPng and OSPFv3).
+ Introduce the ripng and ospfv3 routing-type identities,
+ expose the ietf-rip IPv6 address-family for RIPng (previously
+ deviated not-supported), restrict OSPFv3 interfaces to the
+ supported network types (no non-broadcast, no unicast
+ point-to-multipoint, no static neighbors), and extend the OSPF
+ local-rib and neighbor augments to OSPFv3. The OSPF
+ address-family leaf remains not-supported: OSPFv3 carries IPv6
+ only, IPv4 over OSPFv3 (RFC 5838) is not supported.";
+ reference "RFC 8695, RFC 2080, RFC 5340, RFC 9129";
+ }
+
revision 2026-03-11 {
description "Remove interface-type deviation to expose standard ietf-ospf
interface types including point-to-multipoint and hybrid.
@@ -158,11 +171,21 @@ module infix-routing {
base infix-routing-type;
description "OSPFv2 (IPv4) routing protocol";
}
+ identity ospfv3 {
+ base ospf:ospfv3;
+ base infix-routing-type;
+ description "OSPFv3 (IPv6) routing protocol";
+ }
identity ripv2 {
base rip:ripv2;
base infix-routing-type;
description "RIPv2 (IPv4) routing protocol";
}
+ identity ripng {
+ base rip:ripng;
+ base infix-routing-type;
+ description "RIPng (IPv6) routing protocol";
+ }
identity bfdv1 {
base bfd-types:bfdv1;
base infix-routing-type;
@@ -307,6 +330,11 @@ module infix-routing {
list redistribute {
key "protocol";
description "Redistribute protocols into OSPF";
+ must "not(derived-from-or-self(../../../rt:type, 'infix-rt:ospfv3')) or "
+ + "protocol != 'ospf'" {
+ error-message "OSPFv3 cannot redistribute OSPF routes, OSPFv2 routes
+ are IPv4 only.";
+ }
leaf protocol {
type infix-distribute-protocol;
description "Set protocol to redistribute";
@@ -379,6 +407,9 @@ module infix-routing {
deviate not-supported;
}
+ /* FRR ospf6d carries IPv6 routes only; IPv4-over-OSPFv3 (RFC 5838) is not
+ supported. OSPFv2 vs OSPFv3 is selected by the control-plane-protocol
+ type, so the address-family leaf is omitted. */
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:address-family" {
deviate not-supported;
}
@@ -459,6 +490,19 @@ module infix-routing {
}
}
}
+ /* OSPFv3 supports only broadcast, point-to-point and multicast
+ point-to-multipoint (hybrid) network types -- no non-broadcast (NBMA),
+ no unicast point-to-multipoint, and no static neighbors. */
+ deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface" {
+ deviate add {
+ must "not(derived-from-or-self(../../../../../rt:type, 'infix-rt:ospfv3')) or "
+ + "(not(ospf:interface-type = 'non-broadcast') and "
+ + "not(ospf:interface-type = 'point-to-multipoint') and "
+ + "count(ospf:static-neighbors/ospf:neighbor) = 0)" {
+ error-message "OSPFv3 supports only broadcast, point-to-point and hybrid interfaces, without static neighbors.";
+ }
+ }
+ }
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface/ospf:multi-areas" {
deviate not-supported;
}
@@ -667,11 +711,6 @@ module infix-routing {
description "Number of routes is not tracked separately.";
}
- deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/rip:rip/rip:ipv6" {
- deviate not-supported;
- description "RIPng (IPv6) is not currently supported in Infix.";
- }
-
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/rip:rip/rip:statistics" {
deviate not-supported;
description "Global statistics are not collected.";
@@ -732,7 +771,7 @@ module infix-routing {
*/
augment "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/"
+ "rip:rip/rip:interfaces/rip:interface" {
- when "derived-from-or-self(../../../rt:type, 'infix-routing:ripv2')" {
+ when "derived-from-or-self(../../../rt:type, 'infix-rt:ripv2')" {
description
"This augmentation is only valid for RIPv2.";
}
@@ -783,9 +822,10 @@ module infix-routing {
*/
augment "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/"
+ "ospf:ospf/ospf:local-rib/ospf:route" {
- when "derived-from-or-self(../../../rt:type, 'infix-routing:ospfv2')" {
+ when "derived-from-or-self(../../../rt:type, 'infix-rt:ospfv2') or "
+ + "derived-from-or-self(../../../rt:type, 'infix-rt:ospfv3')" {
description
- "This augmentation is only valid for OSPFv2.";
+ "This augmentation is valid for OSPFv2 and OSPFv3.";
}
description
"Infix extension to add area information to OSPF routes.";
@@ -807,9 +847,10 @@ module infix-routing {
augment "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/"
+ "ospf:ospf/ospf:areas/ospf:area/ospf:interfaces/ospf:interface/"
+ "ospf:neighbors/ospf:neighbor" {
- when "derived-from-or-self(../../../../../../../rt:type, 'infix-routing:ospfv2')" {
+ when "derived-from-or-self(../../../../../../../rt:type, 'infix-rt:ospfv2') or "
+ + "derived-from-or-self(../../../../../../../rt:type, 'infix-rt:ospfv3')" {
description
- "This augmentation is only valid for OSPFv2.";
+ "This augmentation is valid for OSPFv2 and OSPFv3.";
}
description
"Infix extension to add uptime information to OSPF neighbors.";
diff --git a/src/confd/yang/confd/infix-routing@2026-03-11.yang b/src/confd/yang/confd/infix-routing@2026-07-22.yang
similarity index 100%
rename from src/confd/yang/confd/infix-routing@2026-03-11.yang
rename to src/confd/yang/confd/infix-routing@2026-07-22.yang
diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml
index 99f6f8893..8e291c32b 100644
--- a/src/klish-plugin-infix/xml/infix.xml
+++ b/src/klish-plugin-infix/xml/infix.xml
@@ -703,6 +703,35 @@ echo "Public: $pub"
show routes ipv6 |pager
+
+
+
+ show ospf6 |pager
+
+
+
+
+
+ show ospf6 neighbor |pager
+
+
+
+
+
+
+
+
+ show ospf6 interface "$KLISH_PARAM_ifname" |pager
+
+
+
+
+
+ show ospf6 route |pager
+
+
+
+
diff --git a/src/netd/src/config.c b/src/netd/src/config.c
index 8dd0bfbd9..a002fd492 100644
--- a/src/netd/src/config.c
+++ b/src/netd/src/config.c
@@ -262,7 +262,7 @@ static int parse_rip_section(cfg_t *cfg_rip, struct rip_config *rip_cfg)
* Parse a single config file using libconfuse
*/
static int config_parse_file(const char *path, struct route_head *routes,
- struct rip_config *rip_cfg)
+ struct rip_config *rip_cfg, struct rip_config *ripng_cfg)
{
cfg_opt_t timers_opts[] = {
CFG_INT("update", 30, CFGF_NONE),
@@ -299,6 +299,7 @@ static int config_parse_file(const char *path, struct route_head *routes,
cfg_opt_t opts[] = {
CFG_SEC("route", route_opts, CFGF_MULTI),
CFG_SEC("rip", rip_opts, CFGF_NONE),
+ CFG_SEC("ripng", rip_opts, CFGF_NONE),
CFG_END()
};
@@ -342,11 +343,19 @@ static int config_parse_file(const char *path, struct route_head *routes,
ERROR("Failed to parse RIP section in %s", path);
}
+ /* Parse RIPng section if present */
+ cfg_rip = cfg_getsec(cfg, "ripng");
+ if (cfg_rip) {
+ if (parse_rip_section(cfg_rip, ripng_cfg) < 0)
+ ERROR("Failed to parse RIPng section in %s", path);
+ }
+
cfg_free(cfg);
return 0;
}
-int config_load(struct route_head *routes, struct rip_config *rip_cfg)
+int config_load(struct route_head *routes, struct rip_config *rip_cfg,
+ struct rip_config *ripng_cfg)
{
struct dirent **namelist;
char path[PATH_MAX];
@@ -375,7 +384,7 @@ int config_load(struct route_head *routes, struct rip_config *rip_cfg)
snprintf(path, sizeof(path), "%s/%s", CONF_DIR, name);
DEBUG("Loading config %s", path);
- config_parse_file(path, routes, rip_cfg);
+ config_parse_file(path, routes, rip_cfg, ripng_cfg);
free(namelist[i]);
}
diff --git a/src/netd/src/config.h b/src/netd/src/config.h
index 3aac29971..faae4c796 100644
--- a/src/netd/src/config.h
+++ b/src/netd/src/config.h
@@ -5,6 +5,7 @@
#include "netd.h"
-int config_load(struct route_head *routes, struct rip_config *rip_cfg);
+int config_load(struct route_head *routes, struct rip_config *rip_cfg,
+ struct rip_config *ripng_cfg);
#endif /* NETD_CONFIG_H_ */
diff --git a/src/netd/src/frrconf_backend.c b/src/netd/src/frrconf_backend.c
index 0aa6a7bd1..1afe0a809 100644
--- a/src/netd/src/frrconf_backend.c
+++ b/src/netd/src/frrconf_backend.c
@@ -16,6 +16,7 @@
#define FRR_CONF "/etc/frr/frr.conf"
#define FRR_CONF_NEXT FRR_CONF "+"
#define OSPFD_CONF "/etc/frr/ospfd.conf"
+#define OSPF6D_CONF "/etc/frr/ospf6d.conf"
static const char *frr_header =
"! Generated by netd\n"
@@ -147,6 +148,51 @@ static void write_rip_config(FILE *fp, struct rip_config *rip)
DEBUG("frrconf: wrote RIP configuration");
}
+/*
+ * RIPng redistribute keyword differs from RIPv2: OSPF means OSPFv3
+ * (ospf6) for an IPv6 IGP. FRR ripngd has no 'distance' or 'neighbor'
+ * commands, so those are intentionally not emitted here.
+ */
+static const char *ripng_redist_name(enum rip_redist_type type)
+{
+ switch (type) {
+ case RIP_REDIST_CONNECTED: return "connected";
+ case RIP_REDIST_STATIC: return "static";
+ case RIP_REDIST_KERNEL: return "kernel";
+ case RIP_REDIST_OSPF: return "ospf6";
+ }
+ return "unknown";
+}
+
+static void write_ripng_config(FILE *fp, struct rip_config *ripng)
+{
+ struct rip_redistribute *redist;
+ struct rip_network *net;
+
+ if (!ripng->enabled)
+ return;
+
+ fputs("router ripng\n", fp);
+ fprintf(fp, " default-metric %u\n", ripng->default_metric);
+ fprintf(fp, " timers basic %u %u %u\n",
+ ripng->timers.update, ripng->timers.invalid, ripng->timers.flush);
+
+ if (ripng->default_route)
+ fputs(" default-information originate\n", fp);
+
+ TAILQ_FOREACH(net, &ripng->networks, entries) {
+ fprintf(fp, " network %s\n", net->ifname);
+ if (net->passive)
+ fprintf(fp, " passive-interface %s\n", net->ifname);
+ }
+
+ TAILQ_FOREACH(redist, &ripng->redistributes, entries)
+ fprintf(fp, " redistribute %s\n", ripng_redist_name(redist->type));
+
+ fputs("!\n", fp);
+ DEBUG("frrconf: wrote RIPng configuration");
+}
+
static void append_file(FILE *fp, const char *path)
{
char buf[1024];
@@ -164,7 +210,8 @@ static void append_file(FILE *fp, const char *path)
DEBUG("frrconf: appended %s", path);
}
-int frrconf_backend_apply(struct route_head *routes, struct rip_config *rip)
+int frrconf_backend_apply(struct route_head *routes, struct rip_config *rip,
+ struct rip_config *ripng)
{
FILE *fp;
@@ -177,7 +224,9 @@ int frrconf_backend_apply(struct route_head *routes, struct rip_config *rip)
fputs(frr_header, fp);
write_static_routes(fp, routes);
write_rip_config(fp, rip);
+ write_ripng_config(fp, ripng);
append_file(fp, OSPFD_CONF);
+ append_file(fp, OSPF6D_CONF);
fclose(fp);
diff --git a/src/netd/src/frrconf_backend.h b/src/netd/src/frrconf_backend.h
index bf7b0ba63..c6e6c2213 100644
--- a/src/netd/src/frrconf_backend.h
+++ b/src/netd/src/frrconf_backend.h
@@ -7,6 +7,7 @@
int frrconf_backend_init(void);
void frrconf_backend_cleanup(void);
-int frrconf_backend_apply(struct route_head *routes, struct rip_config *rip);
+int frrconf_backend_apply(struct route_head *routes, struct rip_config *rip,
+ struct rip_config *ripng);
#endif /* NETD_FRRCONF_BACKEND_H_ */
diff --git a/src/netd/src/grpc_backend.cc b/src/netd/src/grpc_backend.cc
index dd3e6730a..c1524b56e 100644
--- a/src/netd/src/grpc_backend.cc
+++ b/src/netd/src/grpc_backend.cc
@@ -87,7 +87,8 @@ extern "C" void grpc_backend_cleanup(void)
DEBUG("grpc: finalized");
}
-extern "C" int grpc_backend_apply(struct route_head *routes, struct rip_config *rip)
+extern "C" int grpc_backend_apply(struct route_head *routes, struct rip_config *rip,
+ struct rip_config *ripng)
{
frr::CreateCandidateResponse create_resp;
frr::LoadToCandidateResponse load_resp;
@@ -109,6 +110,9 @@ extern "C" int grpc_backend_apply(struct route_head *routes, struct rip_config *
return -1;
}
+ if (ripng->enabled)
+ DEBUG("grpc: RIPng not supported via gRPC backend, ignoring");
+
/* Build JSON configuration for both static routes and RIP */
json_config = build_routing_json(routes, rip);
if (!json_config) {
diff --git a/src/netd/src/grpc_backend.h b/src/netd/src/grpc_backend.h
index 77db096f4..0b87bb682 100644
--- a/src/netd/src/grpc_backend.h
+++ b/src/netd/src/grpc_backend.h
@@ -9,7 +9,8 @@ extern "C" {
int grpc_backend_init(void);
void grpc_backend_cleanup(void);
-int grpc_backend_apply(struct route_head *routes, struct rip_config *rip);
+int grpc_backend_apply(struct route_head *routes, struct rip_config *rip,
+ struct rip_config *ripng);
#ifdef __cplusplus
}
diff --git a/src/netd/src/linux_backend.c b/src/netd/src/linux_backend.c
index 9e69ad2c3..271642a13 100644
--- a/src/netd/src/linux_backend.c
+++ b/src/netd/src/linux_backend.c
@@ -406,7 +406,8 @@ static int kernel_read_routes(struct route_head *routes, int family)
return 0;
}
-int linux_backend_apply(struct route_head *routes, struct rip_config *rip)
+int linux_backend_apply(struct route_head *routes, struct rip_config *rip,
+ struct rip_config *ripng)
{
struct route_head kernel_routes = TAILQ_HEAD_INITIALIZER(kernel_routes);
struct route *r, *tmp;
@@ -414,8 +415,8 @@ int linux_backend_apply(struct route_head *routes, struct rip_config *rip)
int errors = 0;
int added = 0;
- if (rip->enabled)
- DEBUG("Linux backend: RIP not supported without FRR");
+ if (rip->enabled || ripng->enabled)
+ ERROR("Linux backend: RIP/RIPng not supported without FRR");
/* Read current static routes from kernel (both IPv4 and IPv6) */
kernel_read_routes(&kernel_routes, AF_INET);
diff --git a/src/netd/src/linux_backend.h b/src/netd/src/linux_backend.h
index 97e066b4d..9d999455f 100644
--- a/src/netd/src/linux_backend.h
+++ b/src/netd/src/linux_backend.h
@@ -7,7 +7,8 @@
int linux_backend_init(void);
void linux_backend_cleanup(void);
-int linux_backend_apply(struct route_head *routes, struct rip_config *rip);
+int linux_backend_apply(struct route_head *routes, struct rip_config *rip,
+ struct rip_config *ripng);
/* Internal netlink operations */
int netlink_route_add(const struct route *r);
diff --git a/src/netd/src/netd.c b/src/netd/src/netd.c
index c8f183c16..a3df8e797 100644
--- a/src/netd/src/netd.c
+++ b/src/netd/src/netd.c
@@ -13,35 +13,36 @@ int debug;
static struct route_head active_routes = TAILQ_HEAD_INITIALIZER(active_routes);
static struct rip_config active_rip;
+static struct rip_config active_ripng;
/* Backend selection at compile time */
#ifdef HAVE_FRR_GRPC
#include "grpc_backend.h"
static int backend_init(void) { return grpc_backend_init(); }
static void backend_cleanup(void) { grpc_backend_cleanup(); }
-static int backend_apply(struct route_head *routes, struct rip_config *rip) {
- return grpc_backend_apply(routes, rip);
+static int backend_apply(struct route_head *routes, struct rip_config *rip, struct rip_config *ripng) {
+ return grpc_backend_apply(routes, rip, ripng);
}
#elif defined(HAVE_FRR_CONF)
#include "frrconf_backend.h"
static int backend_init(void) { return frrconf_backend_init(); }
static void backend_cleanup(void) { frrconf_backend_cleanup(); }
-static int backend_apply(struct route_head *routes, struct rip_config *rip) {
- return frrconf_backend_apply(routes, rip);
+static int backend_apply(struct route_head *routes, struct rip_config *rip, struct rip_config *ripng) {
+ return frrconf_backend_apply(routes, rip, ripng);
}
#elif defined(HAVE_FRR_VTYSH)
#include "vtysh_backend.h"
static int backend_init(void) { return vtysh_backend_init(); }
static void backend_cleanup(void) { vtysh_backend_cleanup(); }
-static int backend_apply(struct route_head *routes, struct rip_config *rip) {
- return vtysh_backend_apply(routes, rip);
+static int backend_apply(struct route_head *routes, struct rip_config *rip, struct rip_config *ripng) {
+ return vtysh_backend_apply(routes, rip, ripng);
}
#else
#include "linux_backend.h"
static int backend_init(void) { return linux_backend_init(); }
static void backend_cleanup(void) { linux_backend_cleanup(); }
-static int backend_apply(struct route_head *routes, struct rip_config *rip) {
- return linux_backend_apply(routes, rip);
+static int backend_apply(struct route_head *routes, struct rip_config *rip, struct rip_config *ripng) {
+ return linux_backend_apply(routes, rip, ripng);
}
#endif
@@ -105,25 +106,83 @@ static void rip_config_free(struct rip_config *cfg)
}
}
-static void reload(struct ev_loop *loop)
+/*
+ * Move a freshly loaded rip_config into an (already initialized, empty)
+ * destination: copy scalars and splice the TAILQ lists over. Leaves src
+ * empty, so the caller need not free its lists afterwards.
+ */
+static void rip_config_move(struct rip_config *dst, struct rip_config *src)
{
- struct route_head new_routes = TAILQ_HEAD_INITIALIZER(new_routes);
struct rip_redistribute *redist;
struct rip_system_cmd *cmd;
- struct rip_config new_rip;
struct rip_neighbor *nbr;
struct rip_network *net;
+
+ dst->enabled = src->enabled;
+ dst->default_metric = src->default_metric;
+ dst->distance = src->distance;
+ dst->default_route = src->default_route;
+ dst->debug_events = src->debug_events;
+ dst->debug_packet = src->debug_packet;
+ dst->debug_kernel = src->debug_kernel;
+ dst->timers = src->timers;
+
+ while ((net = TAILQ_FIRST(&src->networks)) != NULL) {
+ TAILQ_REMOVE(&src->networks, net, entries);
+ TAILQ_INSERT_TAIL(&dst->networks, net, entries);
+ }
+ while ((nbr = TAILQ_FIRST(&src->neighbors)) != NULL) {
+ TAILQ_REMOVE(&src->neighbors, nbr, entries);
+ TAILQ_INSERT_TAIL(&dst->neighbors, nbr, entries);
+ }
+ while ((redist = TAILQ_FIRST(&src->redistributes)) != NULL) {
+ TAILQ_REMOVE(&src->redistributes, redist, entries);
+ TAILQ_INSERT_TAIL(&dst->redistributes, redist, entries);
+ }
+ while ((cmd = TAILQ_FIRST(&src->system_cmds)) != NULL) {
+ TAILQ_REMOVE(&src->system_cmds, cmd, entries);
+ TAILQ_INSERT_TAIL(&dst->system_cmds, cmd, entries);
+ }
+}
+
+/*
+ * Execute the deferred vtysh debug commands for a RIP/RIPng instance.
+ * Run in background with retry since daemons may not be ready yet.
+ */
+static void rip_run_system_cmds(struct rip_config *cfg)
+{
+ struct rip_system_cmd *cmd;
+
+ TAILQ_FOREACH(cmd, &cfg->system_cmds, entries) {
+ char retry_cmd[512];
+
+ snprintf(retry_cmd, sizeof(retry_cmd),
+ "(for i in 1 2 3 4 5; do %s && break || sleep 1; done) &",
+ cmd->command);
+ DEBUG("Executing system command with retry: %s", cmd->command);
+ if (system(retry_cmd) != 0)
+ ERROR("Failed to launch system command: %s", cmd->command);
+ }
+}
+
+static void reload(struct ev_loop *loop)
+{
+ struct route_head new_routes = TAILQ_HEAD_INITIALIZER(new_routes);
+ struct rip_config new_rip;
+ struct rip_config new_ripng;
struct route *r;
int count = 0;
DEBUG("Reloading configuration");
rip_config_init(&new_rip);
+ rip_config_init(&new_ripng);
- if (config_load(&new_routes, &new_rip)) {
+ if (config_load(&new_routes, &new_rip, &new_ripng)) {
ERROR("Failed loading config, keeping current routes");
route_list_free(&new_routes);
rip_config_free(&new_rip);
+ rip_config_free(&new_ripng);
return;
}
@@ -132,12 +191,15 @@ static void reload(struct ev_loop *loop)
DEBUG("Loaded %d routes from config", count);
if (new_rip.enabled)
DEBUG("RIP configuration loaded");
+ if (new_ripng.enabled)
+ DEBUG("RIPng configuration loaded");
/* Apply config via backend */
- if (backend_apply(&new_routes, &new_rip)) {
+ if (backend_apply(&new_routes, &new_rip, &new_ripng)) {
ERROR("Failed applying config via backend, retry in 5s");
route_list_free(&new_routes);
rip_config_free(&new_rip);
+ rip_config_free(&new_ripng);
ev_timer_stop(loop, &retry_w);
ev_timer_set(&retry_w, 5., 0.);
ev_timer_start(loop, &retry_w);
@@ -150,6 +212,8 @@ static void reload(struct ev_loop *loop)
TAILQ_INIT(&active_routes);
rip_config_free(&active_rip);
rip_config_init(&active_rip);
+ rip_config_free(&active_ripng);
+ rip_config_init(&active_ripng);
/* Move new_routes to active_routes */
while ((r = TAILQ_FIRST(&new_routes)) != NULL) {
@@ -157,54 +221,13 @@ static void reload(struct ev_loop *loop)
TAILQ_INSERT_TAIL(&active_routes, r, entries);
}
- /* Move new_rip to active_rip - copy scalars and move lists */
- active_rip.enabled = new_rip.enabled;
- active_rip.default_metric = new_rip.default_metric;
- active_rip.distance = new_rip.distance;
- active_rip.default_route = new_rip.default_route;
- active_rip.debug_events = new_rip.debug_events;
- active_rip.debug_packet = new_rip.debug_packet;
- active_rip.debug_kernel = new_rip.debug_kernel;
- active_rip.timers = new_rip.timers;
-
- /* Move network list */
- while ((net = TAILQ_FIRST(&new_rip.networks)) != NULL) {
- TAILQ_REMOVE(&new_rip.networks, net, entries);
- TAILQ_INSERT_TAIL(&active_rip.networks, net, entries);
- }
+ /* Move new RIP/RIPng config into active (scalars + lists) */
+ rip_config_move(&active_rip, &new_rip);
+ rip_config_move(&active_ripng, &new_ripng);
- /* Move neighbor list */
- while ((nbr = TAILQ_FIRST(&new_rip.neighbors)) != NULL) {
- TAILQ_REMOVE(&new_rip.neighbors, nbr, entries);
- TAILQ_INSERT_TAIL(&active_rip.neighbors, nbr, entries);
- }
-
- /* Move redistribute list */
- while ((redist = TAILQ_FIRST(&new_rip.redistributes)) != NULL) {
- TAILQ_REMOVE(&new_rip.redistributes, redist, entries);
- TAILQ_INSERT_TAIL(&active_rip.redistributes, redist, entries);
- }
-
- /* Move system commands list */
- while ((cmd = TAILQ_FIRST(&new_rip.system_cmds)) != NULL) {
- TAILQ_REMOVE(&new_rip.system_cmds, cmd, entries);
- TAILQ_INSERT_TAIL(&active_rip.system_cmds, cmd, entries);
- }
-
- /* Execute system commands after config is applied.
- * Run in background with retry since daemons may not be ready yet. */
- if (!TAILQ_EMPTY(&active_rip.system_cmds)) {
- TAILQ_FOREACH(cmd, &active_rip.system_cmds, entries) {
- char retry_cmd[512];
-
- snprintf(retry_cmd, sizeof(retry_cmd),
- "(for i in 1 2 3 4 5; do %s && break || sleep 1; done) &",
- cmd->command);
- DEBUG("Executing system command with retry: %s", cmd->command);
- if (system(retry_cmd) != 0)
- ERROR("Failed to launch system command: %s", cmd->command);
- }
- }
+ /* Execute deferred debug commands after config is applied. */
+ rip_run_system_cmds(&active_rip);
+ rip_run_system_cmds(&active_ripng);
pidfile(NULL);
}
@@ -286,6 +309,7 @@ int main(int argc, char *argv[])
TAILQ_INIT(&active_routes);
rip_config_init(&active_rip);
+ rip_config_init(&active_ripng);
/* Signal watchers */
ev_signal_init(&sighup_w, sighup_cb, SIGHUP);
@@ -327,6 +351,7 @@ int main(int argc, char *argv[])
close(ifd);
route_list_free(&active_routes);
rip_config_free(&active_rip);
+ rip_config_free(&active_ripng);
backend_cleanup();
closelog();
diff --git a/src/netd/src/vtysh_backend.c b/src/netd/src/vtysh_backend.c
index fad719208..7fb3fb299 100644
--- a/src/netd/src/vtysh_backend.c
+++ b/src/netd/src/vtysh_backend.c
@@ -22,6 +22,7 @@
#define NETD_CONF "/etc/frr/netd.conf"
#define NETD_CONF_NEXT NETD_CONF "+"
#define OSPFD_CONF "/etc/frr/ospfd.conf"
+#define OSPF6D_CONF "/etc/frr/ospf6d.conf"
static const char *frr_header =
"! Generated by netd\n"
@@ -141,6 +142,50 @@ static void write_rip_config(FILE *fp, struct rip_config *rip)
fputs("!\n", fp);
}
+/*
+ * RIPng redistribute keyword differs from RIPv2: OSPF means OSPFv3
+ * (ospf6) for an IPv6 IGP. FRR ripngd has no 'distance' or 'neighbor'
+ * commands, so those are intentionally not emitted here.
+ */
+static const char *ripng_redist_name(enum rip_redist_type type)
+{
+ switch (type) {
+ case RIP_REDIST_CONNECTED: return "connected";
+ case RIP_REDIST_STATIC: return "static";
+ case RIP_REDIST_KERNEL: return "kernel";
+ case RIP_REDIST_OSPF: return "ospf6";
+ }
+ return "unknown";
+}
+
+static void write_ripng_config(FILE *fp, struct rip_config *ripng)
+{
+ struct rip_redistribute *redist;
+ struct rip_network *net;
+
+ if (!ripng->enabled)
+ return;
+
+ fputs("router ripng\n", fp);
+ fprintf(fp, " default-metric %u\n", ripng->default_metric);
+ fprintf(fp, " timers basic %u %u %u\n",
+ ripng->timers.update, ripng->timers.invalid, ripng->timers.flush);
+
+ if (ripng->default_route)
+ fputs(" default-information originate\n", fp);
+
+ TAILQ_FOREACH(net, &ripng->networks, entries) {
+ fprintf(fp, " network %s\n", net->ifname);
+ if (net->passive)
+ fprintf(fp, " passive-interface %s\n", net->ifname);
+ }
+
+ TAILQ_FOREACH(redist, &ripng->redistributes, entries)
+ fprintf(fp, " redistribute %s\n", ripng_redist_name(redist->type));
+
+ fputs("!\n", fp);
+}
+
static void append_file(FILE *fp, const char *path)
{
char buf[1024];
@@ -189,10 +234,11 @@ static void negate_old_conf(FILE *fp)
if (len == 0 || line[0] == '!' || line[0] == '#')
continue;
- /* Track router rip block */
- if (strcmp(line, "router rip") == 0) {
+ /* Track router rip / router ripng block */
+ if (strcmp(line, "router rip") == 0 ||
+ strcmp(line, "router ripng") == 0) {
in_rip = 1;
- fputs("no router rip\n", fp);
+ fprintf(fp, "no %s\n", line);
count++;
continue;
}
@@ -218,7 +264,8 @@ static void negate_old_conf(FILE *fp)
* Save current config to netd.conf for next reload or crash recovery.
* Written atomically via rename.
*/
-static int save_conf(struct route_head *routes, struct rip_config *rip)
+static int save_conf(struct route_head *routes, struct rip_config *rip,
+ struct rip_config *ripng)
{
struct route *r;
FILE *fp;
@@ -233,6 +280,7 @@ static int save_conf(struct route_head *routes, struct rip_config *rip)
write_route(fp, r);
write_rip_config(fp, rip);
+ write_ripng_config(fp, ripng);
fclose(fp);
@@ -245,7 +293,8 @@ static int save_conf(struct route_head *routes, struct rip_config *rip)
return 0;
}
-int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip)
+int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip,
+ struct rip_config *ripng)
{
struct route *r;
int rc, count = 0;
@@ -269,11 +318,13 @@ int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip)
}
DEBUG("vtysh: wrote %d new routes", count);
- /* Write new RIP config */
+ /* Write new RIP / RIPng config */
write_rip_config(fp, rip);
+ write_ripng_config(fp, ripng);
- /* Append OSPF config if present (written by confd) */
+ /* Append OSPF / OSPFv3 config if present (written by confd) */
append_file(fp, OSPFD_CONF);
+ append_file(fp, OSPF6D_CONF);
fclose(fp);
@@ -290,7 +341,7 @@ int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip)
}
/* Persist new state for next reload / crash recovery */
- if (save_conf(routes, rip))
+ if (save_conf(routes, rip, ripng))
ERROR("vtysh: failed to save %s, next reload may be inconsistent", NETD_CONF);
INFO("vtysh: applied config via vtysh -b");
diff --git a/src/netd/src/vtysh_backend.h b/src/netd/src/vtysh_backend.h
index dc60e4fa1..4c3945244 100644
--- a/src/netd/src/vtysh_backend.h
+++ b/src/netd/src/vtysh_backend.h
@@ -7,6 +7,7 @@
int vtysh_backend_init(void);
void vtysh_backend_cleanup(void);
-int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip);
+int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip,
+ struct rip_config *ripng);
#endif /* NETD_VTYSH_BACKEND_H_ */
diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py
index f098c2734..11a6e7bbc 100755
--- a/src/statd/python/cli_pretty/cli_pretty.py
+++ b/src/statd/python/cli_pretty/cli_pretty.py
@@ -5019,16 +5019,30 @@ def show_firewall_address_set(json, name=None):
print("No address-sets configured")
+def _find_ospf_instance(json_data):
+ """Select the OSPF control-plane-protocol matching the requested address
+ family. json_data may carry an '_afi' hint ('ipv4' or 'ipv6', default
+ 'ipv4'); falls back to the first OSPF instance for backward compat."""
+ routing = json_data.get('ietf-routing:routing', {})
+ protocols = routing.get('control-plane-protocols', {}).get('control-plane-protocol', [])
+ want_type = 'ospfv3' if json_data.get('_afi') == 'ipv6' else 'ospfv2'
+ fallback = None
+ for protocol in protocols:
+ if 'ietf-ospf:ospf' not in protocol:
+ continue
+ if fallback is None:
+ fallback = protocol
+ if want_type in protocol.get('type', ''):
+ return protocol
+ return fallback
+
+
def show_ospf(json_data):
"""Show OSPF general instance information"""
routing = json_data.get('ietf-routing:routing', {})
protocols = routing.get('control-plane-protocols', {}).get('control-plane-protocol', [])
- ospf_instance = None
- for protocol in protocols:
- if 'ietf-ospf:ospf' in protocol:
- ospf_instance = protocol
- break
+ ospf_instance = _find_ospf_instance(json_data)
if not ospf_instance:
print("OSPF is not configured or running")
@@ -5099,11 +5113,7 @@ def show_ospf_interfaces(json_data):
routing = json_data.get('ietf-routing:routing', {})
protocols = routing.get('control-plane-protocols', {}).get('control-plane-protocol', [])
- ospf_instance = None
- for protocol in protocols:
- if 'ietf-ospf:ospf' in protocol:
- ospf_instance = protocol
- break
+ ospf_instance = _find_ospf_instance(json_data)
if not ospf_instance:
print("OSPF is not configured or running")
@@ -5322,11 +5332,7 @@ def show_ospf_neighbor(json_data):
routing = json_data.get('ietf-routing:routing', {})
protocols = routing.get('control-plane-protocols', {}).get('control-plane-protocol', [])
- ospf_instance = None
- for protocol in protocols:
- if 'ietf-ospf:ospf' in protocol:
- ospf_instance = protocol
- break
+ ospf_instance = _find_ospf_instance(json_data)
if not ospf_instance:
print("OSPF is not configured or running")
@@ -5408,11 +5414,7 @@ def show_ospf_routes(json_data):
routing = json_data.get('ietf-routing:routing', {})
protocols = routing.get('control-plane-protocols', {}).get('control-plane-protocol', [])
- ospf_instance = None
- for protocol in protocols:
- if 'ietf-ospf:ospf' in protocol:
- ospf_instance = protocol
- break
+ ospf_instance = _find_ospf_instance(json_data)
if not ospf_instance:
print("OSPF is not configured or running")
diff --git a/src/statd/python/ospf6_status/__init__.py b/src/statd/python/ospf6_status/__init__.py
new file mode 100644
index 000000000..dfcb28c5c
--- /dev/null
+++ b/src/statd/python/ospf6_status/__init__.py
@@ -0,0 +1,4 @@
+from .ospf6_status import main
+
+if __name__ == "__main__":
+ main()
diff --git a/src/statd/python/ospf6_status/ospf6_status.py b/src/statd/python/ospf6_status/ospf6_status.py
new file mode 100644
index 000000000..5ea81c8a7
--- /dev/null
+++ b/src/statd/python/ospf6_status/ospf6_status.py
@@ -0,0 +1,97 @@
+#!/usr/bin/python3
+# Transform the output of the various "show ipv6 ospf6 ..." commands into a
+# single structure ordered to match the ietf-ospf YANG model (interfaces
+# nested under areas, neighbors nested under interfaces), mirroring what
+# ospf_status does for OSPFv2. FRR's ospf6d JSON uses different key names
+# than ospfd, so this is a dedicated reshaper.
+
+import sys
+import json
+import subprocess
+
+
+def run_json_cmd(cmd, default=None, check=True):
+ """Run a command (array of args) with JSON output and return the JSON"""
+ try:
+ result = subprocess.run(cmd, check=check, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, text=True)
+ data = json.loads(result.stdout)
+ except (subprocess.CalledProcessError, json.JSONDecodeError):
+ if default is not None:
+ return default
+ raise
+ return data
+
+
+def area_type(area):
+ """Derive the ietf-ospf area-type from ospf6d area flags."""
+ if area.get("areaIsNSSA"):
+ return "nssa-area"
+ if area.get("areaIsStub"):
+ return "stub-area"
+ return "normal-area"
+
+
+def iter_items(data, wrapper):
+ """Yield (name, value) for an object that is either keyed directly by
+ name or nested under a wrapper key (e.g. {"interfaces": {...}})."""
+ if isinstance(data, dict) and wrapper in data and isinstance(data[wrapper], dict):
+ data = data[wrapper]
+ if isinstance(data, dict):
+ for name, value in data.items():
+ if isinstance(value, dict):
+ yield name, value
+
+
+def main():
+ top = run_json_cmd(['sudo', 'vtysh', '-c', "show ipv6 ospf6 json"], default={})
+ ifaces = run_json_cmd(['sudo', 'vtysh', '-c', "show ipv6 ospf6 interface json"], default={})
+ neigh = run_json_cmd(['sudo', 'vtysh', '-c', "show ipv6 ospf6 neighbor json"], default={})
+
+ if not top:
+ print(json.dumps({}))
+ return
+
+ out = {"routerId": top.get("routerId"), "areas": {}}
+
+ # Seed areas with their type. In "show ipv6 ospf6 json" the areas are a
+ # dict keyed by area-id -- the area-id is the KEY, not a field inside the
+ # value -- so read the type flags (areaIsNSSA/areaIsStub) per key.
+ areas = top.get("areas", {})
+ if isinstance(areas, dict):
+ for aid, area in areas.items():
+ out["areas"][aid] = {"area-type": area_type(area), "interfaces": []}
+ else:
+ for area in areas:
+ aid = area.get("areaId")
+ if aid is not None:
+ out["areas"][aid] = {"area-type": area_type(area), "interfaces": []}
+
+ # Collect neighbors, grouped by interface name. FRR's ospf6 neighbor
+ # JSON does not carry an area field, and an interface belongs to exactly
+ # one area, so the interface name alone is a unique key.
+ nbrs_by_iface = {}
+ nlist = neigh.get("neighbors", [])
+ if isinstance(nlist, dict):
+ nlist = list(nlist.values())
+ for n in nlist:
+ nbrs_by_iface.setdefault(n.get("interfaceName"), []).append(n)
+
+ # Nest interfaces (with their neighbors) under areas.
+ for ifname, iface in iter_items(ifaces, "interfaces"):
+ aid = iface.get("areaId")
+ if not aid or not iface.get("attachedToArea", True):
+ continue
+ if aid not in out["areas"]:
+ out["areas"][aid] = {"area-type": "normal-area", "interfaces": []}
+
+ iface["name"] = iface.get("interface", ifname)
+ iface["neighbors"] = nbrs_by_iface.get(iface["name"], [])
+ out["areas"][aid]["interfaces"].append(iface)
+
+ print(json.dumps(out))
+
+
+if __name__ == "__main__":
+ main()
+ sys.exit(0)
diff --git a/src/statd/python/pyproject.toml b/src/statd/python/pyproject.toml
index 1a100287e..9e8faf18d 100644
--- a/src/statd/python/pyproject.toml
+++ b/src/statd/python/pyproject.toml
@@ -7,6 +7,7 @@ packages = [
{ include = "yanger" },
{ include = "cli_pretty" },
{ include = "ospf_status" },
+ { include = "ospf6_status" },
{ include = "rip_status" }
]
authors = [
@@ -22,4 +23,5 @@ build-backend = "poetry.core.masonry.api"
yanger = "yanger.__main__:main"
cli-pretty = "cli_pretty:main"
ospf-status = "ospf_status:main"
+ospf6-status = "ospf6_status:main"
rip-status = "rip_status.rip_status:main"
diff --git a/src/statd/python/yanger/ietf_ospf.py b/src/statd/python/yanger/ietf_ospf.py
index bd6335d37..6cbd4b0da 100644
--- a/src/statd/python/yanger/ietf_ospf.py
+++ b/src/statd/python/yanger/ietf_ospf.py
@@ -5,7 +5,8 @@
def frr_to_ietf_neighbor_state(state):
"""Fetch OSPF neighbor state from Frr"""
state = state.split("/")[0]
- if state == "TwoWay":
+ # ospfd spells it "TwoWay", ospf6d "Twoway".
+ if state.lower() == "twoway":
return "2-way"
return state.lower()
@@ -18,33 +19,99 @@ def frr_to_ietf_neighbor_role(role):
return role
-def add_routes(ospf):
- """Fetch OSPF routes from Frr"""
- cmd = ['vtysh', '-c', "show ip ospf route json"]
- data = HOST.run_json(cmd, default=[])
- if data == []:
- return # No OSPF routes available
+def ospf_interface_type(network_type, p2mp_non_broadcast=False):
+ """Map an FRR network type to an ietf-ospf interface-type.
+
+ Multicast point-to-multipoint is Infix's 'hybrid' type, unicast (NBMA)
+ point-to-multipoint is 'point-to-multipoint'. ospf6d has neither NBMA
+ nor unicast point-to-multipoint, so it only ever yields the first three."""
+ if network_type == "POINTOMULTIPOINT":
+ return "point-to-multipoint" if p2mp_non_broadcast else "hybrid"
+
+ xlate = {
+ "BROADCAST": "broadcast",
+ "POINTOPOINT": "point-to-point",
+ "NBMA": "non-broadcast",
+ }
+ return xlate.get(network_type)
+
+
+def ospf_route_type(info, ipv6):
+ """Map an FRR route/path type to an ietf-ospf route-type."""
+ if ipv6:
+ # ospf6d abbreviates the path type: IA=intra-area, IE=inter-area,
+ # E1/E2=external type 1/2.
+ xlate = {
+ "IA": "intra-area",
+ "IE": "inter-area",
+ "E1": "external-1",
+ "E2": "external-2",
+ }
+ return xlate.get(info.get("pathType"))
+
+ # ospfd spells it out: "N" for intra-area, "N IA", "N E1" and "N E2".
+ routetype = info["routeType"].split(" ")
+ if len(routetype) > 1:
+ xlate = {
+ "IA": "inter-area",
+ "E1": "external-1",
+ "E2": "external-2",
+ }
+ return xlate.get(routetype[1])
+ if routetype[0] == "N":
+ return "intra-area"
+
+ return None
+
+
+def ospf_next_hops(info, ipv6):
+ """Map the FRR next-hop list to ietf-ospf next-hops."""
+ nexthops = []
+
+ for hop in info.get("nextHops" if ipv6 else "nexthops", []):
+ nexthop = {}
+
+ if ipv6:
+ # "::" marks a directly-connected prefix (no gateway).
+ if hop.get("nextHop") and hop["nextHop"] != "::":
+ nexthop["next-hop"] = hop["nextHop"]
+ elif hop.get("interfaceName"):
+ nexthop["outgoing-interface"] = hop["interfaceName"]
+ elif hop["ip"] != " ":
+ nexthop["next-hop"] = hop["ip"]
+ else:
+ nexthop["outgoing-interface"] = hop["directlyAttachedTo"]
+
+ if nexthop:
+ nexthops.append(nexthop)
+
+ return nexthops
+
+
+def add_routes(ospf, ipv6=False):
+ """Fetch OSPF routes from Frr for the OSPF local-rib view"""
+ if ipv6:
+ cmd = ['vtysh', '-c', "show ipv6 ospf6 route json"]
+ else:
+ cmd = ['vtysh', '-c', "show ip ospf route json"]
+ data = HOST.run_json(cmd, default={})
+
+ if ipv6:
+ # ospf6d lists one entry per path, keep the installed (best) one so
+ # the local-rib list stays keyed uniquely by prefix.
+ data = {prefix: next((path for path in paths if path.get("isBestRoute")), paths[0])
+ for prefix, paths in data.get("routes", {}).items() if paths}
routes = []
for prefix, info in data.items():
if prefix.find("/") == -1: # Ignore router IDs
continue
- route = {}
- route["prefix"] = prefix
-
- nexthops = []
- routetype = info["routeType"].split(" ")
+ route = {"prefix": prefix}
- if len(routetype) > 1:
- if routetype[1] == "E1":
- route["route-type"] = "external-1"
- elif routetype[1] == "E2":
- route["route-type"] = "external-2"
- elif routetype[1] == "IA":
- route["route-type"] = "inter-area"
- elif routetype[0] == "N":
- route["route-type"] = "intra-area"
+ rtype = ospf_route_type(info, ipv6)
+ if rtype:
+ route["route-type"] = rtype
# Add area information if available
# Note: augmented by infix-routing.yang since standard ietf-ospf doesn't include it
@@ -62,19 +129,14 @@ def add_routes(ospf):
if info.get("tag") is not None:
route["route-tag"] = info["tag"]
- for hop in info["nexthops"]:
- nexthop = {}
- if hop["ip"] != " ":
- nexthop["next-hop"] = hop["ip"]
- else:
- nexthop["outgoing-interface"] = hop["directlyAttachedTo"]
- nexthops.append(nexthop)
+ nexthops = ospf_next_hops(info, ipv6)
+ if nexthops:
+ route["next-hops"] = {"next-hop": nexthops}
- route["next-hops"] = {}
- route["next-hops"]["next-hop"] = nexthops
routes.append(route)
- insert(ospf, "ietf-ospf:local-rib", "ietf-ospf:route", routes)
+ if routes:
+ insert(ospf, "ietf-ospf:local-rib", "ietf-ospf:route", routes)
def add_areas(control_protocols):
@@ -122,17 +184,10 @@ def add_areas(control_protocols):
interface["passive"] = False
interface["enabled"] = iface["ospfEnabled"]
- if iface["networkType"] == "POINTOPOINT":
- interface["interface-type"] = "point-to-point"
- elif iface["networkType"] == "BROADCAST":
- interface["interface-type"] = "broadcast"
- elif iface["networkType"] == "POINTOMULTIPOINT":
- if iface.get("p2mpNonBroadcast", False):
- interface["interface-type"] = "point-to-multipoint"
- else:
- interface["interface-type"] = "hybrid"
- elif iface["networkType"] == "NBMA":
- interface["interface-type"] = "non-broadcast"
+ itype = ospf_interface_type(iface["networkType"],
+ iface.get("p2mpNonBroadcast", False))
+ if itype:
+ interface["interface-type"] = itype
if iface.get("state"):
# Wev've never seen "DependUpon", and has no entry in
@@ -247,6 +302,101 @@ def add_areas(control_protocols):
insert(control_protocols, "control-plane-protocol", [control_protocol])
+def add_areas6(control_protocols):
+ """Populate OSPFv3 (ospf6d) operational status as a second
+ control-plane-protocol of type infix-routing:ospfv3."""
+ data = HOST.run_json(['/usr/libexec/statd/ospf6-status'], default={})
+ if data == {}:
+ return # No OSPFv3 data available (ospf6d not running)
+
+ control_protocol = {}
+ control_protocol["type"] = "infix-routing:ospfv3"
+ control_protocol["name"] = "default"
+ control_protocol["ietf-ospf:ospf"] = {}
+ control_protocol["ietf-ospf:ospf"]["ietf-ospf:areas"] = {}
+ control_protocol["ietf-ospf:ospf"]["ietf-ospf:router-id"] = data.get("routerId")
+ control_protocol["ietf-ospf:ospf"]["ietf-ospf:address-family"] = "ipv6"
+
+ state_xlate = {
+ "Down": "down",
+ "Waiting": "waiting",
+ "Loopback": "loopback",
+ "PointToPoint": "point-to-point",
+ "DROther": "dr-other",
+ "BDR": "bdr",
+ "DR": "dr",
+ }
+
+ areas = []
+ for area_id, values in data.get("areas", {}).items():
+ area = {}
+ area["ietf-ospf:area-id"] = area_id
+ area["ietf-ospf:interfaces"] = {}
+ if values.get("area-type"):
+ area["ietf-ospf:area-type"] = values["area-type"]
+
+ interfaces = []
+ for iface in values.get("interfaces", []):
+ interface = {}
+ interface["name"] = iface["name"]
+ interface["enabled"] = True
+
+ # FRR ospf6 reports the *operating* OSPF network type in
+ # "operatingAsType" (BROADCAST/POINTOPOINT/POINTOMULTIPOINT);
+ # "type" is the L2 type (always BROADCAST for ethernet).
+ itype = ospf_interface_type(iface.get("operatingAsType") or iface.get("type"))
+ if itype:
+ interface["interface-type"] = itype
+
+ interface["passive"] = bool(iface.get("timerPassiveIface"))
+
+ if iface.get("cost") is not None:
+ interface["cost"] = iface["cost"]
+ if iface.get("priority") is not None:
+ interface["priority"] = iface["priority"]
+
+ # Only set state when it maps to a valid ietf-ospf enum; ospf6
+ # has states (e.g. "PtMultipoint") with no ietf-ospf equivalent,
+ # and emitting an invalid value makes sysrepo reject the whole
+ # operational tree (HTTP 500 on the RESTCONF GET).
+ st = state_xlate.get(iface.get("ospf6InterfaceState"))
+ if st:
+ interface["state"] = st
+
+ if iface.get("timerIntervalsConfigDead") is not None:
+ interface["dead-interval"] = iface["timerIntervalsConfigDead"]
+ if iface.get("timerIntervalsConfigRetransmit") is not None:
+ interface["retransmit-interval"] = iface["timerIntervalsConfigRetransmit"]
+ if iface.get("transmitDelaySec") is not None:
+ interface["transmit-delay"] = iface["transmitDelaySec"]
+ if iface.get("timerIntervalsConfigHello") is not None:
+ interface["hello-interval"] = iface["timerIntervalsConfigHello"]
+
+ neighbors = []
+ for neigh in iface.get("neighbors", []):
+ neighbor = {}
+ neighbor["neighbor-router-id"] = neigh.get("neighborId")
+ if neigh.get("linkLocalAddress"):
+ neighbor["address"] = neigh["linkLocalAddress"]
+ if neigh.get("priority") is not None:
+ neighbor["priority"] = neigh["priority"]
+ # FRR ospf6 neighbor JSON reports the adjacency state in "state".
+ if neigh.get("state"):
+ neighbor["state"] = frr_to_ietf_neighbor_state(neigh["state"])
+ neighbors.append(neighbor)
+
+ interface["ietf-ospf:neighbors"] = {}
+ interface["ietf-ospf:neighbors"]["ietf-ospf:neighbor"] = neighbors
+ interfaces.append(interface)
+
+ area["ietf-ospf:interfaces"]["ietf-ospf:interface"] = interfaces
+ areas.append(area)
+
+ add_routes(control_protocol["ietf-ospf:ospf"], ipv6=True)
+ control_protocol["ietf-ospf:ospf"]["ietf-ospf:areas"]["ietf-ospf:area"] = areas
+ insert(control_protocols, "control-plane-protocol", [control_protocol])
+
+
def operational():
out = {
"ietf-routing:routing": {
@@ -256,4 +406,5 @@ def operational():
}
add_areas(out['ietf-routing:routing']['control-plane-protocols'])
+ add_areas6(out['ietf-routing:routing']['control-plane-protocols'])
return out
diff --git a/src/statd/python/yanger/ietf_rip.py b/src/statd/python/yanger/ietf_rip.py
index 0e4086f03..8fbccde82 100644
--- a/src/statd/python/yanger/ietf_rip.py
+++ b/src/statd/python/yanger/ietf_rip.py
@@ -2,15 +2,18 @@
from .host import HOST
-def parse_rip_status():
- """Parse 'show ip rip status' text output to extract operational state
+def parse_rip_status(cmd=('vtysh', '-c', 'show ip rip status')):
+ """Parse 'show ip rip status' / 'show ipv6 ripng status' text output
+
+ Both RIPv2 and RIPng share the same textual status layout in FRR, so
+ the same parser handles both; the caller selects the vtysh command.
Returns dict with keys: update-interval, invalid-interval, flush-interval,
default-metric, distance, interfaces (list), neighbors (list)
"""
try:
# HOST.run expects tuple, returns text string directly
- text = HOST.run(tuple(['vtysh', '-c', 'show ip rip status']), default="")
+ text = HOST.run(tuple(cmd), default="")
if not text:
return {}
except Exception as e:
@@ -258,6 +261,169 @@ def add_rip(control_protocols):
control_protocols["ietf-routing:control-plane-protocol"].append(control_protocol)
+def parse_ripng_neighbors():
+ """Parse the 'Routing Information Sources' section of
+ 'show ipv6 ripng status'.
+
+ Unlike RIPv2 (one row per peer), FRR's ripngd prints each peer across
+ two lines: the IPv6 source address on the first line, then the counters
+ (bad-packets, bad-routes, distance) and last-update on the next:
+
+ fe80::5054:ff:fe12:3456
+ 0 0 120 00:00:12
+
+ Returns a list of {address, bad-packets, bad-routes} dicts.
+ """
+ text = HOST.run(tuple(['vtysh', '-c', 'show ipv6 ripng status']), default="")
+ if not text:
+ return []
+
+ neighbors = []
+ in_section = False
+ pending_addr = None
+ for raw in text.split('\n'):
+ line = raw.strip()
+
+ if line.startswith('Routing Information Sources:'):
+ in_section = True
+ continue
+ if not in_section:
+ continue
+ # Skip the column header
+ if 'Gateway' in line and 'BadPackets' in line:
+ continue
+ if not line:
+ # End of section once we have entries and no half-parsed peer
+ if neighbors and pending_addr is None:
+ break
+ continue
+
+ if pending_addr is None:
+ # Address line (contains ':'); strip any %zone suffix
+ if ':' in line:
+ pending_addr = line.split()[0].split('%')[0]
+ continue
+
+ # Counters line for the pending address: badpackets badroutes distance [uptime]
+ parts = line.split()
+ if len(parts) >= 3:
+ try:
+ neighbors.append({
+ 'address': pending_addr,
+ 'bad-packets': int(parts[0]),
+ 'bad-routes': int(parts[1]),
+ })
+ except ValueError:
+ pass
+ pending_addr = None
+
+ return neighbors
+
+
+def add_ripng(control_protocols):
+ """Populate RIPng (RIP for IPv6) operational data
+
+ Mirrors add_rip() but for the IPv6 address family: status is scraped
+ from 'show ipv6 ripng status' and learned routes from
+ 'show ipv6 route ripng json'.
+ """
+ # Get operational status from text parsing
+ status = parse_rip_status(('vtysh', '-c', 'show ipv6 ripng status'))
+
+ # If we can't get status, ripngd is probably not running
+ if not status:
+ return
+
+ control_protocol = {}
+ control_protocol["type"] = "infix-routing:ripng"
+ control_protocol["name"] = "default"
+ control_protocol["ietf-rip:rip"] = {}
+
+ rip = control_protocol["ietf-rip:rip"]
+
+ # Add global operational state
+ if status.get('distance'):
+ rip['distance'] = status['distance']
+ if status.get('default-metric'):
+ rip['default-metric'] = status['default-metric']
+
+ # Add timers if available
+ if any(k in status for k in ['update-interval', 'invalid-interval', 'flush-interval']):
+ rip['timers'] = {}
+ if status.get('update-interval'):
+ rip['timers']['update-interval'] = status['update-interval']
+ if status.get('invalid-interval'):
+ rip['timers']['invalid-interval'] = status['invalid-interval']
+ if status.get('flush-interval'):
+ rip['timers']['flush-interval'] = status['flush-interval']
+
+ # Add interfaces if available. RIPng has no protocol version, so unlike
+ # RIPv2 we do not report send-version/receive-version here.
+ if status.get('interfaces'):
+ rip['interfaces'] = {'interface': []}
+ for iface in status['interfaces']:
+ rip['interfaces']['interface'].append({
+ 'interface': iface['name'],
+ 'oper-status': 'up'
+ })
+
+ # Get RIPng-learned routes from the IPv6 routing table (JSON)
+ route_data = HOST.run_json(['vtysh', '-c', 'show ipv6 route ripng json'], default={})
+
+ routes = []
+ for prefix, entries in route_data.items():
+ if not entries or '/' not in prefix:
+ continue
+
+ entry = entries[0] if isinstance(entries, list) else entries
+
+ route = {
+ "ipv6-prefix": prefix,
+ "metric": entry.get("metric", 0),
+ "route-type": "rip"
+ }
+
+ nexthops = entry.get("nexthops", [])
+ if nexthops:
+ first_hop = nexthops[0]
+ if first_hop.get("ip"):
+ route["next-hop"] = first_hop["ip"]
+ if first_hop.get("interfaceName"):
+ route["interface"] = first_hop["interfaceName"]
+
+ routes.append(route)
+
+ # Add neighbors to operational data. RIPng peers use a distinct two-line
+ # layout in 'show ipv6 ripng status', so it needs its own parser.
+ neighbors_list = []
+ for neighbor in parse_ripng_neighbors():
+ neighbors_list.append({
+ 'ipv6-address': neighbor['address'],
+ 'bad-packets-rcvd': neighbor['bad-packets'],
+ 'bad-routes-rcvd': neighbor['bad-routes']
+ })
+
+ # Add routes and neighbors to operational data
+ if routes or neighbors_list:
+ if "ipv6" not in rip:
+ rip["ipv6"] = {}
+
+ if routes:
+ rip["ipv6"]["routes"] = {
+ "route": routes
+ }
+
+ if neighbors_list:
+ rip["ipv6"]["neighbors"] = {
+ "neighbor": neighbors_list
+ }
+
+ # Add the control-protocol
+ if "ietf-routing:control-plane-protocol" not in control_protocols:
+ control_protocols["ietf-routing:control-plane-protocol"] = []
+ control_protocols["ietf-routing:control-plane-protocol"].append(control_protocol)
+
+
def operational():
"""Return RIP operational data in YANG format"""
out = {
@@ -267,4 +433,5 @@ def operational():
}
add_rip(out['ietf-routing:routing']['control-plane-protocols'])
+ add_ripng(out['ietf-routing:routing']['control-plane-protocols'])
return out
diff --git a/src/statd/python/yanger/ietf_routing.py b/src/statd/python/yanger/ietf_routing.py
index da6fd1572..9c55dfc20 100644
--- a/src/statd/python/yanger/ietf_routing.py
+++ b/src/statd/python/yanger/ietf_routing.py
@@ -52,6 +52,7 @@ def add_protocol(routes, proto):
'ospf': 'ietf-ospf:ospfv2',
'ospf6': 'ietf-ospf:ospfv3',
'rip': 'ietf-rip:rip',
+ 'ripng': 'ietf-rip:rip',
}
out = {}
diff --git a/test/case/routing/all.yaml b/test/case/routing/all.yaml
index b8d579484..cbd53ccd7 100644
--- a/test/case/routing/all.yaml
+++ b/test/case/routing/all.yaml
@@ -3,19 +3,19 @@
case: static_routing/test.py
- name: OSPF Basic
- case: ospf_basic/test.py
+ suite: ospf_basic/test.yaml
- name: OSPF Unnumbered Interfaces
case: ospf_unnumbered_interface/test.py
- name: OSPF with Multiple Areas
- case: ospf_multiarea/test.py
+ suite: ospf_multiarea/test.yaml
- name: OSPF BFD
- case: ospf_bfd/test.py
+ suite: ospf_bfd/test.yaml
- name: "Route preference: OSPF vs Static"
- case: route_pref_ospf/test.py
+ suite: route_pref_ospf/test.yaml
- name: "Route preference: DHCP vs Static"
case: route_pref_dhcp/test.py
@@ -24,25 +24,25 @@
case: route_pref_255/test.py
- name: OSPF Default Route Advertise
- case: ospf_default_route_advertise/test.py
+ suite: ospf_default_route_advertise/test.yaml
- name: OSPF Debug Logging
case: ospf_debug/test.py
- name: OSPF Point-to-Multipoint Hybrid
- case: ospf_point_to_multipoint_hybrid/test.py
+ suite: ospf_point_to_multipoint_hybrid/test.yaml
- name: OSPF Point-to-Multipoint
case: ospf_point_to_multipoint/test.py
- name: RIP Basic
- case: rip_basic/test.py
+ suite: rip_basic/test.yaml
- name: RIP Passive Interface
- case: rip_passive_interface/test.py
+ suite: rip_passive_interface/test.yaml
- name: RIP Redistribution
- case: rip_redistribute/test.py
+ suite: rip_redistribute/test.yaml
- name: RIP Multi-hop
- case: rip_multihop/test.py
+ suite: rip_multihop/test.yaml
diff --git a/test/case/routing/ospf_basic/Readme.adoc b/test/case/routing/ospf_basic/Readme.adoc
deleted file mode 120000
index ae32c8412..000000000
--- a/test/case/routing/ospf_basic/Readme.adoc
+++ /dev/null
@@ -1 +0,0 @@
-test.adoc
\ No newline at end of file
diff --git a/test/case/routing/ospf_basic/Readme.adoc b/test/case/routing/ospf_basic/Readme.adoc
new file mode 100644
index 000000000..2322b7ef7
--- /dev/null
+++ b/test/case/routing/ospf_basic/Readme.adoc
@@ -0,0 +1,6 @@
+include::ospfv2.adoc[]
+
+<<<
+
+include::ospfv3.adoc[]
+
diff --git a/test/case/routing/ospf_basic/ospfv2.adoc b/test/case/routing/ospf_basic/ospfv2.adoc
new file mode 100644
index 000000000..8626ebdb9
--- /dev/null
+++ b/test/case/routing/ospf_basic/ospfv2.adoc
@@ -0,0 +1,31 @@
+=== OSPFv2 Basic
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/ospf_basic]
+
+==== Description
+
+Verifies basic OSPFv2 functionality by configuring two routers (R1 and R2)
+with OSPFv2 on their interconnecting link. The test ensures OSPFv2
+neighbors are established, routes are exchanged between the routers, and
+end-to-end connectivity is achieved.
+
+An end-device (HOST) is connected to R2 on an interface without OSPFv2
+enabled. This verifies that OSPFv2 status information remains accessible
+when a router has non-OSPFv2 interfaces.
+
+Note: OSPFv3 has no IPv4 address to derive a router-id from, so an
+explicit-router-id is configured when running OSPFv3.
+
+==== Topology
+
+image::topology.svg[OSPFv2 Basic topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUTs
+. Configure targets
+. Wait for OSPFv2 routes
+. Verify R2 OSPFv2 neighbors with non-OSPFv2 interface
+. Test connectivity from PC:data to R2 loopback
+
+
diff --git a/test/case/routing/ospf_basic/ospfv2.py b/test/case/routing/ospf_basic/ospfv2.py
new file mode 120000
index 000000000..946566431
--- /dev/null
+++ b/test/case/routing/ospf_basic/ospfv2.py
@@ -0,0 +1 @@
+test.py
\ No newline at end of file
diff --git a/test/case/routing/ospf_basic/ospfv3.adoc b/test/case/routing/ospf_basic/ospfv3.adoc
new file mode 100644
index 000000000..fa19347ab
--- /dev/null
+++ b/test/case/routing/ospf_basic/ospfv3.adoc
@@ -0,0 +1,31 @@
+=== OSPFv3 Basic
+
+ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/ospf_basic]
+
+==== Description
+
+Verifies basic OSPFv3 functionality by configuring two routers (R1 and R2)
+with OSPFv3 on their interconnecting link. The test ensures OSPFv3
+neighbors are established, routes are exchanged between the routers, and
+end-to-end connectivity is achieved.
+
+An end-device (HOST) is connected to R2 on an interface without OSPFv3
+enabled. This verifies that OSPFv3 status information remains accessible
+when a router has non-OSPFv3 interfaces.
+
+Note: OSPFv3 has no IPv4 address to derive a router-id from, so an
+explicit-router-id is configured when running OSPFv3.
+
+==== Topology
+
+image::topology.svg[OSPFv3 Basic topology, align=center, scaledwidth=75%]
+
+==== Sequence
+
+. Set up topology and attach to target DUTs
+. Configure targets
+. Wait for OSPFv3 routes
+. Verify R2 OSPFv3 neighbors with non-OSPFv3 interface
+. Test connectivity from PC:data to R2 loopback
+
+
diff --git a/test/case/routing/ospf_basic/ospfv3.py b/test/case/routing/ospf_basic/ospfv3.py
new file mode 120000
index 000000000..946566431
--- /dev/null
+++ b/test/case/routing/ospf_basic/ospfv3.py
@@ -0,0 +1 @@
+test.py
\ No newline at end of file
diff --git a/test/case/routing/ospf_basic/test.adoc b/test/case/routing/ospf_basic/test.adoc
deleted file mode 100644
index d868a634d..000000000
--- a/test/case/routing/ospf_basic/test.adoc
+++ /dev/null
@@ -1,28 +0,0 @@
-=== OSPF Basic
-
-ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/ospf_basic]
-
-==== Description
-
-Verifies basic OSPF functionality by configuring two routers (R1 and R2)
-with OSPF on their interconnecting link. The test ensures OSPF
-neighbors are established, routes are exchanged between the routers, and
-end-to-end connectivity is achieved.
-
-An end-device (HOST) is connected to R2 on an interface without OSPF enabled.
-This verifies that OSPF status information remains accessible when a router
-has non-OSPF interfaces.
-
-==== Topology
-
-image::topology.svg[OSPF Basic topology, align=center, scaledwidth=75%]
-
-==== Sequence
-
-. Set up topology and attach to target DUTs
-. Configure targets
-. Wait for OSPF routes
-. Verify R2 OSPF neighbors with non-OSPF interface
-. Test connectivity from PC:data to 192.168.200.1
-
-
diff --git a/test/case/routing/ospf_basic/test.py b/test/case/routing/ospf_basic/test.py
index eae9d04f0..23e4d7b53 100755
--- a/test/case/routing/ospf_basic/test.py
+++ b/test/case/routing/ospf_basic/test.py
@@ -1,15 +1,17 @@
#!/usr/bin/env python3
-"""OSPF Basic
+"""{version} Basic
-Verifies basic OSPF functionality by configuring two routers (R1 and R2)
-with OSPF on their interconnecting link. The test ensures OSPF
+Verifies basic {version} functionality by configuring two routers (R1 and R2)
+with {version} on their interconnecting link. The test ensures {version}
neighbors are established, routes are exchanged between the routers, and
end-to-end connectivity is achieved.
-An end-device (HOST) is connected to R2 on an interface without OSPF enabled.
-This verifies that OSPF status information remains accessible when a router
-has non-OSPF interfaces.
+An end-device (HOST) is connected to R2 on an interface without {version}
+enabled. This verifies that {version} status information remains accessible
+when a router has non-{version} interfaces.
+Note: OSPFv3 has no IPv4 address to derive a router-id from, so an
+explicit-router-id is configured when running OSPFv3.
"""
# TODO: Remove HOST node once Infamy supports unconnected ports in topologies
@@ -19,42 +21,71 @@
from infamy.util import until, parallel
-def config_target1(target, data, link):
+class ArgumentParser(infamy.ArgumentParser):
+ def __init__(self):
+ super().__init__()
+ self.add_argument("--version", type=str.lower, choices=["ospfv2", "ospfv3"])
+
+
+PARAM = {
+ "ospfv2": {
+ "af": "ipv4",
+ "len": 24,
+ "hostlen": 32,
+ "R1data": "192.168.10.1",
+ "R1link": "192.168.50.1",
+ "R1lo": "192.168.100.1",
+ "R2link": "192.168.50.2",
+ "R2data": "192.168.60.1",
+ "R2lo": "192.168.200.1",
+ "HOSTlink": "192.168.60.2",
+ "blackhole": "192.168.33.1",
+ "PC": "192.168.10.2",
+ },
+ "ospfv3": {
+ "af": "ipv6",
+ "len": 64,
+ "hostlen": 128,
+ "router-id": {"R1": "1.1.1.1", "R2": "2.2.2.2"},
+ "R1data": "2001:db8:10::1",
+ "R1link": "2001:db8:50::1",
+ "R1lo": "2001:db8:100::1",
+ "R2link": "2001:db8:50::2",
+ "R2data": "2001:db8:60::1",
+ "R2lo": "2001:db8:200::1",
+ "HOSTlink": "2001:db8:60::2",
+ "blackhole": "2001:db8:33::1",
+ "PC": "2001:db8:10::2",
+ },
+}
+
+
+def iface(p, name, addr, prefix_length=None, forwarding=True):
+ """Interface with a single address of the tested address family"""
+ ip = {"address": [{"ip": addr, "prefix-length": prefix_length or p["len"]}]}
+ if forwarding:
+ ip["forwarding"] = True
+
+ return {"name": name, "enabled": True, p["af"]: ip}
+
+
+def ospf(p, name):
+ """OSPF instance settings, OSPFv3 needs an explicit router-id"""
+ conf = {}
+ if "router-id" in p:
+ conf["explicit-router-id"] = p["router-id"][name]
+
+ return conf
+
+
+def config_target1(target, data, link, p):
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
- {
- "name": data,
- "enabled": True,
- "ipv4": {
- "forwarding": True,
- "address": [{
- "ip": "192.168.10.1",
- "prefix-length": 24
- }]}
- },
- {
- "name": link,
- "enabled": True,
- "ipv4": {
- "forwarding": True,
- "address": [{
- "ip": "192.168.50.1",
- "prefix-length": 24
- }]
- }
- },
- {
- "name": "lo",
- "enabled": True,
- "ipv4": {
- "address": [{
- "ip": "192.168.100.1",
- "prefix-length": 32
- }]
- }
- }
+ iface(p, data, p["R1data"]),
+ iface(p, link, p["R1link"]),
+ iface(p, "lo", p["R1lo"], p["hostlen"], forwarding=False)
]
}
},
@@ -65,9 +96,9 @@ def config_target1(target, data, link):
"type": "infix-routing:static",
"name": "default",
"static-routes": {
- "ipv4": {
+ p["af"]: {
"route": [{
- "destination-prefix": "192.168.33.1/32",
+ "destination-prefix": f"{p['blackhole']}/{p['hostlen']}",
"next-hop": {
"special-next-hop": "blackhole"
}
@@ -75,9 +106,10 @@ def config_target1(target, data, link):
}
}
}, {
- "type": "infix-routing:ospfv2",
+ "type": f"infix-routing:{p['version']}",
"name": "default",
"ospf": {
+ **ospf(p, "R1"),
"redistribute": {
"redistribute": [{
"protocol": "static"
@@ -106,50 +138,25 @@ def config_target1(target, data, link):
})
-def config_target2(target, link, data):
+def config_target2(target, link, data, p):
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
- "interface": [{
- "name": link,
- "enabled": True,
- "ipv4": {
- "forwarding": True,
- "address": [{
- "ip": "192.168.50.2",
- "prefix-length": 24
- }]
- }
- }, {
- "name": data,
- "enabled": True,
- "ipv4": {
- "forwarding": True,
- "address": [{
- "ip": "192.168.60.1",
- "prefix-length": 24
- }]
- }
- }, {
- "name": "lo",
- "enabled": True,
- "forwarding": True,
- "ipv4": {
- "address": [{
- "ip": "192.168.200.1",
- "prefix-length": 32
- }]
- }
- }]
+ "interface": [
+ iface(p, link, p["R2link"]),
+ iface(p, data, p["R2data"]),
+ iface(p, "lo", p["R2lo"], p["hostlen"], forwarding=False)
+ ]
}
},
"ietf-routing": {
"routing": {
"control-plane-protocols": {
"control-plane-protocol": [{
- "type": "infix-routing:ospfv2",
+ "type": f"infix-routing:{p['version']}",
"name": "default",
"ospf": {
+ **ospf(p, "R2"),
"redistribute": {
"redistribute": [{
"protocol": "connected"
@@ -176,20 +183,11 @@ def config_target2(target, link, data):
})
-def config_host(target, link):
+def config_host(target, link, p):
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
- "interface": [{
- "name": link,
- "enabled": True,
- "ipv4": {
- "address": [{
- "ip": "192.168.60.2",
- "prefix-length": 24
- }]
- }
- }]
+ "interface": [iface(p, link, p["HOSTlink"], forwarding=False)]
}
}
})
@@ -197,7 +195,10 @@ def config_host(target, link):
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUTs"):
- env = infamy.Env()
+ env = infamy.Env(args=ArgumentParser())
+ version = env.args.version
+ param = PARAM[version] | {"version": version}
+
R1, R2, HOST = parallel(lambda: env.attach("R1", "mgmt"),
lambda: env.attach("R2", "mgmt"),
lambda: env.attach("HOST", "mgmt"))
@@ -209,23 +210,27 @@ def config_host(target, link):
_, R2data = env.ltop.xlate("R2", "data")
_, HOSTlink = env.ltop.xlate("HOST", "link")
- parallel(lambda: config_target1(R1, R1data, R1link),
- lambda: config_target2(R2, R2link, R2data),
- lambda: config_host(HOST, HOSTlink))
- with test.step("Wait for OSPF routes"):
- until(lambda: route.ipv4_route_exist(R1, "192.168.200.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
- until(lambda: route.ipv4_route_exist(R2, "192.168.100.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
- until(lambda: route.ipv4_route_exist(R2, "192.168.33.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
+ parallel(lambda: config_target1(R1, R1data, R1link, param),
+ lambda: config_target2(R2, R2link, R2data, param),
+ lambda: config_host(HOST, HOSTlink, param))
+
+ with test.step(f"Wait for {version} routes"):
+ af, plen = param["af"], param["hostlen"]
+ proto = f"ietf-ospf:{version}"
+
+ until(lambda: route.route_exist(R1, f"{param['R2lo']}/{plen}", af=af, proto=proto), attempts=200)
+ until(lambda: route.route_exist(R2, f"{param['R1lo']}/{plen}", af=af, proto=proto), attempts=200)
+ until(lambda: route.route_exist(R2, f"{param['blackhole']}/{plen}", af=af, proto=proto), attempts=200)
- with test.step("Verify R2 OSPF neighbors with non-OSPF interface"):
+ with test.step(f"Verify R2 {version} neighbors with non-{version} interface"):
# Regression test for #1169
- assert route.ospf_has_neighbors(R2)
+ assert route.ospf_has_neighbors(R2, proto=f"infix-routing:{version}")
- with test.step("Test connectivity from PC:data to 192.168.200.1"):
+ with test.step("Test connectivity from PC:data to R2 loopback"):
_, hport0 = env.ltop.xlate("PC", "data")
with infamy.IsolatedMacVlan(hport0) as ns0:
- ns0.addip("192.168.10.2")
- ns0.addroute("192.168.200.1/32", "192.168.10.1")
- ns0.must_reach("192.168.200.1")
+ ns0.addip(param["PC"], prefix_length=param["len"], proto=af)
+ ns0.addroute(f"{param['R2lo']}/{plen}", param["R1data"], proto=af)
+ ns0.must_reach(param["R2lo"])
test.succeed()
diff --git a/test/case/routing/ospf_basic/test.yaml b/test/case/routing/ospf_basic/test.yaml
new file mode 100644
index 000000000..e4feb5075
--- /dev/null
+++ b/test/case/routing/ospf_basic/test.yaml
@@ -0,0 +1,11 @@
+---
+- settings:
+ test-spec: .adoc
+
+- name: OSPFv2 Basic
+ case: ospfv2.py
+ opts: ["--version", "OSPFv2"]
+
+- name: OSPFv3 Basic
+ case: ospfv3.py
+ opts: ["--version", "OSPFv3"]
diff --git a/test/case/routing/ospf_basic/topology.dot b/test/case/routing/ospf_basic/topology.dot
index cb1841563..aa3b84be6 100644
--- a/test/case/routing/ospf_basic/topology.dot
+++ b/test/case/routing/ospf_basic/topology.dot
@@ -1,4 +1,7 @@
graph "2x2" {
+ labelloc="b";
+ label="IPv4 addresses shown, the IPv6 run uses the matching 2001:db8: prefixes";
+ fontname="DejaVu Serif, Book";
layout="neato";
overlap="false";
esep="+20";
diff --git a/test/case/routing/ospf_basic/topology.svg b/test/case/routing/ospf_basic/topology.svg
index 258b6726c..d3c139b94 100644
--- a/test/case/routing/ospf_basic/topology.svg
+++ b/test/case/routing/ospf_basic/topology.svg
@@ -3,99 +3,100 @@
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-