diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md
index d6d9fa15a..003a5aca3 100644
--- a/doc/ChangeLog.md
+++ b/doc/ChangeLog.md
@@ -43,6 +43,9 @@ All notable changes to the project are documented in this file.
editor, show mesh peers on the WiFi and interface status pages, and add
an editor section for access point roaming (802.11k/r/v, band steering,
OKC).
+- 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
### Fixes
diff --git a/doc/routing.md b/doc/routing.md
index f8b99e588..5baac8d13 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,64 @@ 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.
+
+> [!CAUTION]
+> Neighbors cannot be authenticated, in either OSPF version, so any host
+> on an enabled link can inject or poison routes.
+
+
+### 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. Configurations without it are rejected.
+- 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 +483,55 @@ 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.
+
+> [!CAUTION]
+> Messages cannot be authenticated, in either RIP version, so any host on
+> an enabled link can inject or poison routes.
+
+
+### 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 +607,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 +635,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 df239de5f..aae4fea3c 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;
@@ -514,21 +643,35 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly
break;
case SR_EV_ABORT: /* User abort, or other plugin failed */
+ /* Drop every next-config, a stale one would otherwise be
+ * activated by a later commit that does not touch its
+ * protocol. */
(void)remove(NETD_CONF_NEXT);
+ (void)remove(OSPFD_CONF_NEXT);
+ (void)remove(OSPF6D_CONF_NEXT);
+ (void)remove(RIPD_SIGNAL_NEXT);
+ (void)remove(RIPNGD_SIGNAL_NEXT);
+ (void)remove(BFDD_SIGNAL_NEXT);
return SR_ERR_OK;
case SR_EV_DONE:
/* 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 +693,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 +720,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 +742,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 +774,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 62ba37b9d..7cc61fc27 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..aeb44daf3 100644
--- a/src/confd/yang/confd/infix-routing.yang
+++ b/src/confd/yang/confd/infix-routing.yang
@@ -26,6 +26,21 @@ 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. Require
+ explicit-router-id for OSPFv3, there is no IPv4 interface
+ address to derive a router ID from. 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 +173,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 +332,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";
@@ -361,6 +391,14 @@ module infix-routing {
}
}
+ deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf" {
+ deviate add {
+ must "not(derived-from-or-self(../rt:type, 'infix-rt:ospfv3')) or "
+ + "ospf:explicit-router-id" {
+ error-message "OSPFv3 requires an explicit-router-id.";
+ }
+ }
+ }
deviation "/rt:routing/rt:control-plane-protocols/rt:control-plane-protocol/ospf:ospf/ospf:auto-cost" {
deviate not-supported;
@@ -379,6 +417,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 +500,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 +721,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 +781,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 +832,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 +857,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 60b8f670a..ac6304eda 100644
--- a/src/klish-plugin-infix/xml/infix.xml
+++ b/src/klish-plugin-infix/xml/infix.xml
@@ -739,6 +739,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 c6b254b95..bdaf22433 100755
--- a/src/statd/python/cli_pretty/cli_pretty.py
+++ b/src/statd/python/cli_pretty/cli_pretty.py
@@ -5019,16 +5019,33 @@ 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') set by the
+ show command. With a hint, only that family is considered, so 'show ipv6
+ ospf' never reports the OSPFv2 instance. Without one, fall 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', [])
+ afi = json_data.get('_afi')
+ want_type = 'ospfv3' if 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 None if afi else 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 +5116,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 +5335,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 +5417,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..8accac9c0 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,122 @@ 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_state(state):
+ """Map an FRR interface state to an ietf-ospf if-state-type.
+
+ It is the ISM state from RFC 2328, which has no point-to-multipoint
+ member, so ospf6d's "PtMultipoint" has nothing to map to. It returns None
+ and the leaf is left out, a value outside the enumeration makes sysrepo
+ reject the whole operational tree."""
+ xlate = {
+ "DependUpon": "down", # ospfd, internal placeholder
+ "Down": "down",
+ "Waiting": "waiting",
+ "Loopback": "loopback",
+ "Point-To-Point": "point-to-point", # ospfd
+ "PointToPoint": "point-to-point", # ospf6d
+ "DROther": "dr-other",
+ "Backup": "bdr", # ospfd
+ "BDR": "bdr", # ospf6d
+ "DR": "dr",
+ }
+
+ return xlate.get(state)
+
+
+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
+ route = {"prefix": prefix}
- nexthops = []
- routetype = info["routeType"].split(" ")
-
- 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 +152,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,33 +207,14 @@ 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"
-
- if iface.get("state"):
- # Wev've never seen "DependUpon", and has no entry in
- # the YANG model, but is listed before down in Frr
- xlate = {
- "DependUpon": "down",
- "Down": "down",
- "Waiting": "waiting",
- "Loopback": "loopback",
- "Point-To-Point": "point-to-point",
- "DROther": "dr-other",
- "Backup": "bdr",
- "DR": "dr"
- }
- val = xlate.get(iface["state"], "unknown")
- interface["state"] = val
+ itype = ospf_interface_type(iface["networkType"],
+ iface.get("p2mpNonBroadcast", False))
+ if itype:
+ interface["interface-type"] = itype
+
+ state = ospf_interface_state(iface.get("state"))
+ if state:
+ interface["state"] = state
# Interface priority (for DR/BDR election)
if iface.get("priority") is not None:
@@ -247,6 +313,87 @@ 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"
+
+ 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"]
+
+ state = ospf_interface_state(iface.get("ospf6InterfaceState"))
+ if state:
+ interface["state"] = state
+
+ 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 +403,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">
-