<?xml version='1.0' encoding='utf8'?>
<rfc version="3" category="std" ipr="trust200902" submissionType="IETF" consensus="true" docName="draft-xu-rtgwg-resilient-nexthop-group-yang-01" tocInclude="true" symRefs="true" sortRefs="true">
  <front>
    <title abbrev="Resilient Next-Hop Groups">A YANG Data Model for Resilient Next-Hop Groups</title>
    <author fullname="Bohua Xu" initials="B." surname="Xu">
      <organization>China Unicom</organization>
      <address>
        <postal>
          <city>Beijing</city>
          <code>100000</code>
          <country>China</country>
        </postal>
        <email>xubh15@chinaunicom.cn</email>
      </address>
    </author>
    <author fullname="Chang Liu" initials="C." surname="Liu">
      <organization>China Unicom</organization>
      <address>
        <postal>
          <city>Beijing</city>
          <code>100000</code>
          <country>China</country>
        </postal>
        <email>liuc131@chinaunicom.cn</email>
      </address>
    </author>
    <author fullname="Yu Zhou" initials="Y." surname="Zhou">
      <organization>China Unicom</organization>
      <address>
        <postal>
          <city>Beijing</city>
          <code>100000</code>
          <country>China</country>
        </postal>
        <email>zhouy739@chinaunicom.cn</email>
      </address>
    </author>
    <author fullname="Chenyang Wen" initials="C." surname="Wen">
      <organization>China Unicom</organization>
      <address>
        <postal>
          <city>Beijing</city>
          <code>100000</code>
          <country>China</country>
        </postal>
        <email>wency15@chinaunicom.cn</email>
      </address>
    </author>
    <author fullname="Wei Cheng" initials="W." surname="Cheng">
      <organization>Centec</organization>
      <address>
        <postal>
          <city>Suzhou</city>
          <code>215000</code>
          <country>China</country>
        </postal>
        <email>chengw@centec.com</email>
      </address>
    </author>
    <author fullname="Junjie Wang" initials="J." surname="Wang">
      <organization>Centec</organization>
      <address>
        <postal>
          <city>Suzhou</city>
          <code>215000</code>
          <country>China</country>
        </postal>
        <email>wangjj@centec.com</email>
      </address>
    </author>
    <author fullname="Mingyu Wu" initials="M." surname="Wu">
      <organization>Centec</organization>
      <address>
        <postal>
          <city>Suzhou</city>
          <code>215000</code>
          <country>China</country>
        </postal>
        <email>wumy@centec.com</email>
      </address>
    </author>
    <author fullname="Yongtao Yang" initials="Y." surname="Yang">
      <organization>Centec</organization>
      <address>
        <postal>
          <city>Suzhou</city>
          <code>215000</code>
          <country>China</country>
        </postal>
        <email>yangyt@centec.com</email>
      </address>
    </author>
    <date year="2026" month="October" day="30"/>
    <area>Routing</area>
    <workgroup>Individual Submission</workgroup>
    <abstract>
      <t>This document defines a YANG data model for configuring and observing resilient next-hop groups. A fixed bucket table separates flow hashing from member selection. The model augments the routing management model with shared groups, static route bindings, weighted bucket targets, restoration policies, and forwarding-confirmed operational state.</t>
      <t>The specification defines distinct quota and reference restoration policies, consistent timer semantics, and an administrative rebalance action protected by a state-generation precondition. It standardizes management behavior for this class of implementation; it does not define a new routing protocol, hash algorithm, or transport ordering guarantee.</t>
    </abstract>
    <note removeInRFC="true">
      <name>Discussion and Implementation Status</name>
      <t>This is an individual proposal intended for the Standards Track. No working group adoption or independent interoperability implementation is claimed. The authors invite review of the problem statement, overlap with existing work, and implementation experience.</t>
    </note>
  </front>
  <middle>
    <section anchor="intro">
      <name>Introduction</name>
      <t>Changing a multipath group can change the next hop selected for an existing flow. The extent depends on the selection algorithm <xref target="RFC2992"/>. A bucket indirection table permits an implementation to repair buckets associated with a failed member while retaining other assignments. Linux resilient groups provide one public implementation of this approach <xref target="LINUX"/>.</t>
      <t>A controller managing this class of forwarding needs to distinguish capabilities from configured policy, decide whether an active bucket may move on recovery, and determine whether a reported table has reached the forwarding plane. A group name and weighted member list do not fully express these decisions.</t>
      <t>This document proposes a common YANG interface for those decisions and observable invariants. A controller can select a policy, read current assignments and their generation, then issue a guarded rebalance. It does not select a particular internal hash, memory layout, or failure detector. The requirements apply only to implementations claiming support for this module.</t>
      <section anchor="related">
        <name>Existing Models and Scope of the Proposal</name>
        <t>The routing model <xref target="RFC8349"/> defines routing configuration and operational RIB state. This module augments its next-hop choices rather than changing their existing meaning. The first version is limited to IPv4 and IPv6 unicast static-route configuration and group references on operational RIB routes. Routing-protocol signaling, recursive group composition, encapsulation policy, and cross-router synchronized hashing are outside scope.</t>
        <t>OpenConfig already models static next-hop groups <xref target="OC-STATIC"/> and abstract forwarding state <xref target="OC-AFT"/>. gRIBI distinguishes RIB and FIB programming acknowledgments <xref target="GRIBI"/>. SAI provides indexed fine-grained ECMP groups <xref target="SAI-NHG"/>. Consequently, neither a shared group, bucket indirection, nor a forwarding acknowledgment is claimed here as a new mechanism.</t>
        <t>The proposed addition is a coherent policy and observation contract for bucket restoration within the IETF NMDA routing model. The combination of per-group restoration selection, invariant timer semantics, and a generation-guarded operation requires review against existing interfaces. An extension to an established model may be preferable if it meets the same operator need; this individual proposal does not establish working group demand.</t>
        <t>Weighted HRW <xref target="I-D.ietf-bess-weighted-hrw"/> addresses an algorithmic selection approach. This module does not require HRW or replace it. Implementations with no persistent bucket table should not claim conformance by fabricating bucket observations.</t>
      </section>
    </section>
    <section anchor="requirements">
      <name>Requirements Language</name>
      <t>The key words <bcp14>MUST</bcp14>, <bcp14>MUST NOT</bcp14>, <bcp14>REQUIRED</bcp14>, <bcp14>SHOULD</bcp14>, <bcp14>SHOULD NOT</bcp14>, <bcp14>RECOMMENDED</bcp14>, <bcp14>MAY</bcp14>, and <bcp14>OPTIONAL</bcp14> in this document are to be interpreted as described in BCP 14 <xref target="RFC2119"/> <xref target="RFC8174"/> when, and only when, they appear in all capitals, as shown here.</t>
    </section>
    <section anchor="terms">
      <name>Terminology</name>
      <t>A member has a stable group-scoped identifier, positive weight, and an interface and/or next-hop address. Usability is the local forwarding subsystem assessment that the member can carry traffic. The assessment can depend on interface state, neighbor resolution, or liveness detection; it is not prescribed by this model.</t>
      <t>B is the configured number of logical buckets. A(b) is the confirmed effective usable owner of bucket b; it is absent when forwarding through that bucket is confirmed suppressed or unassigned. R(b) is a reference owner used only by the reference policy. Raw hardware programming that is known to be unusable is not a usable assignment.</t>
      <t>A migration changes a confirmed owner to another confirmed owner. Initial assignment and removal are separately observable mapping changes and are not owner-to-owner migration counts. A bucket is idle when no packet has been observed for it over the configured monotonic interval. An idle bucket can still represent a live but temporarily silent connection.</t>
      <t>The quota policy seeks integer weighted shares of all B buckets over usable members. The reference policy seeks a stable reference ownership for every currently usable reference member. These are distinct balance conditions; neither promises equal traffic rates because bucket traffic can be uneven.</t>
      <artwork type="ascii-art">Flow-invariant fields -&gt; fixed hash parameters -&gt; bucket b -&gt; A(b)
                                                         |
Member usability -&gt; safety repair ------------------------+
Per-group policy -&gt; quota targets T or reference R -------+
Recovery timers -&gt; eligible discretionary movements ------+
Controller action -&gt; generation check -&gt; confirmed moves -+
</artwork>
    </section>
    <section anchor="behavior">
      <name>Forwarding and Restoration Behavior</name>
      <section anchor="mapping">
        <name>Mapping and Configuration Boundary</name>
        <t>The mapping from packet fields to bucket index <bcp14>MUST</bcp14> remain independent of member count, member usability, and member weights. Changes to B or hash parameters are explicit administrative changes and <bcp14>MAY</bcp14> remap all flows. Implementations <bcp14>MUST</bcp14> document their hash inputs and supported bucket sizes. This model does not configure hash inputs or a hash seed.</t>
        <t>A server <bcp14>MUST</bcp14> reject unsupported bucket counts or address-family combinations before accepting configuration. It <bcp14>MUST NOT</bcp14> silently substitute a different B. Weights express relative bucket allocation rather than guaranteed bandwidth. A member can receive zero buckets when B is small relative to the number or weight distribution of members; this does not make that member unusable.</t>
        <t>Configuration commits <bcp14>MUST</bcp14> be serialized with group control operations. Configuration values in the operational datastore describe the applied configuration under NMDA <xref target="RFC8342"/>. If intended configuration cannot be applied, the server <bcp14>MUST</bcp14> retain and expose the actual applied state and report the error; an accepted edit is not a forwarding acknowledgment. Changes of B, members, weights, or policy <bcp14>MAY</bcp14> migrate usable buckets and are outside the failure-only minimal-change rule.</t>
      </section>
      <section anchor="safety">
        <name>Failure Repair and Loss of Forwarding</name>
        <t>When a member becomes unusable, safety repair <bcp14>MUST</bcp14> take priority over discretionary restoration. The repair pass <bcp14>MUST</bcp14> target buckets whose effective owner became unusable; it <bcp14>MUST NOT</bcp14> move a bucket with a still-usable owner merely to improve balance. It <bcp14>MUST</bcp14> proceed without waiting for hold-down, idle-timer, or unbalanced-timer. This is a scheduling requirement, not a zero-time hardware repair guarantee.</t>
        <t>Each affected bucket <bcp14>MUST</bcp14> be assigned to a currently usable member, or have forwarding through the failed owner suppressed when no usable assignment can be installed. Before programming each new assignment the implementation <bcp14>MUST</bcp14> recheck destination usability. An unusable member <bcp14>MUST NOT</bcp14> remain eligible for packet selection. If the platform cannot suppress a failed member or confirm the result, it <bcp14>MUST</bcp14> report failed programming and <bcp14>MUST NOT</bcp14> claim successful repair.</t>
        <t>In quota policy, receivers <bcp14>SHOULD</bcp14> be selected to reduce current deficits against T where possible; the repair pass need not achieve T because that could require moving unaffected buckets. In reference policy, an affected bucket <bcp14>MUST</bcp14> use its reference owner when that owner is usable; otherwise any usable temporary owner <bcp14>MAY</bcp14> be selected. Safety repair can use a member whose recovery hold-down has not expired.</t>
        <t>If no member is usable, forwarding through the group <bcp14>MUST</bcp14> be suppressed. When members become usable from that condition, all unassigned buckets <bcp14>MUST</bcp14> be assigned without recovery hold-down. In reference policy a usable reference owner takes precedence; remaining buckets can use another usable owner. Programming can be partial: oper-status is up only if all buckets have usable confirmed assignments, degraded if some do, and down if none do.</t>
      </section>
      <section anchor="quota">
        <name>Quota Policy</name>
        <t>restoration/policy=quota is the default. With usable set U nonempty, let W be the sum of usable weights. The server <bcp14>MUST</bcp14> select a fixed integer T_i for each member, zero for unusable members, with T_i in {floor(B*w_i/W), ceil(B*w_i/W)} for usable members and sum(T_i)=B. It <bcp14>MUST</bcp14> expose target-buckets and retain these targets until B, membership, weights, or usability changes. Calculations <bcp14>MUST</bcp14> avoid intermediate integer overflow.</t>
        <t>Largest-remainder apportionment with ties resolved by ascending member ID is one conforming algorithm. Identical target choices across devices are not required; the selected targets are observable. After safety repair has assigned all B buckets, every discretionary quota migration <bcp14>MUST</bcp14> move one bucket from a member with active-buckets&gt;T_i to a usable eligible receiver with active-buckets&lt;T_i.</t>
        <t>This rule removes a common rounding deadlock: if an integer deficit exists, an integer surplus must also exist. Each successful quota migration reduces the sum of absolute deficits by two. It does not ensure prompt convergence when receiver hold-down, activity, non-revertive mode, or programming errors prevent movements. With U empty all target-buckets values <bcp14>MUST</bcp14> be zero.</t>
      </section>
      <section anchor="reference">
        <name>Reference Policy</name>
        <t>A server advertising reference-restoration supports restoration/policy=reference; advertising the feature <bcp14>MUST NOT</bcp14> switch a group from quota policy. On selecting reference policy, the server <bcp14>MUST</bcp14> establish R across all configured members, independently of current usability. Its per-member counts <bcp14>MUST</bcp14> be floor or ceiling of the configured weighted share and total B.</t>
        <t>R <bcp14>MUST</bcp14> remain unchanged across usability transitions. Configuration changes affecting B, membership, weights, or policy <bcp14>MAY</bcp14> establish a new R; unchanged assignments <bcp14>SHOULD</bcp14> be retained where the new constraints allow. The reference-generation <bcp14>MUST</bcp14> advance whenever R is created or changed. No cross-device equality of R is required.</t>
        <t>For discretionary reference restoration, a bucket is eligible to move only to its usable reference owner and only if A(b) differs from R(b). Quota donor and receiver limits <bcp14>MUST NOT</bcp14> constrain this move. Mixing these policies can prevent restoration when a temporary owner is not above its instantaneous weighted share.</t>
        <t>With all members usable, stable R, and every required move eventually permitted and confirmed, A converges to R independently of the order of prior failures. This is a conditional convergence property: non-revertive mode, disabled force timers, continuously busy buckets, or hardware errors can leave convergence pending indefinitely. While a member is down, reference policy does not guarantee proportional allocation among surviving members.</t>
      </section>
      <section anchor="timers">
        <name>Recovery, Eligibility, and Timer Semantics</name>
        <t>In revertive mode a recovering member becomes eligible for discretionary receipt after hold-down seconds of continuous usability. A fresh failure cancels its hold-down. A continuously usable member remains eligible. An initial applied configuration is allowed to assign traffic directly; hold-down is a recovery safeguard, not a deployment delay.</t>
        <t>For each eligible receiver with unmet policy work, the server starts a monotonic rebalance episode. The episode is cleared when that receiver has no unmet work or loses eligibility. Changes to B, weights, membership, usability, or restoration policy restart episodes against the new target set. A member that becomes eligible later starts its own episode then. rebalance-start is only a wall-clock display of the episode start.</t>
        <t>The idle-timer describes a bucket observation interval. The unbalanced-timer describes how long an eligible receiver waits before forced movement to it is permitted. Zero unbalanced-timer <bcp14>MUST</bcp14> always mean that timer-forced movement is disabled, irrespective of activity support. The expiry of a timer does not guarantee completion within a deadline; hardware programming can fail or be delayed.</t>
        <t>In non-revertive mode recovery <bcp14>MUST NOT</bcp14> automatically migrate buckets, even if they are idle or a force interval would expire. Safety repair, explicit configuration changes, and an accepted rebalance action remain permitted. Targets and pending work <bcp14>MUST</bcp14> still be observable.</t>
        <table anchor="timer-table">
          <name>Discretionary Automatic Movement in Revertive Mode</name>
          <thead>
            <tr>
              <th>Activity capability</th>
              <th>idle-timer</th>
              <th>unbalanced-timer</th>
              <th>Movement allowed after receiver eligibility</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Present</td>
              <td>&gt;0</td>
              <td>0</td>
              <td>Only buckets with measured idle duration &gt;= idle-timer</td>
            </tr>
            <tr>
              <td>Present</td>
              <td>&gt;0</td>
              <td>&gt;0</td>
              <td>Idle buckets first; active buckets after receiver episode expires</td>
            </tr>
            <tr>
              <td>Present</td>
              <td>0</td>
              <td>Any</td>
              <td>All donor buckets are treated as idle immediately</td>
            </tr>
            <tr>
              <td>Absent</td>
              <td>Unavailable</td>
              <td>0</td>
              <td>No automatic discretionary movement; wait for action or configuration</td>
            </tr>
            <tr>
              <td>Absent</td>
              <td>Unavailable</td>
              <td>&gt;0</td>
              <td>Force eligible movement after receiver episode expires</td>
            </tr>
          </tbody>
        </table>
        <t>Liveness safety is outside this table. Absence of activity information <bcp14>MUST NOT</bcp14> be treated as evidence of idleness. An implementation <bcp14>SHOULD</bcp14> show pending work to the operator when the configured timer policy permits indefinite waiting. Timer behavior is consistent with the Linux meaning of a disabled force timer <xref target="LINUX"/>, while the additional reference policy is a separate proposed capability.</t>
      </section>
    </section>
    <section anchor="state">
      <name>State Publication and Concurrency</name>
      <t>boot-id is a UUID for a group incarnation. It <bcp14>MUST</bcp14> change on group deletion and recreation, loss of generation state, or any generation reset. Numeric generations <bcp14>MUST NOT</bcp14> wrap within an incarnation. It need not change for an unrelated device process restart if the group state is fully preserved.</t>
      <t>state-generation <bcp14>MUST</bcp14> increase with each coherent publication that changes applied configuration, member usability or eligibility, targets, R, confirmed A, rebalance episode identity, or programming state. applied-generation <bcp14>MUST</bcp14> increase for a forwarding-confirmed change to effective A, including confirmed suppression or removal. reference-generation changes only when R is created or changed. Counter values and elapsed activity ages do not participate in state-generation.</t>
      <t>The server <bcp14>MUST</bcp14> publish each revision as an internally consistent control-state view. While programming is outstanding it <bcp14>MUST</bcp14> show pending; after a failure it <bcp14>MUST</bcp14> show failed and last-programming-error. A still-usable confirmed owner <bcp14>MAY</bcp14> be retained when a replacement fails. The server <bcp14>MUST NOT</bcp14> expose an unconfirmed owner as active or increment applied-generation merely because a command was accepted.</t>
      <t>A client collecting a group in several reads <bcp14>MUST</bcp14> bracket them with boot-id and state-generation reads. If either changes, the assembled control-state view <bcp14>MUST</bcp14> be discarded and retried. This mechanism covers the defined control state only; activity ages, statistics, and packet observations do not become a simultaneous snapshot. A server that cannot offer consistent revisions cannot claim this behavior by exposing an arbitrary counter.</t>
      <section anchor="action">
        <name>Administrative Rebalance</name>
        <t>The rebalance action requires boot-id and expected-state-generation. Under the same serialization used for group updates, the server <bcp14>MUST</bcp14> compare both before moving any bucket. A mismatch <bcp14>MUST</bcp14> return a protocol operation error with error-app-tag stale-generation and perform no action movement. A pending or failed programming state <bcp14>MUST</bcp14> reject a new action with error-app-tag group-not-ready until the server has reconciled its forwarding state.</t>
        <t>An accepted action bypasses hold-down, idle-timer, and unbalanced-timer. It <bcp14>MUST</bcp14> obey the selected policy, current destination usability, and an optional receiving-member filter. It performs a bounded pass over the current B buckets; it <bcp14>MUST NOT</bcp14> wait indefinitely for an unusable target to recover. An explicitly selected unusable receiver <bcp14>MUST</bcp14> be rejected with error-app-tag member-unusable before any move.</t>
        <t>A liveness event <bcp14>MUST</bcp14> interrupt further discretionary action movements so safety repair can run. The action returns interrupted with the number of its already confirmed migrations. A programming failure returns failed and updates operational diagnostics. Otherwise complete means no work for the requested scope remains against the accepted stable state; it does not mean the whole group is balanced if a member filter was used.</t>
        <t>The output state-generation identifies the view at action completion. A later asynchronous event can change it before the client reads again. Clients <bcp14>MUST</bcp14> re-read before retrying a stale or interrupted action. Confirmed changes are not rolled back merely because an action is interrupted. The migrated-buckets count excludes independent safety repair and counts no planned or unconfirmed moves.</t>
      </section>
      <section anchor="observation">
        <name>Observability and Notifications</name>
        <t>The server <bcp14>MUST</bcp14> expose the required control-state leaves in the module, including per-member usable, eligible, pending work, and active counts. target-buckets is present only in quota policy. Reference counts and assignments are present only in reference policy. A server advertising bucket-table <bcp14>MUST</bcp14> return B bucket entries; an entry has no active-member when no effective usable assignment is confirmed.</t>
        <t>The model exposes cumulative failure and migration counters with a discontinuity timestamp. Each confirmed migration <bcp14>MUST</bcp14> be counted once in the appropriate failure, idle, or forced category; a manual rebalance is forced even if its selected bucket happens to be idle. Initial assignment, removal, and configuration-driven replacement are not included in those three migration categories. A counter discontinuity <bcp14>MUST</bcp14> update discontinuity-time.</t>
        <t>resilient-group-event includes group, event category, boot-id, state-generation, and the count of confirmed migrations for that event. A failure event can report zero migrations while repair is pending. Notifications <bcp14>MAY</bcp14> be coalesced or rate-limited and <bcp14>MUST NOT</bcp14> be treated as a lossless transaction log. Reading the current state is the recovery procedure. Time correlation with buffer telemetry is an operational analysis; it neither proves causality nor creates a dependency between the two protocols.</t>
      </section>
    </section>
    <section anchor="model">
      <name>YANG Model and Route Binding</name>
      <t>The module uses YANG 1.1 <xref target="RFC7950"/> and NMDA <xref target="RFC8342"/>, importing the common types <xref target="RFC9911"/>, interfaces <xref target="RFC8343"/>, and routing modules <xref target="RFC8349"/>. The three independent features are bucket-activity, bucket-table, and reference-restoration. They advertise server capabilities; restoration/policy and restoration/mode select behavior per group.</t>
      <t>Static IPv4 and IPv6 routes gain a next-hop choice that references a configured group. The reference <bcp14>MUST</bcp14> resolve and its address family <bcp14>MUST</bcp14> match the route. Deleting a referenced group is invalid unless the referencing routes are changed in the same valid configuration. Operational RIB references do not require the configuration to remain present indefinitely.</t>
      <t>An IPv6 link-local next-hop address requires an outgoing interface. The model uses case-independent prefix checking and no-zone address types; a zone suffix is not an alternative to an explicit interface reference. Other next hops may identify an interface, an address, or both. Recursive resolution is a local implementation matter and <bcp14>MUST</bcp14> not create a forwarding loop or violate the usability rules.</t>
      <t>The following tree is generated from the accompanying module and uses <xref target="RFC8340"/> notation. The module organization and contact identify an individual proposal rather than asserting working group ownership. RFC XXXX is an RFC Editor placeholder to be replaced upon publication.</t>
      <sourcecode type="yangtree">module: ietf-resilient-next-hop-group

  augment /rt:routing:
    +--rw resilient-next-hop-groups
       +--rw group* [name]
          +---x rebalance
          |  +---w input
          |  |  +---w boot-id                      yang:uuid
          |  |  +---w expected-state-generation    uint64
          |  |  +---w member?
          |  |          -&gt; ../../member/id
          |  +--ro output
          |     +--ro result              enumeration
          |     +--ro migrated-buckets    uint32
          |     +--ro state-generation    uint64
          +--rw name                      string
          +--rw description?              string
          +--rw address-family            identityref
          +--rw bucket-count?             uint32
          +--rw idle-timer?               uint32 {bucket-activity}?
          +--rw unbalanced-timer?         uint32
          +--rw restoration
          |  +--rw mode?        identityref
          |  +--rw hold-down?   uint32
          |  +--rw policy?      enumeration
          +--rw member* [id]
          |  +--rw id                       uint32
          |  +--rw outgoing-interface?      if:interface-ref
          |  +--rw next-hop-ipv4-address?
          |  |       inet:ipv4-address-no-zone
          |  +--rw next-hop-ipv6-address?
          |  |       inet:ipv6-address-no-zone
          |  +--rw weight?                  uint32
          |  +--ro usable                   boolean
          |  +--ro rebalance-pending        boolean
          |  +--ro target-buckets?          uint32
          |  +--ro active-buckets           uint32
          |  +--ro reference-buckets?       uint32
          |  |       {reference-restoration}?
          |  +--ro last-transition?         yang:date-and-time
          |  +--ro eligible                 boolean
          |  +--ro rebalance-start?         yang:date-and-time
          +--ro oper-status               enumeration
          +--ro boot-id                   yang:uuid
          +--ro applied-generation        uint64
          +--ro programming-status        enumeration
          +--ro last-programming-error?   string
          +--ro reference-restored?       boolean
          |       {reference-restoration}?
          +--ro usable-members            uint32
          +--ro balanced                  boolean
          +--ro reference-generation?     uint64
          |       {reference-restoration}?
          +--ro bucket* [index] {bucket-table}?
          |  +--ro index               uint32
          |  +--ro active-member?      -&gt; ../../member/id
          |  +--ro reference-member?   -&gt; ../../member/id
          |  |       {reference-restoration}?
          |  +--ro idle-time?          uint32 {bucket-activity}?
          +--ro statistics
          |  +--ro discontinuity-time?    yang:date-and-time
          |  +--ro member-failures?       yang:counter64
          |  +--ro member-restorations?   yang:counter64
          |  +--ro failure-migrations?    yang:counter64
          |  +--ro idle-migrations?       yang:counter64
          |  +--ro forced-migrations?     yang:counter64
          +--ro state-generation          uint64
  augment /rt:routing/rt:control-plane-protocols
            /rt:control-plane-protocol/rt:static-routes/v4ur:ipv4
            /v4ur:route/v4ur:next-hop/v4ur:next-hop-options:
    +--:(resilient-next-hop-group)
       +--rw resilient-next-hop-group    leafref
  augment /rt:routing/rt:control-plane-protocols
            /rt:control-plane-protocol/rt:static-routes/v6ur:ipv6
            /v6ur:route/v6ur:next-hop/v6ur:next-hop-options:
    +--:(resilient-next-hop-group)
       +--rw resilient-next-hop-group    leafref
  augment /rt:routing/rt:ribs/rt:rib/rt:routes/rt:route/rt:next-hop
            /rt:next-hop-options:
    +--:(resilient-next-hop-group)
       +--ro resilient-next-hop-group?   leafref

  notifications:
    +---n resilient-group-event
       +--ro group               leafref
       +--ro event               identityref
       +--ro member?             uint32
       +--ro migrated-buckets    uint32
       +--ro boot-id             yang:uuid
       +--ro state-generation    uint64
</sourcecode>
    </section>
    <section anchor="yang">
      <name>YANG Module</name>
      <sourcecode type="yang" name="ietf-resilient-next-hop-group@2026-10-30.yang">module "ietf-resilient-next-hop-group" {
  yang-version "1.1";
  namespace
    "urn:ietf:params:xml:ns:yang:ietf-resilient-next-hop-grou"
  + "p";
  prefix "rnhg";
  import "ietf-yang-types" {
    prefix "yang";
    reference
      "RFC 9911: Common YANG Data Types";
  }
  import "ietf-inet-types" {
    prefix "inet";
    reference
      "RFC 9911: Common YANG Data Types";
  }
  import "ietf-interfaces" {
    prefix "if";
    reference
      "RFC 8343: A YANG Data Model for Interface Management";
  }
  import "ietf-routing" {
    prefix "rt";
    reference
      "RFC 8349: A YANG Data Model for Routing Management (NMDA
       Version)";
  }
  import "ietf-ipv4-unicast-routing" {
    prefix "v4ur";
    reference
      "RFC 8349: A YANG Data Model for Routing Management (NMDA
       Version)";
  }
  import "ietf-ipv6-unicast-routing" {
    prefix "v6ur";
    reference
      "RFC 8349: A YANG Data Model for Routing Management (NMDA
       Version)";
  }
  organization
    "Authors of this individual Internet-Draft";
  contact
    "Bohua Xu &lt;xubh15@chinaunicom.cn&gt; Junjie Wang
     &lt;wangjj@centec.com&gt; Discussion: &lt;rtgwg@ietf.org&gt; This
     module has not been adopted by a working group.";
  description
    "Configuration and operational state for resilient next-hop
     groups. This module augments the NMDA routing model with
     bucket policies, forwarding confirmation, and guarded
     administrative rebalancing.
     
     The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL',
     'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT
     RECOMMENDED', 'MAY', and 'OPTIONAL' in this document are to
     be interpreted as described in BCP 14 (RFC 2119) (RFC 8174)
     when, and only when, they appear in all capitals, as shown
     here.
     
     Copyright (c) 2026 IETF Trust and the persons identified as
     authors of the code. All rights reserved.
     
     Redistribution and use in source and binary forms, with or
     without modification, is permitted pursuant to, and subject
     to the license terms contained in, the Revised BSD License
     set forth in Section 4.c of the IETF Trust's Legal
     Provisions Relating to IETF Documents
     (https://trustee.ietf.org/license-info).
     
     This version of this YANG module is part of RFC XXXX
     (https://www.rfc-editor.org/info/rfcXXXX); see the RFC
     itself for full legal notices.";
  revision "2026-10-30" {
    description
      "Initial revision.";
    reference
      "RFC XXXX: A YANG Data Model for Resilient Next-Hop Groups";
  }
  feature "bucket-activity" {
    description
      "Supports idle-based migration and per-bucket activity
       measurement. Activity support does not imply that an idle
       bucket has no live transport sessions. Independent of
       bucket-table support.";
  }
  feature "bucket-table" {
    description
      "Indicates that the server exposes the per-bucket
       operational indirection table.";
  }
  feature "reference-restoration" {
    description
      "Supports the reference value of restoration/policy and
       its operational state. Advertising the feature does not
       select that policy. It is independent of bucket-activity
       and bucket-table.";
    reference
      "RFC XXXX: A YANG Data Model for Resilient Next-Hop Groups";
  }
  identity "restoration-mode" {
    description
      "Base identity for member recovery bucket restoration
       modes.";
  }
  identity "revertive" {
    base "restoration-mode";
    description
      "Buckets are automatically eligible for migration back to
       a recovered member after the hold-down timer expires,
       governed by the idle-timer and unbalanced-timer.";
    reference
      "RFC XXXX: A YANG Data Model for Resilient Next-Hop Groups";
  }
  identity "non-revertive" {
    base "restoration-mode";
    description
      "Buckets are not automatically migrated when a member
       recovers; rebalancing occurs only when explicitly invoked
       via the 'rebalance' action.";
    reference
      "RFC XXXX: A YANG Data Model for Resilient Next-Hop Groups";
  }
  identity "group-event" {
    description
      "Base identity for operational lifecycle events reported
       for a resilient next-hop group.";
  }
  identity "member-failure" {
    base "group-event";
    description
      "A member became unusable; repair may still be pending or
       failed.";
  }
  identity "member-restoration" {
    base "group-event";
    description
      "Buckets were migrated to a recovered usable member.";
  }
  identity "rebalance" {
    base "group-event";
    description
      "Buckets were migrated in response to an explicit
       'rebalance' action.";
  }
  identity "configuration-change" {
    base "group-event";
    description
      "Buckets were recomputed and migrated due to an
       administrative configuration change.";
  }
  identity "group-down" {
    base "group-event";
    description
      "All members of the resilient next-hop group became
       unusable.";
  }
  grouping "group-ref" {
    description
      "Reusable reference to a configured resilient next-hop
       group.";
    leaf "resilient-next-hop-group" {
      type "leafref" {
        path
          "/rt:routing/rnhg:resilient-next-hop-groups/rnhg:gr"
        + "oup/rnhg:name";
      }
      mandatory "true";
      description
        "Name of the resilient next-hop group used as the route
         next hop.";
    }
  }
  notification "resilient-group-event" {
    description
      "Reports a published control-state change. Notification
       delivery is not a durable event log; clients read current
       state to recover. boot-id and state-generation identify
       the state publication.";
    leaf "group" {
      type "leafref" {
        path
          "/rt:routing/rnhg:resilient-next-hop-groups/rnhg:gr"
        + "oup/rnhg:name";
        require-instance "false";
      }
      mandatory "true";
      description
        "The resilient next-hop group that experienced the
         event.";
    }
    leaf "event" {
      type "identityref" {
        base "group-event";
      }
      mandatory "true";
      description
        "The category of group lifecycle or migration event.";
    }
    leaf "member" {
      type "uint32";
      description
        "The member identifier associated with the event, when
         applicable.";
    }
    leaf "migrated-buckets" {
      type "uint32";
      mandatory "true";
      description
        "Confirmed owner-to-owner changes in this event, zero if
         none. An event does not assert that all planned repairs
         succeeded.";
    }
    leaf "boot-id" {
      type "yang:uuid";
      mandatory "true";
      description
        "Group incarnation for this event.";
    }
    leaf "state-generation" {
      type "uint64";
      mandatory "true";
      description
        "State publication associated with this event.";
    }
  }
  augment "/rt:routing" {
    description
      "Augments the routing subsystem with resilient next-hop
       groups.";
    container "resilient-next-hop-groups" {
      description
        "Container for resilient next-hop group configurations
         and operational state.";
      list "group" {
        key "name";
        description
          "A resilient group. Configuration nodes in operational
           represent the applied configuration. A server
           publishes state coherently under state-generation. A
           new group incarnation has a new boot-id.";
        action "rebalance" {
          description
            "Force a bounded administrative rebalance. Bypasses
             hold-down, idle and unbalanced timers, while
             preserving current usability and policy
             constraints. The precondition is checked atomically
             before any movement. Liveness changes interrupt
             remaining work. The reply counts confirmed moves
             only; the operation is not rollback-atomic.";
          input {
            leaf "boot-id" {
              type "yang:uuid";
              mandatory "true";
              description
                "Expected group boot-id; mismatch returns
                 stale-generation.";
            }
            leaf "expected-state-generation" {
              type "uint64";
              mandatory "true";
              description
                "Expected state-generation, checked at action
                 acceptance under the group serialization lock.
                 Mismatch returns stale-generation.";
            }
            leaf "member" {
              type "leafref" {
                path "../../member/id";
              }
              description
                "Optional receiving member. If absent, process
                 all usable receivers.";
            }
          }
          output {
            leaf "result" {
              type "enumeration" {
                enum "complete" {
                  description
                    "No eligible work for the requested scope
                     remains.";
                }
                enum "interrupted" {
                  description
                    "A liveness or control-state change stopped
                     further work.";
                }
                enum "failed" {
                  description
                    "Forwarding programming failed; inspect
                     operational state.";
                }
              }
              mandatory "true";
              description
                "Outcome of this accepted action.";
            }
            leaf "migrated-buckets" {
              type "uint32";
              mandatory "true";
              description
                "Number of confirmed owner-to-owner changes made
                 by this action. Safety repair after an
                 interruption is excluded.";
            }
            leaf "state-generation" {
              type "uint64";
              mandatory "true";
              description
                "State-generation at completion of this action
                 reply.";
            }
          }
        }
        leaf "name" {
          type "string" {
            length "1..64";
          }
          description
            "Unique administrative name of the resilient
             next-hop group.";
        }
        leaf "description" {
          type "string";
          description
            "Human-readable description of the group.";
        }
        leaf "address-family" {
          type "identityref" {
            base "rt:address-family";
          }
          must
            "derived-from-or-self(., 'rt:ipv4') or derived-fr"
          + "om-or-self(., 'rt:ipv6')" {
            error-message
              "Only IPv4 and IPv6 unicast groups are defined.";
          }
          mandatory "true";
          description
            "IPv4 or IPv6 unicast address family for this group.";
        }
        leaf "bucket-count" {
          type "uint32" {
            range "1..65535";
          }
          default "128";
          description
            "Number B of logical buckets. Hardware limitations
             are validated before accepting configuration; an
             unsupported size is rejected. Changing B can remap
             all flows and requires a configuration commit.";
          reference
            "RFC XXXX: A YANG Data Model for Resilient Next-Hop
             Groups";
        }
        leaf "idle-timer" {
          if-feature "bucket-activity";
          type "uint32" {
            range "0..65535";
          }
          units "seconds";
          default "120";
          description
            "Seconds without observed packets before a bucket is
             idle. Zero explicitly makes all buckets eligible
             for idle-based movement. Idleness does not imply
             the absence of established sessions.";
          reference
            "RFC XXXX: A YANG Data Model for Resilient Next-Hop
             Groups";
        }
        leaf "unbalanced-timer" {
          type "uint32" {
            range "0..65535";
          }
          units "seconds";
          default "0";
          description
            "Seconds after an eligible receiver develops unmet
             work before automatic forced movement to that
             receiver is permitted. Zero always disables
             timer-forced movement, including when bucket
             activity is unavailable. Uses monotonic elapsed
             time. It is not a guarantee on hardware programming
             completion time.";
          reference
            "RFC XXXX: A YANG Data Model for Resilient Next-Hop
             Groups";
        }
        container "restoration" {
          description
            "Parameters governing bucket restoration when a
             failed member becomes usable again.";
          leaf "mode" {
            type "identityref" {
              base "restoration-mode";
            }
            default "rnhg:revertive";
            description
              "Specifies whether bucket restoration occurs
               automatically (revertive) or only via explicit
               action (non-revertive).";
          }
          leaf "hold-down" {
            type "uint32" {
              range "0..3600";
            }
            units "seconds";
            default "0";
            description
              "Seconds of continuous usability after recovery
               before a member can receive discretionary
               automatic migrations. Does not delay failure
               repair, allocation from a down group,
               configuration application, or an explicit
               rebalance action.";
          }
          leaf "policy" {
            type "enumeration" {
              enum "quota" {
                description
                  "Use integer weighted targets for usable
                   members.";
              }
              enum "reference" {
                if-feature "reference-restoration";
                description
                  "Restore the reference assignment for usable
                   owners.";
              }
            }
            default "quota";
            description
              "Select the per-group restoration policy.";
          }
        }
        list "member" {
          must
            "outgoing-interface or next-hop-ipv4-address or n"
          + "ext-hop-ipv6-address" {
            error-message
              "Each member must specify an outgoing-interface"
            + ", a\nnext-hop address, or both.";
            description
              "Ensures each member identifies a valid forwarding
               next hop.";
          }
          must
            "not(starts-with(translate(next-hop-ipv6-address,"
          + " 'ABCDEF', 'abcdef'), 'fe8') or starts-with(tran"
          + "slate(next-hop-ipv6-address, 'ABCDEF', 'abcdef')"
          + ", 'fe9') or starts-with(translate(next-hop-ipv6-"
          + "address, 'ABCDEF', 'abcdef'), 'fea') or starts-w"
          + "ith(translate(next-hop-ipv6-address, 'ABCDEF', '"
          + "abcdef'), 'feb')) or outgoing-interface" {
            error-message
              "An IPv6 link-local next hop requires outgoing-"
            + "interface.";
            description
              "Case-independent fe80::/10 interface-scope
               validation.";
          }
          key "id";
          min-elements "1";
          description
            "Configured next-hop members of the resilient group.";
          leaf "id" {
            type "uint32";
            description
              "Unique numeric identifier of the member within
               the group.";
          }
          leaf "outgoing-interface" {
            type "if:interface-ref";
            description
              "Outgoing interface for this next-hop member.";
          }
          leaf "next-hop-ipv4-address" {
            when
              "derived-from-or-self(../../address-family, 'rt"
            + ":ipv4')";
            type "inet:ipv4-address-no-zone";
            description
              "IPv4 next-hop address of the member.";
          }
          leaf "next-hop-ipv6-address" {
            when
              "derived-from-or-self(../../address-family, 'rt"
            + ":ipv6')";
            type "inet:ipv6-address-no-zone";
            description
              "IPv6 next-hop address of the member.";
          }
          leaf "weight" {
            type "uint32" {
              range "1..max";
            }
            default "1";
            description
              "Relative load-sharing weight of the member (w_i).
               Target bucket allocation is proportional to w_i.";
          }
          leaf "usable" {
            type "boolean";
            config "false";
            mandatory "true";
            description
              "Indicates whether this member is currently
               resolved and usable for packet forwarding.";
          }
          leaf "rebalance-pending" {
            type "boolean";
            config "false";
            mandatory "true";
            description
              "True if the member is usable and currently holds
               fewer buckets than its target quota (or has
               un-restored reference buckets) awaiting
               migration.";
          }
          leaf "target-buckets" {
            when "../../restoration/policy = 'quota'";
            type "uint32";
            config "false";
            description
              "Current integer target bucket count (T_i) in
               proportional quota mode; omitted when operating
               under reference-restoration policy.";
          }
          leaf "active-buckets" {
            type "uint32";
            config "false";
            mandatory "true";
            description
              "Count of logical buckets with a confirmed usable
               assignment to this member. A suppressed unusable
               hardware entry is not counted.";
          }
          leaf "reference-buckets" {
            when "../../restoration/policy = 'reference'";
            if-feature "reference-restoration";
            type "uint32";
            config "false";
            description
              "Number of buckets whose canonical baseline
               reference assignment R(b) points to this member.";
          }
          leaf "last-transition" {
            type "yang:date-and-time";
            config "false";
            description
              "Timestamp of the most recent change in this
               member's 'usable' state.";
          }
          leaf "eligible" {
            type "boolean";
            config "false";
            mandatory "true";
            description
              "True when usable and the hold-down for automatic
               receipt has expired. Safety repair and
               administrative action may bypass it.";
          }
          leaf "rebalance-start" {
            type "yang:date-and-time";
            config "false";
            description
              "Wall-clock display of the start of the current
               eligible unmet-work episode. Absent without such
               an episode. Timers use monotonic time, not
               subtraction of this timestamp.";
          }
        }
        leaf "oper-status" {
          type "enumeration" {
            enum "up" {
              description
                "Every bucket has a confirmed usable effective
                 owner.";
            }
            enum "down" {
              description
                "No bucket has a confirmed usable effective
                 owner.";
            }
            enum "degraded" {
              description
                "Some, but not all, buckets have confirmed
                 usable owners.";
            }
          }
          config "false";
          mandatory "true";
          description
            "Forwarding availability of the group, derived from
             confirmed effective bucket assignments, not merely
             member liveness.";
        }
        leaf "boot-id" {
          type "yang:uuid";
          config "false";
          mandatory "true";
          description
            "UUID identifying this group incarnation and counter
             epoch. Changes on recreation or any loss/reset of
             generation state. It is not necessarily a device
             reboot identifier.";
        }
        leaf "applied-generation" {
          type "uint64";
          config "false";
          mandatory "true";
          description
            "Counter advanced on each forwarding-confirmed
             change of effective active assignments, including
             confirmed removal or suppression. State-generation
             also advances for the publication. No wrap within
             boot-id. This is not a general configuration
             revision.";
        }
        leaf "programming-status" {
          type "enumeration" {
            enum "ready" {
              description
                "The latest bucket table update has been
                 confirmed by the forwarding plane.";
            }
            enum "pending" {
              description
                "A bucket table update is in progress and not
                 yet confirmed by the forwarding plane.";
            }
            enum "failed" {
              description
                "The most recent forwarding-plane update failed.";
            }
          }
          config "false";
          mandatory "true";
          description
            "Status of hardware/forwarding-plane programming for
             the group.";
        }
        leaf "last-programming-error" {
          type "string";
          config "false";
          description
            "Last programming error. Absent after a subsequent
             successful programming transaction has cleared the
             error.";
        }
        leaf "reference-restored" {
          when "../restoration/policy = 'reference'";
          if-feature "reference-restoration";
          type "boolean";
          config "false";
          description
            "True when at least one member is usable, all
             buckets have confirmed usable assignments, and
             every usable reference owner owns its reference
             buckets. This does not promise intermediate weight
             balance.";
        }
        leaf "usable-members" {
          type "uint32";
          config "false";
          mandatory "true";
          description
            "Current count of usable members in the group.";
        }
        leaf "balanced" {
          type "boolean";
          config "false";
          mandatory "true";
          description
            "True if all buckets are assigned to usable members
             and the active restoration policy's balance
             condition is satisfied.";
        }
        leaf "reference-generation" {
          when "../restoration/policy = 'reference'";
          if-feature "reference-restoration";
          type "uint64";
          config "false";
          description
            "Advanced whenever R is created or changed.
             Usability transitions never change R. Meaningful
             only in reference policy. No wrap within boot-id.";
        }
        list "bucket" {
          if-feature "bucket-table";
          key "index";
          config "false";
          description
            "Operational state of individual hash buckets in the
             group's indirection table.";
          leaf "index" {
            type "uint32";
            must ". &lt; ../../bucket-count" {
              error-message
                "Bucket index must be less than bucket-count.";
            }
            description
              "Zero-based bucket index (0 .. bucket-count - 1).";
          }
          leaf "active-member" {
            type "leafref" {
              path "../../member/id";
              require-instance "false";
            }
            description
              "Confirmed effective usable owner. Absent for a
               confirmed suppressed or unassigned bucket.
               Unknown hardware state is reported as failed
               programming and must not be presented as
               successful repair.";
          }
          leaf "reference-member" {
            when "../../restoration/policy = 'reference'";
            if-feature "reference-restoration";
            type "leafref" {
              path "../../member/id";
              require-instance "false";
            }
            description
              "Canonical reference member ID for this bucket
               (R(b)).";
          }
          leaf "idle-time" {
            if-feature "bucket-activity";
            type "uint32";
            units "seconds";
            description
              "Monotonic seconds since a packet was observed for
               this bucket, saturated at 4294967295. Not covered
               by state-generation; absent when activity data is
               unavailable.";
          }
        }
        container "statistics" {
          config "false";
          description
            "Cumulative operational counters for the resilient
             next-hop group.";
          leaf "discontinuity-time" {
            type "yang:date-and-time";
            description
              "Timestamp of the most recent occasion on which
               one or more counters experienced a discontinuity.";
          }
          leaf "member-failures" {
            type "yang:counter64";
            description
              "Total number of transitions of any member from
               usable to unusable.";
          }
          leaf "member-restorations" {
            type "yang:counter64";
            description
              "Total number of transitions of any member from
               unusable to usable.";
          }
          leaf "failure-migrations" {
            type "yang:counter64";
            description
              "Total number of immediate bucket migrations
               triggered by member failures.";
          }
          leaf "idle-migrations" {
            type "yang:counter64";
            description
              "Total number of idle-based bucket migrations
               performed while the migrated bucket was idle.";
          }
          leaf "forced-migrations" {
            type "yang:counter64";
            description
              "Total number of bucket migrations performed on
               active buckets due to unbalanced-timer expiry or
               explicit 'rebalance' action invocation.";
          }
        }
        leaf "state-generation" {
          type "uint64";
          config "false";
          mandatory "true";
          description
            "Revision of applied configuration, usability,
             eligibility, targets, reference and active
             assignments, and programming state. Increment on
             each coherent publication affecting these values.
             Excludes activity ages and statistics. Does not
             wrap within boot-id.";
        }
      }
    }
  }
  augment
    "/rt:routing/rt:control-plane-protocols/rt:control-plane-"
  + "protocol/rt:static-routes/v4ur:ipv4/v4ur:route/v4ur:next"
  + "-hop/v4ur:next-hop-options" {
    description
      "Enables an IPv4 static route to reference a resilient
       next-hop group.";
    case "resilient-next-hop-group" {
      uses "group-ref" {
        refine "resilient-next-hop-group" {
          must
            "derived-from-or-self(/rt:routing/rnhg:resilient-"
          + "next-hop-groups/rnhg:group[rnhg:name = current()"
          + "]/rnhg:address-family, 'rt:ipv4')" {
            error-message
              "The referenced resilient next-hop group must b"
            + "e\nconfigured for the IPv4 address family.";
          }
        }
      }
    }
  }
  augment
    "/rt:routing/rt:control-plane-protocols/rt:control-plane-"
  + "protocol/rt:static-routes/v6ur:ipv6/v6ur:route/v6ur:next"
  + "-hop/v6ur:next-hop-options" {
    description
      "Enables an IPv6 static route to reference a resilient
       next-hop group.";
    case "resilient-next-hop-group" {
      uses "group-ref" {
        refine "resilient-next-hop-group" {
          must
            "derived-from-or-self(/rt:routing/rnhg:resilient-"
          + "next-hop-groups/rnhg:group[rnhg:name = current()"
          + "]/rnhg:address-family, 'rt:ipv6')" {
            error-message
              "The referenced resilient next-hop group must b"
            + "e\nconfigured for the IPv6 address family.";
          }
        }
      }
    }
  }
  augment
    "/rt:routing/rt:ribs/rt:rib/rt:routes/rt:route/rt:next-ho"
  + "p/rt:next-hop-options" {
    description
      "Exposes the resilient next-hop group bound to an active
       route in the Routing Information Base (RIB).";
    case "resilient-next-hop-group" {
      leaf "resilient-next-hop-group" {
        type "leafref" {
          path
            "/rt:routing/rnhg:resilient-next-hop-groups/rnhg:"
          + "group/rnhg:name";
          require-instance "false";
        }
        description
          "Name of the resilient next-hop group used by this
           active RIB route.";
      }
    }
  }
}
</sourcecode>
    </section>
    <section anchor="examples">
      <name>Worked Examples and Interoperability Checks</name>
      <section anchor="quota-example">
        <name>Integer Targets</name>
        <t>For B=5 and two usable equal-weight members, valid targets are (3,2) or (2,3). If A has counts (4,1) and the selected targets are (3,2), one permitted donor-to-receiver migration reaches the target. Testing against real shares (2.5,2.5) instead of fixed integer targets can incorrectly keep the group pending or cause oscillation. The wire model exposes the selected integer values.</t>
      </section>
      <section anchor="reference-example">
        <name>Restoration to a Reference Assignment</name>
        <table anchor="ref-example">
          <name>Reference Restoration with Two Failures</name>
          <thead>
            <tr>
              <th>Step</th>
              <th>Reference R</th>
              <th>Active A</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Initial</td>
              <td>[1,2,3,1,2,3]</td>
              <td>[1,2,3,1,2,3]</td>
            </tr>
            <tr>
              <td>1 fails</td>
              <td>Unchanged</td>
              <td>[2,2,3,2,2,3]</td>
            </tr>
            <tr>
              <td>2 fails</td>
              <td>Unchanged</td>
              <td>[3,3,3,3,3,3]</td>
            </tr>
            <tr>
              <td>1 recovers and its work completes</td>
              <td>Unchanged</td>
              <td>[1,3,3,1,3,3]</td>
            </tr>
            <tr>
              <td>2 recovers and its work completes</td>
              <td>Unchanged</td>
              <td>[1,2,3,1,2,3]</td>
            </tr>
          </tbody>
        </table>
        <t>Temporary holders in this example are choices, not a prescribed allocation algorithm. Reversing recovery order also reaches R when the same conditional convergence requirements hold. Recovery of a reference owner moves only its reference buckets; it does not impose quota balance on the temporary holders.</t>
      </section>
      <section anchor="timer-example">
        <name>Waiting and Stale Actions</name>
        <t>With no bucket-activity capability and unbalanced-timer=0, a recovered member can remain pending in revertive mode. This is intentional: the server has neither evidence of idleness nor permission for timer-forced movement. Setting a positive force timer or invoking a guarded action can progress the work.</t>
        <t>If a client reads state-generation=8 and a member becomes unusable without any mapping update yet confirmed, state-generation still advances for the liveness change. An action carrying 8 is rejected. Comparing only applied-generation would miss this change, which is why the action precondition uses state-generation.</t>
      </section>
      <section anchor="config-example">
        <name>Example IPv4 Static Route Configuration</name>
        <t>This JSON configuration is validated against the module and its dependencies. It uses the default quota policy. The address and interface values are illustrative.</t>
        <sourcecode type="json">{
 "ietf-interfaces:interfaces": {
  "interface": [
   {
    "name": "eth0",
    "type": "iana-if-type:ethernetCsmacd"
   }
  ]
 },
 "ietf-routing:routing": {
  "ietf-resilient-next-hop-group:resilient-next-hop-groups": {
   "group": [
    {
     "name": "fabric",
     "address-family": "ietf-routing:ipv4",
     "bucket-count": 8,
     "member": [
      {
       "id": 1,
       "next-hop-ipv4-address": "192.0.2.1",
       "outgoing-interface": "eth0"
      }
     ]
    }
   ]
  },
  "control-plane-protocols": {
   "control-plane-protocol": [
    {
     "type": "ietf-routing:static",
     "name": "static",
     "static-routes": {
      "ietf-ipv4-unicast-routing:ipv4": {
       "route": [
        {
         "destination-prefix": "192.0.2.0/24",
         "next-hop": {
          "ietf-resilient-next-hop-group:resilient-next-hop-group":
  "fabric"
         }
        }
       ]
      }
     }
    }
   ]
  }
 }
}
</sourcecode>
      </section>
      <section anchor="tests">
        <name>Implementation Review Checklist</name>
        <t>Interoperability testing <bcp14>SHOULD</bcp14> exercise all eight feature combinations, both restoration modes, the timer combinations in <xref target="timer-table"/>, fractional weighted shares, B smaller than member count, all-members-down recovery, hardware rejection after partial programming, action interruption, configuration changes during pending work, counter discontinuity, and group recreation with the same name.</t>
        <t>Schema validation <bcp14>SHOULD</bcp14> cover address-family mismatches, unresolved route references, uppercase and lowercase IPv6 link-local addresses without an interface, invalid weights and bucket counts, and reference policy selection when the feature is absent. Behavior simulations and schema tests do not substitute for two independent forwarding implementations.</t>
      </section>
    </section>
    <section anchor="operations">
      <name>Operational Considerations</name>
      <t>Idle intervals should be chosen with expected traffic gaps and session affinity in mind. No finite idle interval proves that a stateful service has forgotten a flow. Non-revertive mode can avoid automatic recovery movements, but cannot prevent required failure repair or administrator-initiated changes.</t>
      <t>Reference policy provides repeatable placement within one reference generation. It can require more movements than a policy that merely reaches current quotas, and can temporarily produce uneven survivor allocation. Operators should select it only when restoration to the recorded baseline matters.</t>
      <t>Resilient local assignment does not guarantee transport ordering, losslessness, or availability of state at a replacement middlebox. A changed path can have different latency; even a flow that keeps its local next hop can encounter changes downstream. These limits <bcp14>SHOULD</bcp14> be disclosed in implementation documentation.</t>
    </section>
    <section anchor="security">
      <name>Security Considerations</name>
      <t>This section follows the YANG security guidance in <xref target="RFC9907"/>. The module is intended for use with authenticated management protocols such as NETCONF <xref target="RFC6241"/> and RESTCONF <xref target="RFC8040"/>, over their secure transport mappings. Deployments <bcp14>MUST</bcp14> authenticate peers and protect confidentiality and integrity. The Network Configuration Access Control Model <xref target="RFC8341"/> can restrict both data access and operations.</t>
      <t>All configuration under /routing/resilient-next-hop-groups is sensitive. Changing membership or next-hop addresses can redirect traffic; changing weights, bucket-count, or policy can move active flows; setting idle-timer to zero or reducing unbalanced-timer can permit disruptive movement. These writes <bcp14>MUST</bcp14> be restricted to authorized administrators.</t>
      <t>The rebalance action bypasses recovery timers and can move active traffic. Authorization <bcp14>MUST</bcp14> be checked independently of its generation precondition. Repeated reads, rejected actions, notifications, and large bucket-table retrievals can consume resources; servers <bcp14>MUST</bcp14> impose suitable request, work, and notification limits. A generation token prevents a stale decision from executing; it is not a secret or an authorization credential.</t>
      <t>Operational next hops, interface bindings, bucket assignments, member transitions, and error diagnostics reveal topology and traffic-handling policy. Access <bcp14>SHOULD</bcp14> be limited to authorized consumers. Implementations <bcp14>SHOULD</bcp14> avoid exposing secrets or unnecessary internal details in diagnostic strings. Malicious liveness inputs can induce repeated repair, so liveness sources and their authentication require deployment-specific protection.</t>
    </section>
    <section anchor="iana">
      <name>IANA Considerations</name>
      <section anchor="iana-xml">
        <name>XML Namespace</name>
        <t>IANA is requested to register urn:ietf:params:xml:ns:yang:ietf-resilient-next-hop-group in the IETF XML Registry <xref target="RFC3688"/>. Registrant Contact: The IESG. XML: N/A; the requested URI is an XML namespace.</t>
      </section>
      <section anchor="iana-yang">
        <name>YANG Module</name>
        <t>IANA is requested to register the module under the YANG Module Names registry <xref target="RFC6020"/>: Name: ietf-resilient-next-hop-group; Namespace: urn:ietf:params:xml:ns:yang:ietf-resilient-next-hop-group; Prefix: rnhg; Reference: RFC XXXX. No assignment or working group ownership is implied before approval.</t>
      </section>
    </section>
  </middle>
  <back>
    <references>
      <name>Normative References</name>
      <reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
        <front>
          <title>Key words for use in RFCs to Indicate Requirement Levels</title>
          <author fullname="S. Bradner" initials="S." surname="Bradner"/>
          <date month="March" year="1997"/>
        </front>
        <seriesInfo name="BCP" value="14"/>
        <seriesInfo name="RFC" value="2119"/>
        <seriesInfo name="DOI" value="10.17487/RFC2119"/>
      </reference>
      <reference anchor="RFC3688" target="https://www.rfc-editor.org/info/rfc3688">
        <front>
          <title>The IETF XML Registry</title>
          <author fullname="M. Mealling" initials="M." surname="Mealling"/>
          <date month="January" year="2004"/>
        </front>
        <seriesInfo name="BCP" value="81"/>
        <seriesInfo name="RFC" value="3688"/>
        <seriesInfo name="DOI" value="10.17487/RFC3688"/>
      </reference>
      <reference anchor="RFC6020" target="https://www.rfc-editor.org/info/rfc6020">
        <front>
          <title>YANG - A Data Modeling Language for the Network Configuration Protocol (NETCONF)</title>
          <author fullname="M. Bjorklund" initials="M." role="editor" surname="Bjorklund"/>
          <date month="October" year="2010"/>
        </front>
        <seriesInfo name="RFC" value="6020"/>
        <seriesInfo name="DOI" value="10.17487/RFC6020"/>
      </reference>
      <reference anchor="RFC6241" target="https://www.rfc-editor.org/info/rfc6241">
        <front>
          <title>Network Configuration Protocol (NETCONF)</title>
          <author fullname="R. Enns" initials="R." role="editor" surname="Enns"/>
          <author fullname="M. Bjorklund" initials="M." role="editor" surname="Bjorklund"/>
          <author fullname="J. Schoenwaelder" initials="J." role="editor" surname="Schoenwaelder"/>
          <author fullname="A. Bierman" initials="A." role="editor" surname="Bierman"/>
          <date month="June" year="2011"/>
        </front>
        <seriesInfo name="RFC" value="6241"/>
        <seriesInfo name="DOI" value="10.17487/RFC6241"/>
      </reference>
      <reference anchor="RFC7950" target="https://www.rfc-editor.org/info/rfc7950">
        <front>
          <title>The YANG 1.1 Data Modeling Language</title>
          <author fullname="M. Bjorklund" initials="M." role="editor" surname="Bjorklund"/>
          <date month="August" year="2016"/>
        </front>
        <seriesInfo name="RFC" value="7950"/>
        <seriesInfo name="DOI" value="10.17487/RFC7950"/>
      </reference>
      <reference anchor="RFC8040" target="https://www.rfc-editor.org/info/rfc8040">
        <front>
          <title>RESTCONF Protocol</title>
          <author fullname="A. Bierman" initials="A." surname="Bierman"/>
          <author fullname="M. Bjorklund" initials="M." surname="Bjorklund"/>
          <author fullname="K. Watsen" initials="K." surname="Watsen"/>
          <date month="January" year="2017"/>
        </front>
        <seriesInfo name="RFC" value="8040"/>
        <seriesInfo name="DOI" value="10.17487/RFC8040"/>
      </reference>
      <reference anchor="RFC8174" target="https://www.rfc-editor.org/info/rfc8174">
        <front>
          <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
          <author fullname="B. Leiba" initials="B." surname="Leiba"/>
          <date month="May" year="2017"/>
        </front>
        <seriesInfo name="BCP" value="14"/>
        <seriesInfo name="RFC" value="8174"/>
        <seriesInfo name="DOI" value="10.17487/RFC8174"/>
      </reference>
      <reference anchor="RFC8341" target="https://www.rfc-editor.org/info/rfc8341">
        <front>
          <title>Network Configuration Access Control Model</title>
          <author fullname="A. Bierman" initials="A." surname="Bierman"/>
          <author fullname="M. Bjorklund" initials="M." surname="Bjorklund"/>
          <date month="March" year="2018"/>
        </front>
        <seriesInfo name="STD" value="91"/>
        <seriesInfo name="RFC" value="8341"/>
        <seriesInfo name="DOI" value="10.17487/RFC8341"/>
      </reference>
      <reference anchor="RFC8342" target="https://www.rfc-editor.org/info/rfc8342">
        <front>
          <title>Network Management Datastore Architecture (NMDA)</title>
          <author fullname="M. Bjorklund" initials="M." surname="Bjorklund"/>
          <author fullname="J. Schoenwaelder" initials="J." surname="Schoenwaelder"/>
          <author fullname="P. Shafer" initials="P." surname="Shafer"/>
          <author fullname="K. Watsen" initials="K." surname="Watsen"/>
          <author fullname="R. Wilton" initials="R." surname="Wilton"/>
          <date month="March" year="2018"/>
        </front>
        <seriesInfo name="RFC" value="8342"/>
        <seriesInfo name="DOI" value="10.17487/RFC8342"/>
      </reference>
      <reference anchor="RFC8343" target="https://www.rfc-editor.org/info/rfc8343">
        <front>
          <title>A YANG Data Model for Interface Management</title>
          <author fullname="M. Bjorklund" initials="M." surname="Bjorklund"/>
          <date month="March" year="2018"/>
        </front>
        <seriesInfo name="RFC" value="8343"/>
        <seriesInfo name="DOI" value="10.17487/RFC8343"/>
      </reference>
      <reference anchor="RFC8349" target="https://www.rfc-editor.org/info/rfc8349">
        <front>
          <title>A YANG Data Model for Routing Management (NMDA Version)</title>
          <author fullname="L. Lhotka" initials="L." surname="Lhotka"/>
          <author fullname="A. Lindem" initials="A." surname="Lindem"/>
          <author fullname="Y. Qu" initials="Y." surname="Qu"/>
          <date month="March" year="2018"/>
        </front>
        <seriesInfo name="RFC" value="8349"/>
        <seriesInfo name="DOI" value="10.17487/RFC8349"/>
      </reference>
      <reference anchor="RFC9911" target="https://www.rfc-editor.org/info/rfc9911">
        <front>
          <title>Common YANG Data Types</title>
          <author fullname="J. Schoenwaelder" initials="J." role="editor" surname="Schoenwaelder"/>
          <date month="December" year="2025"/>
        </front>
        <seriesInfo name="RFC" value="9911"/>
        <seriesInfo name="DOI" value="10.17487/RFC9911"/>
      </reference>
    </references>
    <references>
      <name>Informative References</name>
      <reference anchor="GRIBI" target="https://github.com/openconfig/gribi/blob/master/v1/proto/service/gribi.proto">
        <front>
          <title>gRIBI Service Definition</title>
          <author>
            <organization>OpenConfig</organization>
          </author>
          <date/>
        </front>
      </reference>
      <reference anchor="I-D.ietf-bess-weighted-hrw" target="https://datatracker.ietf.org/doc/html/draft-ietf-bess-weighted-hrw-02">
        <front>
          <title>Weighted HRW and its applications</title>
          <author fullname="Satya R. Mohanty" initials="S. R." surname="Mohanty"/>
          <author fullname="M. P. Mishra" initials="M. P." surname="Mishra"/>
          <author fullname="A. Lindem" initials="A." surname="Lindem"/>
          <author fullname="A. Sajassi" initials="A." surname="Sajassi"/>
          <author fullname="J. Drake" initials="J." surname="Drake"/>
          <date day="7" month="July" year="2025"/>
        </front>
        <seriesInfo name="Internet-Draft" value="draft-ietf-bess-weighted-hrw-02"/>
      </reference>
      <reference anchor="LINUX" target="https://docs.kernel.org/networking/nexthop-group-resilient.html">
        <front>
          <title>Resilient Next-hop Groups</title>
          <author>
            <organization>Linux contributors</organization>
          </author>
          <date/>
        </front>
      </reference>
      <reference anchor="OC-AFT" target="https://github.com/openconfig/public/blob/master/release/models/aft/openconfig-aft-common.yang">
        <front>
          <title>OpenConfig Abstract Forwarding Table Common Model, revision 2026-10-04</title>
          <author>
            <organization>OpenConfig</organization>
          </author>
          <date/>
        </front>
      </reference>
      <reference anchor="OC-STATIC" target="https://github.com/openconfig/public/blob/master/release/models/network-instance/openconfig-network-instance-static.yang">
        <front>
          <title>OpenConfig Network Instance Static Model, revision 2026-05-12</title>
          <author>
            <organization>OpenConfig</organization>
          </author>
          <date/>
        </front>
      </reference>
      <reference anchor="RFC2992" target="https://www.rfc-editor.org/info/rfc2992">
        <front>
          <title>Analysis of an Equal-Cost Multi-Path Algorithm</title>
          <author fullname="C. Hopps" initials="C." surname="Hopps"/>
          <date month="November" year="2000"/>
        </front>
        <seriesInfo name="RFC" value="2992"/>
        <seriesInfo name="DOI" value="10.17487/RFC2992"/>
      </reference>
      <reference anchor="RFC8340" target="https://www.rfc-editor.org/info/rfc8340">
        <front>
          <title>YANG Tree Diagrams</title>
          <author fullname="M. Bjorklund" initials="M." surname="Bjorklund"/>
          <author fullname="L. Berger" initials="L." role="editor" surname="Berger"/>
          <date month="March" year="2018"/>
        </front>
        <seriesInfo name="BCP" value="215"/>
        <seriesInfo name="RFC" value="8340"/>
        <seriesInfo name="DOI" value="10.17487/RFC8340"/>
      </reference>
      <reference anchor="RFC9907" target="https://www.rfc-editor.org/info/rfc9907">
        <front>
          <title>Guidelines for Authors and Reviewers of Documents Containing YANG Data Models</title>
          <author fullname="A. Bierman" initials="A." surname="Bierman"/>
          <author fullname="M. Boucadair" initials="M." role="editor" surname="Boucadair"/>
          <author fullname="Q. Wu" initials="Q." surname="Wu"/>
          <date month="March" year="2026"/>
        </front>
        <seriesInfo name="BCP" value="216"/>
        <seriesInfo name="RFC" value="9907"/>
        <seriesInfo name="DOI" value="10.17487/RFC9907"/>
      </reference>
      <reference anchor="SAI-NHG" target="https://github.com/opencomputeproject/SAI/blob/master/inc/sainexthopgroup.h">
        <front>
          <title>Switch Abstraction Interface: Next Hop Group API</title>
          <author>
            <organization>Open Compute Project</organization>
          </author>
          <date/>
        </front>
      </reference>
    </references>
  </back>
</rfc>
