Skip to content

ospf: RFC 9129 ietf-ospf YANG support (operational state + config-write)#22058

Draft
lamestllama wants to merge 45 commits into
FRRouting:masterfrom
lamestllama:ospfd-yang-rfc9129-v2
Draft

ospf: RFC 9129 ietf-ospf YANG support (operational state + config-write)#22058
lamestllama wants to merge 45 commits into
FRRouting:masterfrom
lamestllama:ospfd-yang-rfc9129-v2

Conversation

@lamestllama
Copy link
Copy Markdown

@lamestllama lamestllama commented May 25, 2026

Overview

Adds RFC 9129 ietf-ospf YANG support for the implemented FRR-backed
OSPFv2 and OSPFv3 subset through mgmtd.

The implementation is anchored on the IETF model. FRR-specific behaviour stays
on the existing CLI surface unless there is a concrete FRR-native augment with
callbacks.

This PR provides the OSPF YANG model integration, callback wiring, mgmtd
dispatch support needed for the shared OSPFv2/OSPFv3 subtree, RPC callbacks,
notifications, user documentation and developer notes.

Generic gRPC Execute and Subscribe support through mgmtd is handled in
#22158. When #22158 is present, external gRPC clients can use the same mgmtd
frontend/backend machinery for OSPF RPCs and notifications. This PR does not
depend on #22158.

What This PR Provides

Operators get broad RFC 9129 YANG configuration, RPC and operational state
coverage for the implemented FRR-backed OSPF subset.

The supported mgmtd-facing surfaces are:

  • vtysh through mgmt set-config, mgmt rpc and mgmt commit apply
  • the mgmtd frontend socket binary protocol
  • daemon-backed operational state, RPC and notification callbacks registered
    with mgmtd

The OSPFv2 and OSPFv3 implementations share the RFC 9129
control-plane-protocol subtree. Requests are routed to the correct daemon by
matching the predicated protocol entry, for example the OSPFv2 or OSPFv3
type and name keys.

Architecture

RFC 9129 puts OSPFv2 and OSPFv3 under the shared RFC 8349
control-plane-protocol list, distinguished by type and name keys. That is
the standard shape, but it means ospfd and ospf6d share one schema subtree
and must receive only the requests that belong to their daemon and instance.

The series adds reusable infrastructure for that shape:

Area Purpose
Operational callbacks with suppressed config callbacks Allows operational callbacks on modules that suppress config callbacks.
Per-node config opt-in Lets selected writable nodes in a mostly suppressed standard module opt into config callbacks.
Batched daemon config-file loads Gives daemon-direct startup files the same single-transaction semantics vtysh already gets.
Predicate-aware backend xpath matching Lets backends register predicated list entries such as control-plane-protocol[type='ietf-ospf:ospfv2'].

For multi-instance OSPFv2, backend client names are made instance-aware:
non-instanced daemons keep their existing names, while instanced daemons append
the decimal instance ID, such as ospfd-1 or ospfd-2. The RFC 9129 protocol
name in instance mode is the decimal instance ID.

Configuration Coverage

Per-instance leaves:

RFC 9129 path OSPFv2 OSPFv3 Notes
explicit-router-id yes yes
preference/{all,intra-area,inter-area,internal,external} yes yes RFC internal maps onto intra/inter-area distances.
spf-control/paths yes yes Capped at MULTIPATH_NUM.
auto-cost/{enabled,reference-bandwidth} yes yes FRR has no off-switch, so enabled=false is rejected at validation.
mpls/ldp/igp-sync yes N/A ospf6d has no LDP/IGP-sync.
mpls/te-rid/ipv4-router-id yes N/A ospf6d has no MPLS-TE.
graceful-restart/{enabled,restart-interval,helper-enabled,helper-strict-lsa-checking} yes yes
stub-router/always yes N/A ospf6d has no stub-router.

Per-area:

RFC 9129 path OSPFv2 OSPFv3
areas/area lifecycle yes yes
area/area-type normal, stub and NSSA yes yes
area/summary yes yes
area/default-cost yes N/A
area/ranges/range, advertise and cost yes yes

Per-interface:

RFC 9129 path OSPFv2 OSPFv3
interfaces/interface area attachment yes yes
interface/cost yes yes
interface/{hello,dead,retransmit}-interval yes yes
interface/priority yes yes
interface/mtu-ignore yes yes
interface/transmit-delay yes yes
interface/interface-type yes yes
interface/passive yes yes
interface/prefix-suppression yes N/A
interface/bfd/{enabled,local-multiplier,desired-min-tx-interval,required-min-rx-interval} yes yes
interface/static-neighbors/neighbor, poll-interval and priority yes N/A
interface/authentication/ospfv2-key-chain / ospfv3-key-chain yes yes

RPCs

RFC 9129 RPC OSPFv2 OSPFv3 Notes
clear-neighbor yes yes Whole-instance and per-interface variants.
clear-database yes yes Maps onto ospf{,6}_process_reset.

Notifications

RFC 9129 notification OSPFv2 OSPFv3 Source
nbr-state-change yes yes ospf_nsm_change / ospf6_neighbor_change hooks
if-state-change yes yes ospf_ism_change / ospf6_interface_change hooks
restart-status-change yes yes Direct emit from GR enter and exit
nbr-restart-helper-status-change yes yes Direct emit from GR helper enter and exit
if-rx-bad-packet yes yes Post-header-validation failure path
if-config-error yes yes Hello/dead interval mismatch checks
nssa-translator-status-change yes N/A ABR NSSA translator transition
lsdb-{approaching-,}overflow N/A N/A FRR has no max-LSA threshold matching this model.

Out of Scope

Documented in doc/developer/ospf-yang-northbound-notes.rst.

Deferred because there is no matching FRR surface, the FRR surface has a
different shape, or the RFC 9129 path needs a separate future mapping:

  • Redistribution and default-information-originate
  • Virtual links
  • Per-address OSPFv2 interface overrides
  • FRR-specific NSSA knobs
  • ospf/nsr
  • database-control/max-lsa
  • spf-control/ietf-spf-delay
  • node-tag-config
  • interface/{enabled,multi-areas,ttl-security}
  • ospf/enabled
  • address-family
  • per-interface fast-reroute/lfa
  • nested authentication choice branches that pyang/libyang reject through the
    current deviation paths

Related Work

Two earlier OSPF YANG attempts shaped the choices here:

FRR also has an experimental YANG-module translator for mapping non-native
models onto native FRR models via deviation modules and XPath translation
tables. This PR does not use it because OSPF does not yet have a complete
callback-backed native OSPF YANG model to serve as the source of truth. RFC
9129 is implemented directly as the canonical northbound surface for the OSPF
behaviour it covers.

Documentation

  • doc/user/ospfd.rst and doc/user/ospf6d.rst: supported leaves,
    daemon-instance naming and worked mgmt set-config / mgmt commit apply
    examples.
  • doc/developer/ospf-yang-northbound-notes.rst: RFC 9129 design choice,
    predicate-aware dispatch, startup batching, validation approach,
    notification wiring and future FRR-native augment boundaries.

IETF Module Sources

ietf-ospf.yang, ietf-routing.yang, ietf-bfd-types.yang,
iana-routing-types.yang and iana-bfd-types.yang are pulled from their
respective RFCs unchanged, with the IETF Trust BSD licence text.

This follows the existing handling of ietf-interfaces.yang,
ietf-key-chain.yang and ietf-routing-types.yang already in yang/ietf/.

Commit Organisation

The series is organised so that reusable infrastructure lands before the OSPF
callbacks that depend on it:

  1. Reusable lib and mgmtd infrastructure.
  2. RFC 9129 operational state.
  3. Per-leaf config-write support and matching legacy CLI conversions.
  4. RFC 9129 RPC callbacks.
  5. RFC 9129 notification callbacks.
  6. User and developer documentation.

Each commit is intended to be reviewable in isolation.

@Jafaral
Copy link
Copy Markdown
Member

Jafaral commented May 25, 2026

Please stop closing/opening PRs for the same changes. Update your branch and force push to the same PR.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 25, 2026

Greptile Summary

This PR implements broad RFC 9129 ietf-ospf YANG northbound support for FRR's OSPFv2 and OSPFv3 daemons via mgmtd, including config-write callbacks, operational state, RPCs, and notifications. It also lands reusable infrastructure: predicate-aware backend XPath matching so ospfd and ospf6d can share the RFC 8349 control-plane-protocol list, per-node cfg_opt_in for incremental module conversion, and a gRPC-to-mgmtd RPC bridge.

  • Config coverage: per-instance, per-area, and per-interface RFC 9129 leaves are wired to FRR internals for both OSPFv2 and OSPFv3; previously-flagged bugs (use-after-free in area-range drain, virtual-link check silently dropped at APPLY, atomic interface-area move rejected at VALIDATE, wrong multi-instance XPath from vrf->info) are all addressed in this revision.
  • Missing destroy callbacks: hello-interval and dead-interval register only a modify callback (no destroy) in both ospfd/ospf_nb.c and ospf6d/ospf6_nb.c; deleting either leaf from the YANG candidate leaves the corresponding FRR timer value at its last operator-set value while the running datastore reverts to the RFC default, causing a persistent split between the YANG plane and what ospfd/ospf6d actually uses.

Confidence Score: 3/5

Generally safe to review further but the hello/dead-interval timer omission means YANG-managed configs can silently diverge from FRR's running state after a delete operation.

The timer destroy gap (hello-interval and dead-interval, both OSPFv2 and OSPFv3) means that once an operator sets these values via YANG and then removes them, FRR keeps the old values indefinitely while the YANG datastore reports RFC defaults — a silent, persistent config/oper split that is difficult to detect and cannot be recovered through YANG alone.

ospfd/ospf_nb.c and ospf6d/ospf6_nb.c (callback registration for hello-interval and dead-interval), and their corresponding implementation files ospfd/ospf_nb_config.c and ospf6d/ospf6_nb_config.c where the destroy functions need to be added.

Important Files Changed

Filename Overview
ospfd/ospf_nb_config.c New OSPFv2 RFC 9129 config callbacks; missing destroy for hello-interval and dead-interval leaves causes persistent config/oper state split when these are deleted from YANG.
ospf6d/ospf6_nb_config.c New OSPFv3 RFC 9129 config callbacks; same missing destroy callbacks for hello-interval and dead-interval as the OSPFv2 side.
mgmtd/mgmt_be_adapter.c Adds predicate-aware XPath matching so OSPFv2/OSPFv3 backends can share the RFC 9129 control-plane-protocol YANG list while only receiving requests for their own instance; parser logic and segment/predicate walking look correct.
mgmtd/mgmt_grpc.c New gRPC RPC dispatch bridge with synchronous (event-loop reentrant) and async paths; refcount and mutex logic is sound, re-entrancy caveat is well documented.
lib/northbound.c Removes F_NB_NODE_IGNORE_CFG_CBS guards from oper callbacks (replaced with explicit null checks), adds per-node cfg_opt_in lift mechanism, and wires global RPC dispatch hooks.
ospfd/ospf_vty.c Converts CLI commands to YANG-backed DEFPY_YANG; ospf_per_iface_xpath now correctly resolves multi-instance via ospf_interface backpointers instead of vrf->info, fixing the previously-noted XPath bug.
ospfd/ospf_nb_notifications.c New OSPFv2 RFC 9129 notification wiring; NSM/ISM state translations are correct, DR/BDR enum remapping from FRR ordering to RFC ordering is properly handled.
ospfd/ospf_nb.c YANG module registration for ietf-ospf with ignore_cfg_cbs + per-node cfg_opt_in opt-ins; hello-interval and dead-interval are registered without destroy callbacks.
ospf6d/ospf6_nb.c OSPFv3 YANG module registration; mirrors OSPFv2 structure with same missing destroy registrations for hello-interval and dead-interval.
yang/ietf/ietf-ospf.yang Adds RFC 9129 ietf-ospf.yang verbatim from the RFC with IETF Trust BSD licence; no code changes.
yang/ietf/frr-deviations-ietf-routing-ospf.yang FRR deviation module relaxing leafref constraints and suppressing unimplemented features; approach is sound and consistent with existing FRR ietf deviation modules.

Sequence Diagram

sequenceDiagram
    participant CLI as vtysh CLI
    participant NB as northbound
    participant MGMTD as mgmtd
    participant OSPFD as ospfd backend
    participant OSPF6D as ospf6d backend

    Note over CLI,OSPF6D: Config write path (e.g. area stub)
    CLI->>NB: "nb_cli_enqueue_change(area-type=stub-area)"
    NB->>MGMTD: candidate datastore update
    MGMTD->>MGMTD: libyang VALIDATE (when/must clauses)
    MGMTD->>OSPFD: NB_EV_VALIDATE callback
    OSPFD-->>MGMTD: NB_OK (vlink check at VALIDATE)
    MGMTD->>OSPFD: NB_EV_APPLY callback
    OSPFD-->>MGMTD: ospf_area_stub_set()

    Note over CLI,OSPF6D: Predicate-aware dispatch (RFC 8349 shared list)
    MGMTD->>MGMTD: mgmt_be_xpath_prefix(map_path, xpath)
    MGMTD->>MGMTD: strip predicates + check compatibility
    MGMTD->>OSPFD: "route type=ietf-ospf:ospfv2 requests here"
    MGMTD->>OSPF6D: "route type=ietf-ospf:ospfv3 requests here"

    Note over CLI,OSPF6D: RPC path (clear-neighbor)
    CLI->>MGMTD: mgmt rpc /ietf-ospf:clear-neighbor
    MGMTD->>MGMTD: mgmt_grpc_rpc_event (or FE adapter)
    MGMTD->>OSPFD: mgmt_txn_send_rpc_notify
    OSPFD-->>MGMTD: RPC result
    MGMTD-->>CLI: response

    Note over CLI,OSPF6D: Notification path
    OSPFD->>NB: nb_notification_send(nbr-state-change)
    NB->>MGMTD: dispatch to subscribers
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
ospfd/ospf_nb_config.c:946-999
**Missing `destroy` callbacks for `hello-interval` and `dead-interval`**

Neither `hello-interval` nor `dead-interval` registers a destroy callback (confirmed in `ospfd/ospf_nb.c` registration tables). When an operator deletes either leaf from the YANG candidate, the northbound framework calls `nb_callback_destroy`, finds `cbs.destroy == NULL`, and silently returns `NB_OK` — leaving `params->v_hello` / `params->v_wait` / `params->is_v_wait_set` at their last-set values while the YANG running config no longer carries the explicit leaf.

The immediate consequence is a permanent split: the YANG datastore reports the RFC default (10 s / 40 s) while ospfd keeps the old operator-supplied value. Any subsequent `show run``mgmt commit apply` cycle based on YANG output will omit those intervals, leaving ospfd stuck with stale timers indefinitely.

Every sibling that has a non-trivial revert (`retransmit-interval`, `priority`, `cost`, etc.) registers an explicit destroy. `hello-interval` and `dead-interval` need the same treatment, resetting `params->v_hello` to `OSPF_HELLO_INTERVAL_DEFAULT`, unsetting `SET_IF_PARAM(params, v_hello)`, resetting `params->v_wait` to `OSPF_ROUTER_DEAD_INTERVAL_DEFAULT` when `is_v_wait_set` is false (the auto-derived path), clearing `is_v_wait_set`, and then calling the matching timer-update helpers.

### Issue 2 of 2
ospf6d/ospf6_nb_config.c:751-830
**Missing `destroy` callbacks for `hello-interval` and `dead-interval` (OSPFv3 mirror)**

Same issue as the OSPFv2 side: `ospf6d/ospf6_nb.c` registers only `modify` for these two leaves. Deleting them from the candidate leaves `oi->hello_interval` and `oi->dead_interval` at whatever value the last `modify` set, while the YANG running config shows the RFC defaults.

The OSPFv3 destroy should reset `oi->hello_interval` to `OSPF6_INTERFACE_HELLO_INTERVAL` and `oi->dead_interval` to `OSPF6_INTERFACE_DEAD_INTERVAL`, and call the appropriate timer-update helper, mirroring what the interface destroy already does for these fields.

Reviews (25): Last reviewed commit: "ospf: RFC 9129 nssa-translator-status-ch..." | Re-trigger Greptile

Comment thread ospfd/ospf_nb_config.c
@lamestllama lamestllama force-pushed the ospfd-yang-rfc9129-v2 branch from 5b9699b to 4b9886d Compare May 25, 2026 01:43
Comment thread ospfd/ospf_nb_config.c Outdated
@lamestllama lamestllama force-pushed the ospfd-yang-rfc9129-v2 branch from 4b9886d to 1bfe80e Compare May 25, 2026 02:06
Comment thread ospfd/ospf_nb_config.c
Comment thread ospf6d/ospf6_nb_config.c
@lamestllama lamestllama force-pushed the ospfd-yang-rfc9129-v2 branch 3 times, most recently from ed9b4d6 to 05b4fac Compare May 25, 2026 03:20
@lamestllama lamestllama marked this pull request as ready for review May 25, 2026 04:42
@lamestllama lamestllama marked this pull request as draft May 25, 2026 06:10
@riw777 riw777 self-requested a review May 25, 2026 11:51
@lamestllama lamestllama force-pushed the ospfd-yang-rfc9129-v2 branch 2 times, most recently from 32dcdb3 to c48c147 Compare May 25, 2026 12:39
@frrbot frrbot Bot added the mgmt FRR Management Infra label May 25, 2026
@lamestllama lamestllama force-pushed the ospfd-yang-rfc9129-v2 branch 4 times, most recently from c1ac4db to af790dd Compare May 25, 2026 22:44
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 25, 2026

Want your agent to iterate on Greptile's feedback? Try greploops.

@lamestllama lamestllama force-pushed the ospfd-yang-rfc9129-v2 branch 2 times, most recently from 777454f to ada554e Compare May 26, 2026 00:32
@frrbot frrbot Bot added the ospfv3 label May 26, 2026
@lamestllama lamestllama force-pushed the ospfd-yang-rfc9129-v2 branch from e5b76ea to 2a634e7 Compare May 26, 2026 05:21
… batching

Add a single-router topology that exercises the daemon-direct
startup config-file batching path added by

    lib: batch direct-daemon config-file loads into one NB transaction

The ospfd fixture places

    area 0.0.0.61 default-cost 31
    area 0.0.0.61 stub no-summary

with default-cost *before* the stub-area line. With per-line commit
the first line fails RFC 9129's "default-cost is valid only on
stub/NSSA areas" when clause and the daemon never reaches the
stub-area line. Only the batched path, where the whole file commits
in one northbound transaction, lets the two leaves validate against
each other and both end up in the committed running configuration.

The ospf6d fixture carries a matching stub area without default-cost
(v3 has no default-cost surface) so the v3 startup-batching code path
is exercised too.

The test asserts both stub-area lines are present in show
running-config after the topology has started.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Replace the older "operational data only" sections in doc/user/ospfd.rst
and doc/user/ospf6d.rst with a YANG / NETCONF Support entry that lists
every config-write leaf the ietf-ospf series now supports plus the
deliberate scope gaps:

  * redistribute and default-information-originate remain legacy-CLI
    only (RFC 9129 / RFC 8349 leave them to a separate import / export
    mechanism).
  * Per-address overrides (e.g. `ip ospf cost N A.B.C.D`) have no RFC
    9129 counterpart; YANG is strictly per-interface.
  * FRR-specific NSSA augments (translator-role, suppress-fa,
    default-information-originate) are not in the RFC 9129 area
    grouping; they remain legacy-CLI-only.
  * `router ospf [vrf NAME]` instance creation is still CLI-only;
    YANG operations that target a non-existent instance are rejected
    at VALIDATE with a clear error pointing at `router ospf`.
  * ospf6d has no per-area stub default-cost knob; the leaf is not
    implemented (mgmtd reports "no backend handles this path"). This
    matches the existing v3 CLI surface, not a regression introduced
    by this conversion.
  * ospf6 interface-type rejects non-broadcast and hybrid at VALIDATE
    (ospf6d only accepts broadcast, point-to-point and
    point-to-multipoint).

Adds a worked example showing mgmt set-config / commit apply for the
explicit-router-id leaf so operators see the end-to-end mgmtd path.
Documents the frr-deviations-ietf-routing-ospf interface-name leafref
relaxation and the restoration of referential checks inside the
per-interface callbacks.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
The converted ietf-ospf config leaves live below the ietf-routing control-plane-protocol list. The legacy router ospf and router ospf6 commands still create daemon instances directly, so they also need to seed and destroy the corresponding RFC 9129 parent list entry in the northbound candidate. Without that parent entry, child writes can apply in the daemon while the candidate tree is missing the list entry that owns them.

Register create and destroy callbacks for the OSPFv2 and OSPFv3 control-plane-protocol entries, route the CLI router/no-router lifecycle through those callbacks, and clear pending NB changes after direct daemon teardown. Keep OSPFv3 config-file batching enabled immediately before ospf6_init(), where the converted config wrappers are registered.

Share the RFC 9129 control-plane-protocol XPath helpers between module info, backend registration, and CLI wrappers, and use the daemon's existing instance-name helpers instead of repeating default-instance literals.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Finishes the per-interface RFC 9129 tunable set. Adds modify+destroy
callbacks for transmit-delay on both daemons, following the same
shape as retransmit-interval:

  * ospfd mutates `params->transmit_delay` on IF_DEF_PARAMS with the
    SET_IF_PARAM marker; destroy restores OSPF_TRANSMIT_DELAY_DEFAULT
    (1) and is also folded into the per-interface destroy cascade.
  * ospf6d mutates `oi->transdelay` directly; destroy restores
    OSPF6_INTERFACE_TRANSDELAY (1).
  * Resolves the instance and interface from the dnode through the
    same _resolve_instance / _resolve_interface helpers used for the
    rest of the per-interface table, gating on NB_EV_APPLY.
  * No protocol side effects: transmit-delay is a passive flood-time
    scalar, so no neighbour reschedule or interface-state update is
    needed.

No YANG deviation is required because RFC 9129's transmit-delay has
no inter-leaf constraint analogous to dead > hello.

Topotest: adds test_ospf_yang_area_interface_transmit_delay_config
as a focused mgmtd round-trip test for both daemons; the existing
generic negative tests (test_ospf_yang_negative_missing_instance,
test_ospf_yang_negative_missing_interface) already cover the
validation rejections through the same _resolve_* chain.

Doc: extends the converted-leaves paragraph and adds the matching
row in the mapping table, per the convention at the bottom of the
table ("When adding another RFC 9129 config node, add its row here
before or alongside the callback implementation.").

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
…spf YANG

Extends the per-interface CLI-through-YANG conversion already done for
cost / hello-interval / dead-interval / priority / mtu-ignore / passive to
the two remaining per-interface scalars that have RFC 9129 callbacks:
retransmit-interval and transmit-delay.

Commands converted on each daemon:
  v2:  ip ospf retransmit-interval / no ip ospf retransmit-interval +
       the hidden `ospf retransmit-interval` / `no ospf retransmit-interval`
       backwards-compat aliases
       ip ospf transmit-delay / no ip ospf transmit-delay + the matching
       hidden `ospf transmit-delay` / `no ospf transmit-delay` aliases
  v3:  ipv6 ospf6 retransmit-interval / no ipv6 ospf6 retransmit-interval
       ipv6 ospf6 transmit-delay / no ipv6 ospf6 transmit-delay

Conversion pattern (matches the cost / hello / dead / priority / mtu-ignore
/ passive slice):

  * v2 commands take an optional `[A.B.C.D]` per-address override that
    has no RFC 9129 representation. Each DEFPY_YANG body checks
    `if (!ifaddr_str && per_iface_xpath returns 0)` and dispatches
    through nb_cli_enqueue_change / nb_cli_apply_changes; on the
    negative path it falls back to the legacy direct-mutation logic,
    preserving the per-address-override capability for operators that
    need it.
  * The YANG path also requires the interface to be in an area
    (if_area on IF_DEF_PARAMS). Operators setting per-interface attrs
    on an unattached interface continue to use the legacy path -- the
    YANG model can't express "interface params before area assignment"
    because the leaves live under areas/area[id]/interfaces.
  * Per-leaf bodies factor into small `_set_apply` / `_unset_apply`
    helpers shared by the main `ip ospf X` form and the hidden
    backwards-compat `ospf X` alias (v2 only).
  * v3 collapses the legacy DEFUN+ALIAS pair into two DEFPY_YANG forms
    (one for set, one for `no`), each guarded by `ospf6_per_iface_xpath`
    before falling back to direct ospf6_interface mutation.

Topotest: extends `test_ospf_per_iface_cli_routes_through_yang` to drive
`ip ospf retransmit-interval 23` + `ip ospf transmit-delay 31` (and v3
equivalents) on r1-eth1 alongside the previously-covered six leaves,
verifies each lands in `show running-config <daemon>`, then unwinds via
`no` and verifies absence. The `b3b` and `transmit-delay` focused
mgmtd-write tests continue to cover the YANG-side of the round-trip.

Doc: no change. The converted-leaves paragraph already lists
retransmit-interval and transmit-delay; the "Existing CLI commands for
those leaves set the same YANG nodes as mgmtd writes" sentence now
becomes accurate for these two.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Extends test_ospf_per_iface_cli_routes_through_yang to exercise
`ip ospf network point-to-point` and the v3 equivalent alongside
the other per-interface CLIs, and adds dedicated CLI tests for
the new YANG-routed paths:

  * test_ospf_network_dmvpn_falls_back_to_legacy -- confirms the
    FRR-augment fallback for `ip ospf network point-to-point dmvpn`
    keeps producing the expected running-config text and that
    `no ip ospf network` unwinds both the legacy and the YANG-
    backed state cleanly.

  * test_ospf_distance_cli_routes_through_yang -- cycles `distance
    137` / `no distance` and the three-scope `distance ospf
    intra-area X inter-area Y external Z` / `no distance ospf` on
    ospfd and asserts the running-config cycles in and out as
    expected. v3 path coverage stays in
    test_ospf_yang_preference_config because legacy distance on
    OSPF6_NODE additionally calls ospf6_restart_spf, which trips
    a pre-existing ospf6_route_remove_all use-after-free with a
    populated route table.

Coverage for the area attachment and area range CLI conversions
(`ip ospf area`, `ipv6 ospf6 area`, `area X range PREFIX [...]`)
relies on the existing
test_ospf_yang_area_interface_*_config / area_ranges_config tests,
which exercise the same YANG list-create / leaf-modify path via
`mgmt set-config`; a dedicated legacy-CLI fixture would require a
topology without `network <prefix> area X` statements and is left
for a follow-up.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Wires the per-instance maximum-ECMP-paths leaf on both ospfd and
ospf6d.  RFC 9129 types `/spf-control/paths` as uint16 (range
1..65535), which trivially covers FRR's compile-time `MULTIPATH_NUM`
cap (16 by default, up to 64 with `--enable-multipath=0` etc.).

What the slice contains:

  * `ospfd_ietf_ospf_spf_control_paths_modify` / `_destroy` and
    matching `ospf6d_*` callbacks that update
    `ospf->max_multipath` / `ospf6->max_multipath` and trigger SPF
    restart, mirroring the long-standing `ospf_maxpath_set` /
    `ospf6_maxpath_set` helpers.

  * Registration of `OSPF{,6}D_IETF_OSPF_XPATH "/spf-control/paths"`
    in the existing `ospfd_ietf_ospf_info` / `ospf6d_ietf_ospf_info`
    arrays with `cfg_opt_in = true`.

  * `DEFUN` -> `DEFPY_YANG` conversion of `maximum-paths N` /
    `no maximum-paths` on both daemons.  The grammar widens to
    `(1-65535)` so the YANG range fully drives clippy; the body
    enforces the FRR `MULTIPATH_NUM` cap and emits the canonical
    error message before any YANG dispatch.

  * Doc table row in
    `doc/developer/ospf-yang-northbound-notes.rst` and the converted
    leaves paragraph extended with `spf-control paths`.

  * Two topotests in `ospf_topo1/test_ospf_topo1.py`:
    `test_ospf_yang_spf_control_paths_config` round-trips the leaf
    via `mgmt set-config` for both daemons;
    `test_ospf_max_multipath_cli_routes_through_yang` confirms the
    legacy `maximum-paths` / `no maximum-paths` CLI continues to
    drive the YANG callback.

`show running-config` rendering is unchanged: the existing config
writer reads `max_multipath` from the FRR struct that the YANG
callback already mutates, so no `cli_show` is needed.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Wires the per-instance MPLS LDP/IGP-sync toggle for ospfd.  RFC 9129
models it as a boolean leaf `/mpls/ldp/igp-sync`.  ospf6d has no
LDP/IGP sync implementation, so this slice is OSPFv2-only -- the
ospf6d module-info array gets no corresponding entry and the doc
table flags the gap.

What the slice contains:

  * `ospfd_ietf_ospf_mpls_ldp_igp_sync_modify` / `_destroy` in
    `ospfd/ospf_nb_config.c`.  The modify callback validates that the
    instance is in the default VRF (FRR restricts LDP-sync to the
    default VRF; the legacy CLI rejects non-default with
    `ldp-sync only runs on DEFAULT VRF`).  Apply for `true` registers
    the opaque LDP-IGP zclient handlers and walks every interface in
    the VRF to start sync; apply for `false` and destroy both call
    `ospf_ldp_sync_gbl_exit(ospf, true)`, which clears the
    enable/holddown flags and unwinds per-interface state.

  * Registration of `OSPFD_IETF_OSPF_XPATH "/mpls/ldp/igp-sync"` in
    `ospfd_ietf_ospf_info` with `cfg_opt_in = true`, placed
    immediately after `/spf-control/paths`.

  * `DEFPY` -> `DEFPY_YANG` conversion of `mpls ldp-sync` /
    `no mpls ldp-sync` in `ospfd/ospf_ldp_sync.c`.  The bodies enqueue
    a YANG modify / destroy on the leaf and let the callback do the
    work, so vtysh and `mgmt set-config` produce the same datastore
    state.

  * `ospf_per_instance_xpath` is exposed via `ospfd/ospf_vty.h` so the
    new DEFPY_YANG bodies in `ospf_ldp_sync.c` can build the
    instance-level xpath without duplicating snprintf logic.

  * Doc table row in `doc/developer/ospf-yang-northbound-notes.rst`
    naming the ospfd owner and explicitly flagging the OSPFv3 gap.
    Converted-leaves paragraph extended with `OSPFv2 mpls/ldp/igp-sync`.

  * Two topotests in `ospf_topo1/test_ospf_topo1.py`:
    `test_ospf_yang_mpls_ldp_igp_sync_config` round-trips the leaf
    via `mgmt set-config`;
    `test_ospf_mpls_ldp_sync_cli_routes_through_yang` confirms the
    legacy `mpls ldp-sync` / `no mpls ldp-sync` CLI continues to
    drive the YANG callback.

`show running-config` rendering is unchanged: the existing
config-write reads `ospf->ldp_sync_cmd.flags` which the YANG callback
already mutates.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Wires the per-instance RFC 6987 unconditional stub-router presence
container for ospfd.  RFC 9129 models it under a `choice trigger`
between `stub-router` and `always`; because YANG `choice` and `case`
nodes don't appear in the data tree (RFC 7950 sec 7.9.2), the data
path skips straight to `/stub-router/always`.  ospf6d has no
stub-router implementation; this slice is OSPFv2-only and the
mapping table flags the gap.

What the slice contains:

  * `ospfd_ietf_ospf_stub_router_always_create` and `_destroy` in
    `ospfd/ospf_nb_config.c`.  Create walks every existing area to
    set `OSPF_AREA_ADMIN_STUB_ROUTED`, calls
    `ospf_router_lsa_update_area` for any area that wasn't already
    advertising stub, and arms `ospf->stub_router_admin_set =
    OSPF_STUB_ROUTER_ADMINISTRATIVE_SET` so later-created areas
    inherit the property.  Destroy unwinds with the same per-area
    walk, preserving any startup-timer-driven stub state already in
    flight (`area->t_stub_router`) so the unset doesn't trample on
    the boot-time stub timer.

  * Registration of `OSPFD_IETF_OSPF_XPATH "/stub-router/always"` in
    `ospfd_ietf_ospf_info` with `cfg_opt_in = true`, placed after
    `/mpls/ldp/igp-sync`.

  * `DEFUN` -> `DEFPY_YANG` conversion of `max-metric router-lsa
    administrative` / `no max-metric router-lsa administrative` in
    `ospfd/ospf_vty.c`.  Bodies enqueue `NB_OP_CREATE` / `NB_OP_DESTROY`
    on the presence container; the callback owns the per-area flag
    flip and the LSA reorigination.

  * Doc table row in `doc/developer/ospf-yang-northbound-notes.rst`
    naming the ospfd owner and flagging the OSPFv3 gap.  Converted-
    leaves paragraph extended with `OSPFv2 stub-router unconditional`.

  * Topotest in `ospf_topo1/test_ospf_topo1.py`:
    `test_ospf_max_metric_router_lsa_admin_cli_routes_through_yang`
    exercises the legacy CLI which routes through the YANG callback.
    A direct `mgmt set-config` round-trip test is intentionally
    omitted: presence containers carry no value, but FRR's mgmtd CLI
    `mgmt set-config WORD VALUE` grammar requires a VALUE token, so
    there's no clean entry point.  The CLI test covers the same
    create/destroy callback path.

`show running-config` rendering is unchanged: the existing config
writer at `ospfd/ospf_vty.c` reads `ospf->stub_router_admin_set`
which the YANG callback already mutates.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
…nversion

Wires the per-interface RFC 6860 prefix-suppression boolean for ospfd.
RFC 9129 leaf `/areas/area/interfaces/interface/prefix-suppression`,
type boolean.  ospf6d has no equivalent per-interface
prefix-suppression flag, so this slice is OSPFv2-only and the mapping
table flags the gap.

What the slice contains:

  * `ospfd_ietf_ospf_areas_area_interfaces_interface_prefix_`
    `suppression_modify` / `_destroy` in `ospfd/ospf_nb_config.c`.
    Mutates `params->prefix_suppression`, toggles the
    `OSPF_IF_PARAM_CONFIGURED` bit, and -- if the value actually
    changed -- reoriginates the Router-LSA on every adjacency plus
    the Network-LSA on any interface where this router is DR (helper
    `ospfd_ietf_ospf_prefix_suppression_lsa_update`).  Mirrors the
    legacy DEFPY's per-interface fan-out exactly.

  * Entry added to the per-interface destroy cascade in
    `ospfd_ietf_ospf_areas_area_interfaces_interface_destroy` so a
    subsequent re-create of the area/interface list entry starts from
    the compile-time default.

  * Registration of the per-interface xpath in
    `ospfd_ietf_ospf_info` with `.cfg_opt_in = true`, placed
    immediately after `interface/passive` to keep the source-order
    convention.

  * DEFPY -> DEFPY_YANG conversion of `[no] ip ospf prefix-suppression`
    `[A.B.C.D]` in `ospfd/ospf_vty.c`.  The whole-interface form
    (no per-address override) routes through the YANG callback; per-
    address overrides stay on the legacy direct-mutation path because
    RFC 9129's per-interface list cannot express them, and not-yet-
    attached interfaces fall back via the `ospf_per_iface_xpath`
    helper which is already gated on
    `OSPF_IF_PARAM_CONFIGURED(if_area)`.

  * Doc table row in `doc/developer/ospf-yang-northbound-notes.rst`
    naming the ospfd owner, calling out the OSPFv3 gap, and noting
    the LSA reorigination side effect.  Converted-leaves paragraph
    extended with `OSPFv2 prefix-suppression`.

  * Two topotests in `ospf_topo1/test_ospf_topo1.py`:
    `test_ospf_yang_prefix_suppression_config` round-trips the leaf
    via `mgmt set-config`;
    `test_ospf_prefix_suppression_cli_routes_through_yang` confirms
    the legacy `ip ospf prefix-suppression` / `no ...` CLI without a
    per-address override drives the YANG callback.

`show running-config` rendering is unchanged: the existing
config-write reads `params->prefix_suppression` which the YANG
callback already mutates.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Adds the per-instance /auto-cost/enabled and /auto-cost/reference-bandwidth
leaves on both daemons and routes the matching legacy `auto-cost
reference-bandwidth` CLI through ietf-ospf YANG.

- /auto-cost/enabled: FRR has no off-switch for auto-cost; the modify
  callback rejects `false` at NB_EV_VALIDATE.  A deviation pins the
  default to `true` so the YANG `when "../enabled = 'true'"` clause on
  the bandwidth leaf is always satisfied and operators never need to
  set this leaf themselves.
- /auto-cost/reference-bandwidth: modify writes ospf->ref_bandwidth on
  the resolved instance and walks that instance's VRF interfaces with
  ospf_if_recalculate_output_cost; destroy restores
  OSPF_DEFAULT_REF_BANDWIDTH.  The ospf6d side mirrors via
  ospf6_interface_recalculate_cost across the resolved ospf6's
  area_list / if_list.

CLI conversion uses ospf_per_instance_xpath / ospf6_per_instance_xpath
so the multi-instance and multi-VRF xpath is built the same way
established leaves use it; both helpers are now extern (promoted with
prototypes in ospf_vty.h / ospf6_top.h).

Tests:
- test_ospf_yang_auto_cost_reference_bandwidth_config: mgmtd round-trip
  for both daemons.
- test_ospf_yang_auto_cost_disable_rejected: confirms the NB_EV_VALIDATE
  rejection.  Uses the existing _mgmt_commit_attempt helper so the
  rejected candidate is aborted and does not leak into subsequent
  commits.
- test_ospf_auto_cost_cli_routes_through_yang: legacy CLI lands via
  YANG; verified through `show mgmt get-data`.

Doc:
- Mapping-table rows for /auto-cost/enabled and
  /auto-cost/reference-bandwidth.
- "auto-cost" appended to the converted-leaves paragraph.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Adds the OSPFv2 /mpls/te-rid/ipv4-router-id leaf and routes the legacy
`mpls-te router-address A.B.C.D` CLI through ietf-ospf YANG.  ospf6d
has no MPLS-TE module so the leaf is OSPFv2-only.

FRR's MPLS-TE state is a process-wide global (`OspfMplsTE`).  The
modify callback rejects non-default VRF at NB_EV_VALIDATE -- matching
the constraint the legacy `mpls-te on` CLI enforces -- and delegates
to a new `ospf_mpls_te_apply_router_addr` helper extracted from the
DEFUN body so the YANG path and the CLI shim share identical
side effects (store the value, refresh Opaque Router-Address LSAs when
MPLS-TE is enabled).  Destroy clears the TLV header so the running
config writer skips the line cleanly.

The legacy CLI never exposed `no mpls-te router-address` -- the value
was cleared by `no mpls-te` (which disables MPLS-TE entirely).  Adding
a new `no mpls-te router-address` form here conflicts with the
optional-on branch of `no mpls-te [on]` in the CLI graph and breaks
both commands; the targeted clear is therefore only available through
`mgmt delete-config` and is exercised by the YANG round-trip test.

The legacy `ospf_mpls_te_config_write_router` unconditionally emitted
`mpls-te router-address 0.0.0.0` even when no address had ever been
set; gating the line on `header.type != 0` cleans up that pre-existing
quirk and makes the YANG destroy path observable in running-config.

ospf_te.c is added to clippy_scan so DEFPY_YANG expansion lands.

Tests:
- test_ospf_yang_mpls_te_router_addr_config: mgmtd round-trip (set,
  verify in running-config, delete, verify gone) with `mpls-te on`
  enabled up front and torn down in a finally block.
- test_ospf_mpls_te_router_addr_cli_routes_through_yang: legacy set
  form lands via YANG.

Doc:
- Mapping-table row for /mpls/te-rid/ipv4-router-id.
- "mpls/te-rid" appended to the converted-leaves paragraph.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Adds /graceful-restart/enabled and /graceful-restart/restart-interval
on both daemons and routes the legacy `graceful-restart [grace-period
N]` / `no graceful-restart` CLI through ietf-ospf YANG.

The two leaves are independent in the YANG model and each callback
manages its own state -- `enabled` controls `ospf->gr_info.restart_
support` plus zebra-GR enable/disable + NVM bookkeeping;
`restart-interval` controls `ospf->gr_info.grace_period` and refreshes
the zebra stale-route timer when GR is already on.  Three helpers per
daemon (`ospf_gr_restart_support_enable`, `_disable`,
`_set_grace_period`) are extracted from the legacy DEFPY body so the
CLI shim and the YANG callbacks share a single code path.  The
`enabled` modify rejects disable at NB_EV_VALIDATE when a GR prepare
is in flight, matching the legacy CLI's rejection.

RFC default for restart-interval is 120s, which matches FRR's
compile-time `OSPF_DFLT_GRACE_INTERVAL` / `OSPF6_DFLT_GRACE_INTERVAL`,
so no deviation is required.

CLI shim: the unified `graceful-restart [grace-period N]` enqueues
both `/enabled=true` and (when N is given) `/restart-interval=N` in
one apply.  The `no` form destroys both leaves so the legacy "no GR +
period reset" semantics is preserved.  Both daemons promote
`ospf_gr_nvm_update` from static to extern so the helpers can reuse
it.

Tests:
- test_ospf_yang_graceful_restart_config: mgmtd round-trip on both
  daemons -- set with non-default interval, drop interval alone
  (running-config collapses to bare `graceful-restart`), then
  disable.
- test_ospf_graceful_restart_cli_routes_through_yang: legacy
  `graceful-restart grace-period 180` / `no graceful-restart` on
  both daemons.

Doc:
- Mapping-table rows for both leaves.
- "graceful-restart enabled and restart-interval" appended to the
  converted-leaves paragraph.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Adds /graceful-restart/helper-enabled and
/graceful-restart/helper-strict-lsa-checking on both daemons and
routes the matching legacy `graceful-restart helper enable` and
`graceful-restart helper {strict-lsa-checking,lsa-check-disable}`
CLI forms through ietf-ospf YANG.

The two leaves map onto FRR's existing helper helpers:
`ospf_gr_helper_support_set` / `ospf_gr_helper_lsa_check_set` (v2)
and `ospf6_gr_helper_support_set` / `ospf6_gr_helper_lsacheck_set`
(v3).  The v3 helpers are promoted from static to extern so the YANG
callbacks can call them; the v2 helpers were already extern.

Two structural mismatches with RFC 9129 are documented in the notes
mapping rows and handled at the CLI shim:

- The legacy `graceful-restart helper enable [A.B.C.D]` per-router-id
  form has no RFC counterpart (the RFC has no helper enable-list).
  The DEFPY_YANG shim splits: bare form enqueues
  `/helper-enabled=true`; per-router-id form stays on the legacy
  direct mutation path.

- v3's strict-LSA-check CLI is inverted -- `graceful-restart helper
  lsa-check-disable` disables strict checking, `no ...` restores it.
  The YANG leaf `helper-strict-lsa-checking` is positive, so the v3
  shim flips the value (`MODIFY false` / `MODIFY true`) before
  enqueueing.  v2's CLI is positive and maps cleanly.

Tests:
- test_ospf_yang_graceful_restart_helper_config: mgmtd round-trip on
  both daemons -- enable helper + relax strict-check, drop strict
  alone (line disappears since FRR's default is strict-on), then
  disable helper.
- test_ospf_graceful_restart_helper_cli_routes_through_yang:
  legacy CLI on both daemons, tolerating either v2's
  `no ... strict-lsa-checking` or v3's `lsa-check-disable` line in
  running-config.

Doc:
- Mapping-table rows for both leaves with the per-router-id and
  inverted-v3-CLI caveats called out.
- "helper-enabled and helper-strict-lsa-checking" appended to the
  converted-leaves paragraph.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
…fig out of scope

These four RFC 9129 nodes have no FRR-side surface to map onto:

- nsr: FRR has no OSPF Non-Stop Routing.
- database-control/max-lsa: FRR's max-metric router-lsa on-shutdown /
  on-startup is structurally different from the RFC's accept-threshold.
- spf-control/ietf-spf-delay: FRR's timers throttle spf implements a
  different back-off algorithm.
- node-tag-config: neither daemon has any node-tag CLI, struct, or LSA
  encoding; router-info only enables the Opaque Router Information LSA.

Documenting the absence lets future readers see that each was considered
and ruled out, rather than rediscovering the gap through grep.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
ospf6_route_remove_all walked the route table with
`route = ospf6_route_next(route)` AFTER calling ospf6_route_remove,
which drops the table-owned lock and -- on the last lock -- frees the
route.  The loop then dereferenced freed memory.

In practice the failure surfaces when ospf6_restart_spf calls this
helper while a hook_remove callback (e.g. the zebra route-table
update path) re-enters ospf6_route_remove on a sibling route in the
same prefix's `node->info` chain.  The chain mutation underneath the
in-flight remove leaves the head route detached from `node->info`,
and the next iteration trips `assert(current == route)` at
ospf6_route_remove (line 928).

Switch to a re-read-the-head loop and explicitly drop the iterator
lock that ospf6_route_head takes on the snapshot.  This is the same
shape that other ospf6d walkers (e.g. ospf6_abr.c:118-127) use and
matches the existing convention that hook_remove may mutate the
table.

Triggered locally by the per-interface BFD topotest sequence in
ospf_topo1 -- adding state churn ahead of
test_ospf_max_multipath_cli_routes_through_yang (which routes
through ospf6_restart_spf) was enough to expose the use-after-free.
The fix is independent of that test suite and benefits every caller
of ospf6_restart_spf (distance / max-paths / future YANG callbacks).

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Adds /areas/area/interfaces/interface/bfd/* on both daemons -- the
RFC 9129 ietf-bfd-types client-cfg-parms surface (enabled,
local-multiplier, desired-min-tx-interval, required-min-rx-interval)
-- and routes the matching legacy `ip ospf bfd` / `ipv6 ospf6 bfd`
CLI through ietf-ospf YANG.

ospfd maps onto `params->bfd_config` (heap-allocated on enable via
`ospf_interface_enable_bfd`; freed by `ospf_interface_disable_bfd`).
ospf6d maps onto `oi->bfd_config` (embedded struct).  The shared
session-lifecycle helpers are promoted from static to extern:
`ospf_interface_enable_bfd`, `ospf_interface_bfd_apply` on ospfd, and
`ospf6_bfd_reg_dereg_all_nbr` on ospf6d.

Two FRR-specific extensions stay on the legacy direct path because
they have no RFC 9129 counterpart:
- ospfd `[quick]` (quick-establishment mode)
- ospf6d `[profile BFDPROF]` (FRR BFD profile selection)
The DEFUN_YANG shims split: bare form enqueues YANG; parametric form
stays direct.  Same `if-area`-bound fallback as the other per-
interface conversions -- if the interface is not in an area, no YANG
path exists, so direct mutation is used.

The DEFUN macros stay as DEFUN_YANG (not DEFPY_YANG) because the
parametric forms have `#if HAVE_BFDD > 0` switching between
DEFUN_HIDDEN and DEFUN, which the clippy scanner can't handle inside
macro argument lists.

ospfd's `no ip ospf bfd` is kept as plain DEFUN for the same reason
(the cmd-string has #if/#else inside it) but its body routes through
YANG when the area binding exists.

Deviations against RFC 9129 / ietf-bfd-types:
- `desired-min-tx-interval` / `required-min-rx-interval` defaults
  pinned to 300000us (FRR's `BFD_DEF_MIN_TX/RX = 300ms`), via
  `deviate replace` because the RFC base sets a default of
  1000000us.
- `single-interval` case marked `not-supported` -- FRR's BFD library
  only exposes the (tx, rx) interval form.

Unit handling:
- RFC uses microseconds, FRR stores milliseconds.  Reject non-
  multiples of 1000us at NB_EV_VALIDATE; clamp to FRR's CLI grammar
  range (50..60000 ms = 50000..60000000 us).

Build:
- ospfd/ospf_vty.c: `ospf_per_iface_xpath` promoted from static to
  extern (declared in ospf_vty.h).
- ospf6d/ospf6_interface.c: `ospf6_per_iface_xpath` promoted from
  static to extern (declared in ospf6_interface.h).

Tests:
- test_ospf_yang_interface_bfd_config: mgmtd round-trip on both
  daemons (enable + multiplier + tx/rx, then delete via destroy).
- test_ospf_yang_interface_bfd_interval_rejection: NB_EV_VALIDATE
  rejection for non-multiple-of-1000us and out-of-range values,
  using `_mgmt_commit_attempt` for proper candidate cleanup.
- test_ospf_bfd_cli_routes_through_yang: legacy `ip ospf bfd` /
  `ipv6 ospf6 bfd` and the param forms via legacy CLI, with
  try/finally cleanup.

Doc:
- Mapping-table rows for each of the four leaves.
- Converted-leaves paragraph extended with the BFD entry.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Adds /areas/area/interfaces/interface/static-neighbors/neighbor on
OSPFv2 only (ospf6d has no NBMA neighbour surface) with the per-leaf
priority and poll-interval modify/destroy callbacks.

Structural mismatch documented and contained: RFC 9129 keys the list
per-(area, interface, identifier), but FRR's `struct ospf_nbr_nbma`
table is per-(instance, addr).  The callback walks up to the OSPF
instance from the dnode and uses `identifier` only; the area /
interface labels are stored in the candidate but ignored on the FRR
side.  FRR auto-binds the entry to whichever OI's subnet matches the
neighbour address via `ospf_nbr_nbma_set`/`_if_update`.  Multiple
YANG entries with the same identifier collapse onto the same FRR
neighbour (idempotent set, last destroy wins -- no reference
counting in this slice).

Cost leaf marked not-supported via deviation: FRR's NBMA neighbour
struct exposes only `priority` and `v_poll`, so there is no `cost`
knob to map onto.  mgmtd rejects writes against `cost` cleanly.

Legacy `neighbor A.B.C.D` CLI stays on the direct mutation path: it
is instance-level and cannot synthesise a credible YANG area /
interface key from CLI alone.

Tests:
- test_ospf_yang_interface_static_neighbor_config: mgmtd round-trip
  (set poll-interval + priority, verify in running-config, delete,
  verify gone) with try/finally cleanup.
- test_ospf_yang_static_neighbor_cost_rejected: confirms the
  not-supported deviation is enforced (uses `_mgmt_commit_attempt`).

Doc:
- Mapping-table row spelling out the per-(area, iface) vs
  per-(instance) mismatch and the cost not-supported note.
- Converted-leaves paragraph extended with "OSPFv2 per-interface
  static neighbours (poll-interval, priority)".

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Adds /areas/area/interfaces/interface/authentication/ospfv2-key-chain
(OSPFv2) and ospfv3-key-chain (OSPFv3).  Covers only the key-chain
case of RFC 9129's authentication choice -- the explicit-key triplet
(key-id + key + crypto-algorithm) and OSPFv3 IPsec SA case are
deferred to follow-on slices because they map onto different FRR-side
surfaces with multi-leaf coordination.

OSPFv2 callback writes `params->keychain_name`, sets `auth_type` to
`OSPF_AUTH_CRYPTOGRAPHIC`, and clears any explicit `auth_crypt`
list -- mirrors the legacy `ip ospf authentication key-chain X` CLI
exactly (see ospf_vty.c:7754+).  Destroy frees the keychain string,
clears the field, and restores `auth_type = OSPF_AUTH_NOTSET`.

OSPFv3 callback writes `oi->at_data.keychain` and sets the
`OSPF6_AUTH_TRAILER_KEYCHAIN` flag.  NB_EV_VALIDATE rejects the
modify if `OSPF6_AUTH_TRAILER_MANUAL_KEY` is already set -- matches
the legacy CLI's mutually-exclusive lock.  Destroy frees the
keychain string and clears the flag set.

`MTYPE_OSPF6_AUTH_KEYCHAIN` promoted from DEFINE_MTYPE-only to
DECLARE_MTYPE in ospf6_interface.h so the new callback can use
XSTRDUP/XFREE on it.

Deferred non-key-chain authentication leaves (explicit-key triplet,
IPsec SA, auth-trailer-rfc): pyang / libyang refuse the deviation
paths inside the nested choice/case nodes (`ietf-ospf::ospfv2-key`
etc. not found), so this slice cannot mark them not-supported.
mgmtd silently accepts writes against them with no callback fire.
Documented in the developer notes for the slice; future work either
implements those leaves as real callbacks or sorts the schema-path-
resolution gap.

Tests:
- test_ospf_yang_interface_authentication_keychain_config: mgmtd
  round-trip on both daemons.  Creates a real `key chain kc-test`
  before the YANG writes so libyang's leafref into
  /key-chain:key-chains/.../name resolves; tears it down in the
  finally clause.  The legacy CLI is more relaxed about forward
  references but matching YANG's leafref semantics is the
  principled behaviour for this slice.

Doc:
- Mapping-table row covering both v2 and v3 key-chain leaves.
- Converted-leaves paragraph extended with the key-chain entry.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
The BFD slice (commit ba4863b) added ietf-bfd-types to each of
ospfd, ospf6d and mgmtd's `frr_yang_module_info` table as a
separate inline definition.  Three copies of the same data, but
the precedent is one: ietf-keychain has lived in lib/keychain_nb.c
since the lib/ housekeeping consolidation, exported via
lib/keychain.h as `ietf_key_chain_info`, and every daemon that
needs it (ospfd, ospf6d, ripd, ripngd, isisd, ...) references the
single symbol.

Mirror that.  Define `ietf_bfd_types_info` once in lib/bfd.c with
`*` features (so client-base-cfg-parms enables the multiplier and
tx/rx-interval leaves) and `ignore_cfg_cbs = true`.  Declare it in
lib/bfd.h alongside the existing BFD helpers.  Drop the three
inline duplicates and have ospfd, ospf6d, mgmtd reference the
shared symbol.

No behaviour change.  ospfd, ospf6d and mgmtd continue to load
ietf-bfd-types with the same feature set and the same null
callback table; the symbol is just sourced from one place instead
of three.

The shape now matches lib/keychain.h's `ietf_key_chain_info`
exactly, which keeps the lib/ <-> backends boundary uniform for
future foreign-module loaders (ietf-srv6-types is the next
candidate if any backend needs to publish data inheriting from
it).

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
After completing the per-interface config-write conversions (cost,
hello/dead/retransmit, priority, mtu-ignore, transmit-delay,
interface-type, passive, prefix-suppression, BFD, static-neighbors,
authentication key-chain), the remaining ietf-ospf config nodes were
audited against FRR's actual surface on both daemons.

Every leaf left in scope is either:

a) blocked at the schema layer (the auth `choice` / `case` branches
   that pyang / libyang refuse deviation paths for), or
b) has no matching FRR struct field, CLI command, or runtime helper
   to mutate.

Document this explicitly in the "Remaining Scope" section so the
boundary between "intentionally deferred" and "implementation
missing" is unambiguous.  New entries cover ospf/enabled and
interface/enabled (no separate on/off in FRR -- the protocol runs
whenever an instance is configured and an interface is bound to an
area), interface/multi-areas (no multi-area-adjacency surface),
interface/ttl-security (no per-interface TTL-security check),
ospf/fast-reroute/lfa and the per-interface lfa subtree (FRR's only
LFA surface is the instance-level `fast-reroute ti-lfa` command on
ospfd, which doesn't match RFC 9129's per-interface enable shape;
ospf6d has no TI-LFA at all), ospf/address-family (fixed per
daemon), and the auth nested-choice branches (deviation-path
limitation already discussed in the ospfv2-key-chain row).

Adding empty validation-only callbacks for these would only create
silent-acceptance traps -- the rule established in earlier slices is
that every committed leaf must map to a real FRR mutation.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Add the two RPCs RFC 9129 defines at the ietf-ospf module root, with
matching handlers on both ospfd and ospf6d.  The RPCs are registered
at the module root rather than under control-plane-protocol, so both
daemons subscribe to the same xpaths and mgmtd fans each call out
to every backend; each handler looks up the named instance and
returns NB_OK silently when the instance isn't local.

Mapping:

* `clear-neighbor` on ospfd reuses `ospf_neighbor_reset` for the
  whole-instance case and walks the matching OI's neighbour table
  for the per-interface case (legacy `ospf_neighbor_reset` has no
  per-interface filter, so the iteration inlines the NSM_KillNbr
  loop the helper would otherwise run unconditionally).
* `clear-neighbor` on ospf6d reuses `ospf6_interface_clear`,
  iterating every OSPFv3-bound interface when no `interface` input
  is supplied or invoking it once when an interface name is given.
* `clear-database` on both daemons maps onto `ospf_process_reset` /
  `ospf6_process_reset` -- the helpers the legacy
  `clear ip ospf process` / `clear ipv6 ospf6 process` commands
  already invoke, so the RFC's "force every neighbour DOWN and
  reoriginate self-originated LSAs" semantics line up exactly.

Per-interface clear with an interface that doesn't belong to the
instance returns NB_ERR_RESOURCE with the RFC-mandated app-tag
`ospf-interface-not-found`.  RPC against an unknown
`routing-protocol-name` returns NB_OK on every backend -- that is
the legitimate "no daemon owns this name" outcome, not an error.

The xpaths register via the `rpc_xpaths` array on each daemon's
`mgmt_be_client_cbs` rather than via `frr_yang_module_info.nodes`
alone; mgmtd's RPC dispatcher consults the BE-adapter subscription
map, not the schema callback registry, so the subscribe message
that ships at client connect must list the RPC xpaths explicitly.

Audit-driven changes vs the initial draft:

* `ospf-interface-not-found` now returns NB_ERR_NOT_FOUND instead
  of NB_ERR_RESOURCE.  mgmtd maps NB_ERR_NOT_FOUND to
  MGMTD_INVALID_PARAM and NB_ERR_RESOURCE to MGMTD_INTERNAL_ERROR;
  the latter incorrectly signals a daemon-side failure rather than
  a bad client reference.

* Documented two deliberate deviations from strict RFC 9129 wording:
  (a) silent NB_OK on a non-local routing-protocol-name is forced by
  the mgmtd fan-out model -- a non-owner cannot distinguish "name
  doesn't exist on me" from "name doesn't exist anywhere"; (b)
  FRR's nb_cb_rpc_args has no first-class error-app-tag field, so
  the RFC-prescribed app-tag strings ride in the unstructured errmsg
  buffer rather than via NETCONF / RESTCONF <error-app-tag>.

* clang-format clean on both new files.

* Investigated the per-daemon gRPC frontend (lib/northbound_grpc.cpp)
  as a second invocation path: it bypasses mgmtd and calls
  `nb_callback_rpc` directly via the daemon's `-M grpc:<port>`
  listener.  Cannot dispatch these RPCs in standalone mode because
  `HandleUnaryExecute` validates input via `lyd_validate_op` with
  `LYD_TYPE_RPC_YANG` before invoking the handler, and the daemon
  has no `/ietf-routing:routing/...` data populated locally to
  satisfy the `routing-protocol-name` leafref.  Every Execute call
  with a valid name is rejected at the validator with
  `INVALID_ARGUMENT` / "Invalid input data".  Documented as a
  frontend-side gap in the RPC Support section; not specific to
  these RPCs (it affects every RFC 9129 RPC whose inputs include a
  leafref into the routing tree).  Use the mgmtd-fronted vtysh path
  until either the gRPC frontend relaxes leafref validation for
  RPC inputs or backend daemons mirror the routing-protocol list
  locally.

Tests:

* `test_ospf_yang_clear_neighbor_rpc` -- whole-instance reset plus
  per-interface reset on both v2 and v3, verifying neighbours
  renegotiate back to Full.
* `test_ospf_yang_clear_database_rpc` -- same round-trip for the
  process-reset path.
* `test_ospf_yang_rpc_unknown_instance_silent` -- RPC against
  `does-not-exist` returns OK and leaves r1's v2 neighbour
  undisturbed.
* `test_ospf_yang_clear_neighbor_rpc_unknown_interface` -- per-
  interface reset against `lo` (not in any OSPF area) surfaces the
  `ospf-interface-not-found` app-tag.

After the disruptive clear-database RPC the test calls
`_force_ospf_reconvergence_to_steady_state` to restore the LSDB
baseline the downstream read-only tests
(`test_ospf_json`, `test_ospf_kernel_route`) compare against.

The libyang RPC parser expects the input wrapped in the RPC name
(`{"ietf-ospf:clear-neighbor":{"routing-protocol-name":"default"}}`);
this is documented alongside the new mapping table in
`doc/developer/ospf-yang-northbound-notes.rst`.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Emit /ietf-ospf:nbr-state-change on every NSM transition by hooking
the existing per-daemon state-change hook (ospf_nsm_change on ospfd,
ospf6_neighbor_change on ospf6d) and dispatching through
nb_notification_send.

The notification carries the RFC 9129 instance header (routing-
protocol-name), the interface link container with the
ietf-interfaces interface name, the neighbour-router-id and
neighbour-ip-addr leaves, and the RFC nbr-state-type enum.

State translation:

* OSPFv2's NSM_* constants reserve 0/1 for DependUpon/Deleted, so
  NSM_Down=2..NSM_Full=9 maps to RFC values 1..8 via a small lookup
  table.
* OSPFv3's OSPF6_NEIGHBOR_* values match RFC 9129's nbr-state-type
  numeric values 1:1, so no translation is needed.

Each daemon registers its hook subscriber from a new
ospf{,6}d_ietf_notif_init() called once from
ospf{,6}_master_init().  Handlers log every emit through
DEBUGD(&nb_dbg_notif, ...) so operators can verify wiring with
`debug northbound notifications`.

Tests:

* test_ospf_yang_nbr_state_change_notification -- enables northbound
  notification debug on r1, triggers a neighbour reset via the
  existing clear-neighbor RPC, and greps both ospfd and ospf6d logs
  for the OSPF-NOTIF / "northbound notification: /ietf-ospf:nbr-
  state-change" markers.  Restores steady-state after so downstream
  LSDB-snapshot tests stay deterministic.

Doc:

* New "Notification Support" section in
  doc/developer/ospf-yang-northbound-notes.rst documenting the
  hook wiring, the state translation, and which RFC 9129
  notifications remain to be wired in subsequent slices.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Emit /ietf-ospf:if-state-change on every ISM transition by hooking the
existing per-daemon interface state-change hook (ospf_ism_change on
ospfd, ospf6_interface_change on ospf6d) and dispatching through
nb_notification_send.

State translation tables (numeric values differ between FRR internals
and RFC 9129's `if-state-type` enum):

* OSPFv2: Down/Loopback/Waiting/PointToPoint agree, but FRR orders the
  DR-election trio DROther=5, Backup=6, DR=7 while the RFC uses dr=5,
  bdr=6, dr-other=7.
* OSPFv3: same RFC-vs-FRR DR ordering mismatch; FRR also reserves
  value 5 (gap between PointToPoint=4 and DROther=6), so the full
  table is needed.

Tests:

* test_ospf_yang_if_state_change_notification -- enables northbound
  notification debug on r1, toggles r1-eth1 down/up to force ISM
  transitions on both ospfd and ospf6d, then greps both daemon logs
  for the OSPF-NOTIF / "northbound notification: /ietf-ospf:if-state-
  change" markers.

Doc:

* New row in the Notification Support table documenting the hook and
  the state-translation rationale.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Wire two RFC 9129 graceful-restart notifications on both daemons:

* /ietf-ospf:restart-status-change emitted from ospf{,6}_gr_restart_
  enter (status=planned-restart) and ospf{,6}_gr_restart_exit
  (status=not-restarting + exit-reason=completed).
* /ietf-ospf:nbr-restart-helper-status-change emitted from the GR
  helper accept path (status=helping, age=grace interval,
  exit-reason=in-progress) and from ospf{,6}_gr_helper_exit
  (status=not-helping, age=0, exit-reason translated from FRR's
  helper-exit-reason enum).

Mapping notes:

* All FRR-known restart reasons (SW_RESTART, SW_UPGRADE,
  SWITCH_CONTROL_PROCESSOR, UNKNOWN_RESTART) are software-initiated,
  so the emit calls hardcode RFC `planned-restart` (2).
  `unplanned-restart` (3) would correspond to a crash recovery path
  FRR doesn't currently signal; left for future work.
* FRR's `enum ospf_helper_exit_reason` (0..4) maps name-for-name to
  RFC 9129's `restart-exit-reason-type` (1..5) via a small switch.

No new state-change hook is added; we call the notification builders
directly from the existing GR enter/exit sites because graceful
restart isn't otherwise observed by FRR hook subscribers (no SNMP-
hook precedent like ospf_nsm_change).

Doc:

* New rows in the developer-notes Notification Support table
  describing both notifications + the emit-site choice.
* "Out of scope for now" paragraph updated to note that triggering
  GR in topotest is non-trivial; live test coverage is deferred
  with a manual reproduction path documented (clear ip ospf process
  on a neighbour during its grace period).

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Two packet-side notifications on both daemons:

* /ietf-ospf:if-rx-bad-packet -- emitted from ospf_read_helper (v2)
  and ospf6_receive (v3) when post-header verification fails.  The
  packet-source leaf carries the source IP; packet-type is included
  when the header parsed far enough to extract it.
* /ietf-ospf:if-config-error -- emitted at hello-interval and dead-
  interval mismatch checks in ospf_hello (v2) and the corresponding
  v3 receive path.  The error leaf is set to the RFC enum identifier
  string (`hello-interval-mismatch`, `dead-interval-mismatch`).

Both notification builders take the OI plus the packet-source / type
inputs and produce the full RFC 9129 envelope (instance header,
interface header, packet-source, packet-type if known, error name
for the config-error variant).

Other reject paths the RFC `error` enum can carry (bad-version,
area-mismatch, unknown-nbma-nbr, unknown-virtual-nbr, auth-type-
mismatch, auth-failure, net-mask-mismatch, option-mismatch, mtu-
mismatch, duplicate-router-id) wire as incremental future work --
the emit helper is generic enough that each new reject site is a
one-line `ospfd_ietf_notif_if_config_error(...)` insertion.

Doc:

* Notification Support table grows two rows describing the wired
  sites and the future-incremental-wiring note.

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
Emit /ietf-ospf:nssa-translator-status-change when the OSPFv2
NSSATranslatorState for an area transitions.  Wired at the existing
ABR check site in ospf_abr_nssa_check_status where FRR already
detects the transition (`if (old_state != area->NSSATranslatorState)`).

State mapping:

* RFC 9129 defines three states: enabled (1), elected (2),
  disabled (3).
* FRR tracks only DISABLED (0) and ENABLED (1).  ENABLED means this
  router has been elected as the NSSA translator for the area, so
  FRR ENABLED maps to RFC `elected`.  FRR has no separate "enabled
  but not yet elected" state, so RFC `enabled` is unreachable from
  FRR's side -- it's a state RFC distinguishes that FRR
  consolidates.

Notification body carries the instance header (routing-protocol-
name) plus the area-id and the translated status enum.

OSPFv3 has no NSSA translator surface in FRR, so this notification
is OSPFv2-only -- consistent with the existing config-write scope
documented in the Remaining Scope section.

Doc:

* New row in the Notification Support table.
* "Out of scope for now" paragraph updated: this was the last RFC
  9129 notification to wire; only the two lsdb-overflow events
  remain unimplemented (FRR has no max-LSA threshold).

Signed-off-by: Eric Parsonage <eric@eparsonage.com>
@lamestllama lamestllama force-pushed the ospfd-yang-rfc9129-v2 branch from d278944 to c704313 Compare June 2, 2026 14:56
@lamestllama
Copy link
Copy Markdown
Author

@greptile[bot] review

@lamestllama lamestllama marked this pull request as draft June 2, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation libfrr master mgmt FRR Management Infra ospfv3 rebase PR needs rebase size/XXL tests Topotests, make check, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants