Skip to content

Latest commit

 

History

History
3262 lines (2790 loc) · 189 KB

File metadata and controls

3262 lines (2790 loc) · 189 KB

gRPC API Reference

Document class: REFERENCE.

This reference documents rustbgpd's gRPC API. rustbgpd exposes twelve native rustbgpd.v1 gRPC services (Global, Config, Neighbor, Policy, PeerGroup, Rib, BFD, RPKI, Event, Injection, Control, Evpn) plus the gnmi.gNMI OpenConfig service over one or more configured listeners. The gNMI surface is read-only telemetry (Capabilities / Get / Subscribe) plus an operator-tier Set subset — transaction-backed create/update/delete for static numbered BGP neighbors, peer-group catalog entries, dynamic-neighbor prefixes, and the commit-confirmed extension through ADR-0076. Unsupported Set paths return UNIMPLEMENTED. The default listener is a local Unix domain socket at /var/lib/rustbgpd/grpc.sock.

For same-host administration, prefer UDS:

grpcurl -plaintext -unix /var/lib/rustbgpd/grpc.sock \
  -import-path . -proto proto/rustbgpd.proto \
  rustbgpd.v1.GlobalService/GetGlobal

The remaining examples below use grpcurl against an explicit local TCP listener for readability. Those examples require grpc_tcp to be enabled, and under the default tier authorization a TCP listener must authenticate — bearer token plus a role-mapped principal, or native mTLS:

[global.telemetry.grpc_tcp]
address = "127.0.0.1:50051"
token_file = "/etc/rustbgpd/grpc.token"
principal = "automation.example"

[security.grpc.roles]
"automation.example" = "operator"

For readability the examples omit the accompanying -H "authorization: bearer <token>" metadata header.

The proto definition lives at proto/rustbgpd.proto.

For a worked client in a general-purpose language — stub generation, mTLS with composed bearer-token call credentials, per-call deadlines, and the authorization tiers each call needs — see examples/python-client/.

The project-wide API remains alpha outside the explicit native-gRPC methods listed in the narrow v1 route-server / route-reflector contract. That machine inventory pins method names, streaming modes, and top-level request/response messages; a service or RPC existing in this reference does not by itself make it v1-stable.

Service overview

Service RPCs Purpose
GlobalService GetGlobal Daemon identity
ConfigService DiffRuntimeConfig, PlanConfigTransaction, StreamPlanConfigTransaction, StreamApplyConfigTransaction, ApplyConfigTransaction, ConfirmConfigTransaction, AbortConfigTransaction, GetConfigTransactionStatus, GetEffectiveConfig, ListConfigHistory, RollbackConfigTransaction Candidate-vs-live config diff, effective running config with defaults resolved (selected default-empty policy lists omitted) and secrets redacted, plus the v1 config-transaction lifecycle: validate/plan, commit/apply (incl. commit-confirmed), confirm, abort, status, and the bounded recorded config history with Junos-style rollback N through the same transaction executor; the outside-v1 streams provide bounded large-candidate plan and apply ingress
NeighborService AddNeighbor, DeleteNeighbor, ListNeighbors, GetNeighborState, EnableNeighbor, DisableNeighbor, SoftResetIn, RefreshOutbound, ReplayOutbound, ResetNeighbor, SetGracefulShutdown, AddDynamicNeighbor, DeleteDynamicNeighbor, ListDynamicNeighbors Peer lifecycle, inbound soft reset, single-peer outbound re-advertisement, outside-v1 administrative session reset, RFC 8326 graceful-shutdown toggle, and dynamic-neighbor CRUD — AddDynamicNeighbor / DeleteDynamicNeighbor add and remove [[dynamic_neighbors]] prefix ranges at runtime (queued to the config file), ListDynamicNeighbors for visibility
PolicyService ListPolicies, GetPolicy, SetPolicy, DeletePolicy, ListNeighborSets, GetNeighborSet, SetNeighborSet, DeleteNeighborSet, GetGlobalPolicyChains, GetNeighborPolicyChains, SetGlobalImportChain, SetGlobalExportChain, ClearGlobalImportChain, ClearGlobalExportChain, SetNeighborImportChain, SetNeighborExportChain, ClearNeighborImportChain, ClearNeighborExportChain, ExplainImportPolicy, ListRejectedRoutes, TestPolicy, GetPolicyStats, GetValidationPolicyPosture Named policy CRUD, neighbor sets, global/per-neighbor chain attachment, import-policy diagnostics, read-only candidate-policy dry runs, live counters, and bounded invalid-validation disposition posture
PeerGroupService ListPeerGroups, GetPeerGroup, SetPeerGroup, DeletePeerGroup, SetNeighborPeerGroup, ClearNeighborPeerGroup Peer-group CRUD and neighbor membership assignment
RibService ListReceivedRoutes, ListBestRoutes, ListAdvertisedRoutes, ExplainAdvertisedRoute, ExplainBestPath, LookupBestPath, ListFlowSpecRoutes, ListEvpnRoutes, ListReceivedEvpnRoutes, ListAdvertisedEvpnRoutes, ExplainEvpnRoute, ListBgpLsRoutes, ListTopologyNodes, ListTopologyLinks, ListOrrStatus, ListVpnRoutes, ListRtcRoutes, ListLabeledRoutes, ListBlackholeDiscards, ListFibRoutes, ListFibTables, SetFibTable, DeleteFibTable, ListRouteEvents Query-only RIB route surfaces (incl. EVPN, BGP-LS, VPNv4/v6, RT-Constrain, and labeled-unicast), the RFC 9107 ORR / BGP-LS topology read surface (ListTopologyNodes / ListTopologyLinks / ListOrrStatus), BLACKHOLE discard status, paginated FIB status, runtime FIB-table CRUD, exact explain plus outside-v1 global LPM, and recent route-event history; live route streaming is owned by EventService.WatchEvents / SubscribeFromEvent
BfdService GetBfdSessions BFD session inspection (single-hop and multihop) for configured static neighbors
RpkiService ValidateRouteOrigin, ListCaches, LookupAspa, VerifyAsPath Bounded point validation and configured RTR-cache accepted-epoch inventory, plus the outside-v1 ASPA diagnostics: a bounded merged-provider lookup for one customer ASN and a literal AS_PATH verification with an explicit local role and neighbor ASN
EventService WatchEvents, SubscribeFromEvent, ListEvpnEvents, ListSessionEvents, ListPolicyEvents Unified live stream for route, session lifecycle, BGP NOTIFICATION metadata, policy mutation, EVPN route events, BFD session events, and FIB / BLACKHOLE dataplane status-row summary events, with stream_lagged warnings for bounded-source backpressure; durable cursor replay via SubscribeFromEvent when [event_history].enabled = true; plus bounded after-the-fact EVPN, session-lifecycle, and policy-mutation history. Per-MAC EVPN dataplane categories remain follow-up work
InjectionService AddPath, DeletePath, AddFlowSpec, DeleteFlowSpec, AddEvpnRoute, DeleteEvpnRoute Programmatic route, FlowSpec, and EVPN injection
ControlService CheckLiveness, GetHealth, GetMetrics, Shutdown, TriggerMrtDump Health, metrics, lifecycle, MRT dumps
EvpnService GetEvpnRuntime, ListEvpnInstances, ListEvpnNexthops, ListEthernetSegments, ListIpVrfs, ListManagedNetdevs, GetIpVrf, ListDuplicateMacQuarantines, ClearDuplicateMacQuarantine, SetEthernetSegmentDrain, ApplyEvpnRuntime Local EVPN VTEP instance state, ADR-0059 FDB-nexthop ownership, ADR-0083/0085 Ethernet Segment multi-homing diagnose state, symmetric IRB (Type-5 / L3VNI) IP-VRF readiness / route counters, ADR-0091 managed-netdev lifecycle/status, duplicate-MAC quarantine status and clear, ADR-0084 Ethernet Segment drain for access-circuit maintenance, and ADR-0063 runtime model status / apply
gnmi.gNMI Capabilities, Get, Set, Subscribe OpenConfig BGP telemetry subset (Get / Subscribe) plus a transaction-backed Set subset (static numbered-neighbor create/update/delete, peer-group config, and dynamic-neighbor-prefix entries, with commit-confirmed via ADR-0076; unsupported paths UNIMPLEMENTED; see gNMI); served on UDS and mTLS TCP listeners

Authentication and TLS

The daemon supports three deployment patterns for the gRPC surface:

Pattern Config Auth
Unix domain socket [global.telemetry.grpc_uds] with path + mode File-system permissions on the socket path
Plaintext TCP + bearer token [global.telemetry.grpc_tcp] with address and optional token_file Bearer token in the authorization: bearer <value> metadata header (when token_file is set)
mTLS TCP [global.telemetry.grpc_tcp] with tls_cert_file + tls_key_file + tls_client_ca_file Client certificate signed by the configured CA

The mTLS path is the recommended default for any non-loopback gRPC listener. All three TLS fields are required together; partial configuration is rejected at Config::load. There is no "TLS-without-mTLS" half-mode by design — when TLS is enabled the daemon presents the server certificate, requires every client to present a certificate signed by tls_client_ca_file, and rejects unverified clients at the TLS layer before any gRPC handler runs.

Config loading checks file readability and PEM framing. Both --check and --check --strict also run startup credential staging: they parse the server certificate, private key, and client CA bundle and check the cert/key match without binding listeners. After staging replacement files at the configured paths, run rustbgpd --check --strict /etc/rustbgpd/config.toml before SIGHUP or restart. This checks the files at that moment; it does not prove client trust or hostname matching, or cover subsequent file changes. A positive tls_expiry_warning_seconds adds expiry warnings from that staged generation; the default 0 disables those warnings. See native gRPC certificate expiry for metrics, successful-client metadata logs, and bundle-date limits.

Native mTLS accepts TLS 1.2 and TLS 1.3 using the rustls/ring default cipher suites; protocol versions and cipher suites have no configuration overrides. A client whose certificate is trusted can complete TLS without a mapped principal, but its RPCs receive PERMISSION_DENIED from role authorization.

Valid credential bytes behind unchanged TLS paths rotate on SIGHUP. Adding or removing TLS, changing a configured TLS path, or changing TLS/auth mode remains restart-required; runtime config pins that drift to the live values until the daemon is restarted.

Native gNMI is available on the local UDS listener and on TCP listeners only when mTLS is configured. Plaintext or bearer-token-only TCP listeners serve the native rustbgpd.v1 API but do not register gnmi.gNMI. See GNMI.md for the operator-facing OpenConfig path list and gnmic examples.

# mTLS client example with grpcurl
grpcurl \
  -cacert /etc/rustbgpd/server-ca.pem \
  -cert /etc/operator/client.pem -key /etc/operator/client.key \
  -import-path . -proto proto/rustbgpd.proto \
  rustbgpd.example.net:50051 \
  rustbgpd.v1.GlobalService/GetGlobal
# mTLS gNMI client example with gnmic
gnmic \
  --address rustbgpd.example.net:50051 \
  --tls-ca /etc/rustbgpd/server-ca.pem \
  --tls-cert /etc/operator/client.pem \
  --tls-key /etc/operator/client.key \
  --tls-server-name rustbgpd.example.net \
  capabilities

Per-listener access_mode = "read_only" rejects mutating RPCs (neighbor add/delete, route injection, policy changes, peer-group changes, shutdown, MRT trigger) with PERMISSION_DENIED. Use this on a dedicated monitoring listener that exposes the read surface without the mutating control plane.

Each configured listener can independently set access_mode = "read_write" or "read_only". Read-only listeners allow query and watch RPCs but reject all mutating RPCs with PERMISSION_DENIED.

Listener Access Matrix

read_only is a listener-level authorization boundary. It does not create per-user roles: every client accepted by that listener gets the same read-only surface. Use a separate read_write listener for automation that needs to mutate daemon state.

docs/reference/grpc-method-inventory.md, docs/reference/grpc-method-inventory.json, and crates/api/src/authz.rs classify every RPC into read, sensitive_read, mutating, or operator_only for ADR-0064. The JSON file is the machine-readable export for auditors and generated clients; the Rust authz tests verify it against the source-of-truth matrix. The runtime records tier decisions for every RPC via structured grpc_authz logs and bgp_grpc_authz_decisions_total{tier,result,authn,access_mode}. Listener max_tier caps are always enforced. The same runtime layer also enforces the authenticated principal's configured role ceiling before the handler runs — unconditionally: "tier" has been the default since v0.24.0 and is the only mode since v0.63.0. The former "legacy" migration mode (roles recorded as audit context but never enforced) was removed; a config that still sets it is rejected at boot, --check, and reload with a migration message. Forwarded calls emit result-aware labels such as result="handler_ok" or result="handler_invalid_argument" after the handler returns. Rejected calls use bounded pre-handler labels: result="listener_tier_denied" means the method was rejected before the handler ran, and result="authn_failed" means an over-cap bearer-token request failed authentication before tier details were disclosed. Tier-mode denials use result="principal_unmapped" when the authenticated principal has no role entry and result="role_tier_denied" when the principal's role is below the method tier. Credential-bearing request summaries are masked before entering grpc_authz logs; DiffRuntimeConfigRequest.candidate_toml, PlanConfigTransactionRequest.candidate_toml, and ApplyConfigTransactionRequest.candidate_toml are always summarized as redacted metadata. StreamPlanConfigTransaction logs only framing version, bounded counts, token presence, and outcome; StreamApplyConfigTransaction adds only expected-token and confirm-handle presence — never candidate bytes, digest, plan token, path, or spool name. Transaction apply comments are not logged verbatim, and SetPeerGroup logs MD5 state without the MD5 value. Operators declare [security.grpc.roles] and set explicit listener principal labels for bearer-token TCP and explicitly named UDS listeners; those labels are the principal strings looked up in [security.grpc.roles]. An owner-only UDS without a principal retains its implicit local-operator role even when a bearer token is required. Token-protected UDS listeners use authn="bearer_token"; permissions-only UDS listeners use uds for an explicit principal or uds_owner for the implicit identity. Their parent directories must meet the UDS path requirements. Native mTLS listeners derive the audit principal from the client certificate using ADR-0064 precedence: rustbgpd: URI SAN, then email SAN, then Subject CN. Extracted principal values must fit the bounded audit label form and must not contain embedded control characters; unsupported values fall back to mtls-unresolved, which cannot be mapped in [security.grpc.roles] and is therefore denied.

Tier enforcement is the default since v0.24.0 and mandatory since v0.63.0. When upgrading from an older release, stage [security.grpc.roles] plus explicit listener principals and validate the candidate config with rustbgpd --check. The implicit default UDS listener needs no staging: it is owner-only, so its clients are authorized as the implicit local-operator principal at operator tier without a roles entry — a config whose only management surface is a local socket needs no [security.grpc] block at all. Only group/world-accessible UDS sockets require an explicit principal mapped in [security.grpc.roles]. enforcement = "legacy" was removed in v0.63.0 and from the typed schema in v0.65; the exact retired value still receives a migration diagnostic. Earlier editions of this document projected a two-minor/90-day compatibility floor (approximately 2026-10-09) as the earliest eligibility for removal; that guidance is superseded. The removal landed earlier as an explicit owner decision under the project's pre-1.0 alpha stability posture (see README/ROADMAP on version stability), taken once the implicit local-operator identity removed the migration burden for local-only deployments: for them the migration is deleting the [security.grpc] block. A stuck operator keeps a named-principal setup working with three lines — enforcement = "tier" plus a [security.grpc.roles] entry mapping the listener's principal; the rejection message carries the exact paste block. docs/reference/security.md covers deployment hardening for this surface, and ADR-0064 records the tier model itself and which of its slices remain open. Operational collection, retention, query examples, and resource-abuse guardrails for grpc_authz logs and the related Prometheus metrics live in docs/reference/operations.md.

Service Read-only RPCs Mutating RPCs rejected on read_only
GlobalService GetGlobal
ConfigService DiffRuntimeConfig, PlanConfigTransaction, StreamPlanConfigTransaction, GetConfigTransactionStatus, GetEffectiveConfig, ListConfigHistory StreamApplyConfigTransaction, ApplyConfigTransaction (pure [[fib_tables]], pure [[dynamic_neighbors]], static [[neighbors]] add/delete/modify, catalog-only policy/neighbor-set/peer-group/global-chain changes, pure live policy-chain impact for static neighbors and accepted dynamic peers, or peer-group/session reshape impact for static members and live dynamic sessions; mixed or unsupported candidates rejected without mutation), ConfirmConfigTransaction, AbortConfigTransaction, RollbackConfigTransaction
NeighborService ListNeighbors, GetNeighborState, ListDynamicNeighbors AddNeighbor, DeleteNeighbor, EnableNeighbor, DisableNeighbor, SoftResetIn, RefreshOutbound, ReplayOutbound, ResetNeighbor, AddDynamicNeighbor, DeleteDynamicNeighbor, SetGracefulShutdown
PolicyService ListPolicies, GetPolicy, ListNeighborSets, GetNeighborSet, GetGlobalPolicyChains, GetNeighborPolicyChains, ExplainImportPolicy, ListRejectedRoutes, TestPolicy, GetPolicyStats, GetValidationPolicyPosture SetPolicy, DeletePolicy, SetNeighborSet, DeleteNeighborSet, SetGlobalImportChain, SetGlobalExportChain, ClearGlobalImportChain, ClearGlobalExportChain, SetNeighborImportChain, SetNeighborExportChain, ClearNeighborImportChain, ClearNeighborExportChain
PeerGroupService ListPeerGroups, GetPeerGroup SetPeerGroup, DeletePeerGroup, SetNeighborPeerGroup, ClearNeighborPeerGroup
RibService All read/list/explain RPCs (incl. ListFibTables) SetFibTable, DeleteFibTable
EventService All RPCs None
EvpnService GetEvpnRuntime, ListEvpnInstances, ListEvpnNexthops, ListEthernetSegments, ListIpVrfs, ListManagedNetdevs, GetIpVrf, ListDuplicateMacQuarantines ClearDuplicateMacQuarantine, SetEthernetSegmentDrain, ApplyEvpnRuntime
BfdService GetBfdSessions None
RpkiService ValidateRouteOrigin, ListCaches, LookupAspa, VerifyAsPath None
gnmi.gNMI Capabilities, Get, Subscribe Set (operator-only; transaction-backed OpenConfig subset — static numbered-neighbor neighbor-address/peer-as/description/peer-group create/update/delete, peer-group catalog entries, dynamic-neighbor prefixes, and the commit-confirmed extension via ADR-0076; unsupported paths return UNIMPLEMENTED)
InjectionService None AddPath, DeletePath, AddFlowSpec, DeleteFlowSpec, AddEvpnRoute, DeleteEvpnRoute
ControlService CheckLiveness, GetHealth, GetMetrics Shutdown, TriggerMrtDump

Error Taxonomy

The API uses gRPC status codes consistently across services:

Code Meaning
UNAUTHENTICATED Listener authentication failed: missing bearer token, non-ASCII authorization metadata, or a token mismatch
PERMISSION_DENIED The request reached a read-only listener but called a mutating RPC, the RPC method tier exceeds the listener max_tier ceiling, the principal is unmapped in tier mode, or the principal's role is below the method tier
INVALID_ARGUMENT Client-supplied request data is malformed, missing, out of range, uses an unsupported enum value, or combines incompatible filters
NOT_FOUND A named or targeted resource does not exist: peer, policy, neighbor set, peer group, IP-VRF, route-event target, or injected route
ALREADY_EXISTS A create request targets an existing resource, such as duplicate neighbor creation or an exact-duplicate dynamic-neighbor range
FAILED_PRECONDITION The request is valid but the daemon is not in a state where it can complete it, such as MRT export being disabled or a policy object still being referenced
DEADLINE_EXCEEDED A bounded actor or session read did not complete before its operation deadline; this does not mean the targeted runtime state is absent
UNAVAILABLE A required actor or session task exited, or a required actor, command channel, or persistence queue is unavailable, closed, or back-pressured
UNIMPLEMENTED The RPC is reserved in the protobuf but runtime support has not shipped yet, the connected daemon predates the RPC, or a gNMI path or extension is outside the supported subset
INTERNAL An internal daemon actor, metrics encoder, or RIB boundary failed unexpectedly after the request passed validation

Runtime-config mutations that fail after transient runtime changes, but whose forward effects are then fully compensated, preserve the original gRPC status code and append the original message after this exact prefix: runtime effects were fully compensated; retry may repeat transient runtime changes:. When an original message is present, exactly one ASCII space separates it from that prefix; an empty original message adds no trailing space. Their trailing metadata also includes rustbgpd-runtime-config-outcome: fully-compensated. The staged persistent candidate is discarded, so the runtime and TOML remain at their prior state. The marker is a retry warning: repeating the request can repeat those transient runtime changes even though the reported attempt left no final-state change.

Operator-read deadlines

Read budgets depend on the operation; there is no server-wide read timeout.

Read path Existing backend wait budget
Peer-manager operator reads 2 s per request to the peer manager, including channel admission and reply
ListNeighbors / GetNeighborState RIB summaries A separate 2 s for the RIB summary admission and reply, after the peer-manager read; this is not one 2 s end-to-end RPC budget
GetPolicyStats One absolute 2 s deadline shared by peer validation, export, import and dataset backend waits; admission consumes that budget and no stage resets it
GetHealth One 200 ms internal core-probe budget shared by peer-manager and RIB snapshot queries; failures return INTERNAL
General RIB listing and RIB explain reads No fixed server-side timeout in the shared RIB read helper; callers set their own deadlines and cancel abandoned reads

General RIB reads can wait behind policy-transition consistency fences. A raw gRPC caller with no deadline can remain waiting for that fence to clear. The CLI bounds ordinary unary reads, including these RIB reads, at 30 s; methods with separate documented budgets retain those allowances. Custom clients should supply their own deadline. Cancellation releases the waiting reply and is checked by actor work; it does not preempt a synchronous work unit already executing. Route work retains priority. These backend budgets do not promise end-to-end response latency, including synchronous response construction, transport and client scheduling. The separate measured end-to-end soak acceptance gates remain unchanged.


gnmi.gNMI

OpenConfig BGP telemetry surface (ADR-0070). The supported subset is deliberately narrow: Capabilities, Get, and Subscribe (ONCE, POLL, STREAM SAMPLE, and STREAM ON_CHANGE — the last is scoped to the neighbor session-state leaf, requires [event_history] enabled, and returns FAILED_PRECONDITION otherwise) for global and neighbor state under the default network instance. Set is operator-only and supports the first durable config subset: static, numbered BGP neighbor create/update/delete for neighbor-address, peer-as, description, and peer-group, peer-group catalog entries, and dynamic-neighbor prefixes, plus graceful-restart config leaves. Supported Set edits are translated into full candidate TOML and fed through PlanConfigTransaction / ApplyConfigTransaction; the standard gNMI commit-confirmed extension maps to the same confirm / abort lifecycle as native config transactions. Unsupported paths return UNIMPLEMENTED instead of bypassing the transaction model. See GNMI.md for the full ON_CHANGE v1 scope (initial sync, reconnect-no-replay, lag → DATA_LOSS) and Set path matrix.

Network gNMI is served only on mTLS TCP listeners. The UDS listener also exposes the service as a local-only extension.

For the full supported path list, setup guidance, troubleshooting table, and tested gnmic commands, see GNMI.md.

gnmic \
  --address rustbgpd.example.net:50051 \
  --tls-ca /etc/rustbgpd/server-ca.pem \
  --tls-cert /etc/operator/client.pem \
  --tls-key /etc/operator/client.key \
  --tls-server-name rustbgpd.example.net \
  get \
  --encoding json_ietf \
  --type STATE \
  --path '/network-instances/network-instance[name=DEFAULT]/protocols/protocol[identifier=BGP][name=BGP]/bgp/global/state'

GlobalService

Daemon identity and configuration.

RPC Description
GetGlobal Returns ASN, router ID, listen port, host TCP-AO capability probe status and detail, and the Unix-epoch-seconds timestamp of the last successfully accepted full policy generation
# Get daemon identity
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.GlobalService/GetGlobal

policy_generation_loaded_timestamp_seconds is the Unix epoch second at which the daemon last accepted a complete policy generation — initial load, SIGHUP, or a config transaction. It is 0 until the first generation is accepted, and a rejected load never advances it (the prior generation stays live and the timestamp freezes), so a caller can prove a reload was accepted rather than merely attempted. It is the same value the bgp_policy_generation_loaded_timestamp_seconds Prometheus gauge exports; see "Policy artifact freshness" in OPERATIONS.md for the staleness alert expressions.

tcp_ao_support is a read-only Linux capability probe for RFC 5925 TCP-AO. It reports whether the host kernel accepts the TCP-AO socket primitive that rustbgpd uses for static-neighbor and dynamic-prefix startup key installation. If a tcp_ao key is configured on a host where the primitive fails, listener setup aborts startup, and active-open setup rejects that connect attempt without falling back to unauthenticated TCP. Dynamic-range keys are config-file-only: runtime range CRUD rejects protected ranges and overlaps. SIGHUP can append a non-preferred successor generation; selection, deprecation, deletion, and protected-owner CRUD are not exposed. tcp_ao_detail carries human-readable probe detail alongside it, populated only when tcp_ao_support is TCP_AO_SUPPORT_UNSUPPORTED or TCP_AO_SUPPORT_PROBE_FAILED, and empty otherwise.

NeighborState.authentication reports the effective protected transport as PLAINTEXT, MD5, or TCP_AO. For direct dynamic-prefix TCP-AO sessions, that identity comes from the validated accepted socket rather than a synthesized per-neighbor key configuration. When socket inspection succeeds for a connected TCP-AO session, NeighborState.tcp_ao contains current/RNext KeyIDs, Linux verification/error counters, and an ordered keys inventory. Each key row is deliberately redacted: it contains only peer/prefix, directional IDs, algorithm, current/RNext and local rollover flags, optional Linux VRF L3-master ifindex, and per-key counters. The VRF ifindex is not an IPv6 link-local scope ID; absence means the MKT is VRF-unbound, not default-VRF bound. The inventory never contains key material, key length, a hash, or a fingerprint. The daemon refreshes TCP_AO_INFO and TCP_AO_GET_KEYS from the live socket for each neighbor state query, publishes them only as one internally consistent result, and clears it on disconnect or inspection failure; it never serves an older healthy snapshot as a fallback. The counters are cumulative for the lifetime of that TCP socket, so any non-zero error counter keeps the socket DEGRADED until reconnect. NeighborState.tcp_ao_health is NOT_APPLICABLE for plaintext and MD5 peers, UNAVAILABLE when TCP-AO protects the session but there is no socket snapshot (including disconnect, inspection failure, or persistent INFO/inventory inconsistency), HEALTHY when the published snapshot has both current/RNext key-validity flags mapped to a nonempty, internally consistent live inventory, neither active key is deprecated, and no error counters, and DEGRADED when either key-validity flag is absent, an active key is deprecated, or any bad, key-not-found, unsigned-required, or dropped-ICMP counter is non-zero. An inconsistent INFO/inventory pair is never published as degraded state.

For TCP-AO peers, NeighborState.tcp_ao_desired_generation, tcp_ao_applied_generation, tcp_ao_rotation_phase, and tcp_ao_rotation_error expose the secret-free ordered-rotation state. A phase of add_only_failed retains the prior selectable keys and is retryable with another SIGHUP. During add_only, protected accepts may carry either the globally applied generation or the exactly proved desired generation. During add_only_failed, only the applied generation may enter the peer manager; fully installed but uncommitted children remain fail-closed. A partial listener mutation may reject affected passive accepts until retry or restart. During selecting and awaiting_peer, the applied or exactly adjacent desired generation may enter; selection_failed admits only the applied generation. awaiting_peer is a one-shot result, not actor polling: the next identical SIGHUP re-observes the same generation. Selection sets only local RNext, never Linux Current, and predecessor deprecation metadata commits only after the whole affected session cohort has observed verified successor traffic beyond its per-socket pre-selection baseline. During deleting, the applied or exactly adjacent desired generation may enter; a desired-generation accept is projected from the coordinator's retained immutable survivor inventory. delete_failed admits only the applied generation. Deletion removes only deprecated MKTs that are neither Current nor RNext; an ambiguous partial session mutation discards the complete changed cohort before the failure is exposed.

NeighborState.effective_distribution_mode reports the live RIB selection surface. When multiple mechanisms apply, its primary-label precedence is ADD_PATH > PER_CLIENT_BEST > ORR > SINGLE_BEST. It is UNKNOWN when the peer has no active outbound registration; UNSPECIFIED remains the backward-compatible value returned by older servers. This field is independent of the diagnostic update_group label. A peer eligible for both per-client-best and ORR reports PER_CLIENT_BEST because it is the primary label; that value does not mean its configured ORR vantage is inactive.

NeighborState.selection_deferral is empty on a cold start. During a planned, marker-backed RFC 4724 restart it reports one row per frozen address-family gate: whether the gate is active, this peer's waiter state and stamped session, the process-wide blocking-waiter count, and remaining time. Released rows are retained for the daemon lifetime with reason all_eor, all_excluded, or timer, so an operator can distinguish complete convergence from timer-driven release.

NeighborState.paths_limits is sorted by numeric AFI then SAFI. Optional effective_send_limit is presence-aware: presence means active, with zero unlimited and non-zero finite; absence means inactive. The superseded raw sentinel field number and name (effective_send_max, field 5) are reserved.

NeighborState.slow_peer is true while the peer is flagged slow: the session is Established and alive but its outbound queue has stayed above the configured backlog threshold for the configured duration (slow_peer_threshold_pct / slow_peer_duration in the neighbor config). It clears when the queue drains and on session teardown, and mirrors the bgp_peer_slow{peer} gauge. See the "Slow peers" section in OPERATIONS.md for interpretation.

NeighborState.rejected_routes_retained is the actor-authoritative size of that exact peer session's bounded retained-reject store. Presence distinguishes a current fresh snapshot, including an explicit zero, from an older daemon or a stale peer-session query; clients must not turn absence into zero. Inventory consumers can therefore obtain every peer's count in one ListNeighbors request without scanning routes or issuing ListRejectedRoutes per peer.

NeighborState.reconnect_in_seconds is the whole-second wait, rounded up, until this session's next automatic reconnect attempt while it sits in Idle after an unplanned teardown; 0 means no reconnect is pending. Each response recomputes it from the live timer, so it is a sample rather than a countdown a client can decrement. Consecutive NOTIFICATION-driven returns to Idle lengthen the wait from the peer's connect-retry base up to a fixed ceiling, so a rising value distinguishes a peer that keeps failing the OPEN exchange from one waiting out an ordinary retry. Presence is explicit: a current daemon always sends the field, including the zero, so absence means only that the daemon predates it and must never be read as "no reconnect pending". rbgp neighbor omits the field rather than rendering the zero.

NeighborState.negotiation_available and negotiated_session expose the actor-authoritative capability outcome of the current Established session: hold time, cached local IP address, negotiated keepalive cadence, remote router ID, four-octet AS, negotiated families, graceful-restart coverage, the peer's Route Refresh / Enhanced Route Refresh / Extended Message capabilities, and the resulting outbound message-size ceiling. Presence of negotiation_available distinguishes an older daemon that does not expose negotiation state from a current daemon reporting no Established session; negotiated_session is absent while the session is down, during OpenConfirm, or when the actor query is stale. The scalars inside it are optional so genuinely negotiated false/zero values stay distinct from fields an older daemon never sent during a rolling upgrade. local_address is copied from the Established session's cached export profile; it is never substituted with the remote peer address or queried from the socket during an API request. keepalive_interval_seconds is the negotiated send cadence and remains present as zero when negotiated hold time is zero. Both fields and the enclosing negotiated state are absent for down or stale responses; older daemons may send the enclosing state while omitting these two additive fields. Neither field is a live countdown.

NeighborState.rfc8212_import_policy and rfc8212_export_policy report the RFC 8212 explicit-policy status of each direction (ADR-0112); a one-sided configuration is never collapsed into a single "policy present" answer. PRESENT means enforcement applies and explicit operator policy is installed. MISSING means enforcement applies and the reserved internal deny is installed on that direction of this peer — no route crosses it, in any negotiated family. NOT_REQUIRED covers process-wide disabled enforcement and iBGP sessions. Clients must render UNSPECIFIED (a daemon that predates the field) and any unrecognized future value as "unknown", never as NOT_REQUIRED.

NeighborState.inbound_prefix_limits reports ADR-0108 inbound max-prefix capacity, one InboundPrefixLimitState per finite configured bound in enforcement order: the scope name (aggregate, ipv4_unicast, ipv6_unicast, ipv4_unicast_received, or ipv6_unicast_received), the session actor's usage for that scope, the configured limit, the saturating headroom, a blocking flag for an open max_prefix_action = "block" episode, and the stable reason inbound_prefix_limit_reached while blocking. Unlike the outbound sibling, a scope with no configured bound has no row at all, so the list is empty for a peer with no finite inbound bound — and equally empty from a daemon that does not expose the field and from a stale peer-session snapshot. Absence is therefore never proof that no bound is configured. Counts come from the session actor's O(1) enforcement accounting, never from the RIB, whose rows carry different Add-Path and lifecycle semantics.

NeighborState.max_prefix_action is the effective current disposition of a crossed inbound bound, not a straight echo of the max_prefix_action configuration key. block and warning mirror the configured value. Configured shutdown reports restart whenever a timed max_prefix_restart_seconds is configured, and while a peer latched by a max-prefix violation still has a hold-down deadline pending; it reports shutdown otherwise, including once a latch has become indefinite. The companion max_prefix_restart_remaining_millis is present only while a hold-down is counting down. The field is a plain string: an empty value is a daemon that predates it, and clients must render an unrecognized value as unknown rather than mapping it back onto a configuration key.

NeighborState.outbound_prefix_limits reports ADR-0113 outbound unicast capacity, one OutboundPrefixLimitState per family: the family name (ipv4_unicast / ipv6_unicast), usage — distinct prefixes currently admitted into this peer's advertised state, post-policy, post-OTC, and post-exact-export, agreeing with advertised-route queries and never the shared update-group table's count — the optional configured limit (absence means unlimited, never zero), the saturating headroom (absent while the family is unlimited), a blocking flag for an open blocking episode, and the stable reason outbound_prefix_limit_reached while blocking. The list is empty from a daemon that does not expose it and from a peer with no outbound registration; a registered peer always reports both unicast families.

NeighborState.effective_posture is a read-only snapshot of the resolved running neighbor posture: NEXT_HOP ownership enforcement, RFC 1997 interpretation, route-server control-community handling, and the optional ORR vantage. The containing message is always present from a current daemon, so explicit false remains distinguishable from an older daemon that omitted the field. Clients must render an absent message, UNSPECIFIED, and unrecognized future ownership modes as unknown rather than disabled.


ConfigService

Live runtime config diagnostics and transaction planning. Diff / plan callers submit candidate TOML and receive only redacted diff / plan output; GetEffectiveConfig is the one deliberate full-document export — it returns the effective running config as normalized TOML with defaults resolved (selected default-empty policy lists may be omitted) and secret material replaced with <redacted> before it leaves the daemon (rbgp config effective). The two shipped full-document consumers (rbgp config effective and rbgp doctor) accept at most 384 MiB of TOML plus its protobuf envelope; other config RPC clients retain tonic's 4 MiB decode default.

RPC Description
DiffRuntimeConfig Validate candidate TOML and compare it against the daemon's live runtime config snapshot
PlanConfigTransaction Validate candidate TOML, return a runtime snapshot token, and classify v1 transaction support without mutating daemon state
StreamPlanConfigTransaction Outside-v1 authenticated client stream for plan-only candidates larger than the unary ceiling; returns the unchanged plan plus an optional ephemeral plan token
StreamApplyConfigTransaction Outside-v1 authenticated operator stream that consumes an exact streamed-plan binding and invokes the existing transaction executor
ApplyConfigTransaction Operator-tier commit entry point for ADR-0076 config transactions; currently commits one pure runtime family at a time: full-set [[fib_tables]], full-set [[dynamic_neighbors]], static [[neighbors]] add/delete/modify changes, catalog-only policy/neighbor-set/peer-group/global-chain changes, pure live policy-chain impact for static neighbors and accepted dynamic peers, or peer-group/session reshape impact for static members with post-persist best-effort reset of live dynamic sessions
ConfirmConfigTransaction Confirm a pending confirmed transaction before its timer expires
AbortConfigTransaction Abort a pending confirmed transaction and roll back immediately
GetConfigTransactionStatus Return redacted confirmed-transaction lifecycle state
GetEffectiveConfig Return the effective running config as normalized TOML — defaults resolved, selected default-empty policy lists omitted, secrets redacted (rbgp config effective)
ListConfigHistory List up to twenty mixed v2/v3 config history rows — newest row at index 0, then older entries; per-entry timestamp, normalized-TOML SHA-256, config-source SHA-256 over that TOML digest plus the canonical accepted rpol/dataset source roster, provenance status, and one-line summary; never config documents (rbgp config history). Payload-bearing v2 rows are RECORDED; accepted normalized TOML above 10 MiB produces a rollback-ineligible METADATA_ONLY v3 row with normalized_toml_bytes and metadata_only_reason. Corrupt or duplicate-sequence rows are UNREADABLE with both digests empty. Retired TOML files are ignored and retained.
RollbackConfigTransaction Restore a provenance-verified v2 row through the same transaction executor as apply — same plan/impact classification and receipts (rbgp config rollback N). Metadata-only rows return FAILED_PRECONDITION before payload/source access, planning, confirm authority, or mutation; index zero remains INVALID_ARGUMENT. Unreadable rows and provenance mismatches fail closed before planning or mutation.

History recording is best effort after an accepted durable config. Exactly 10 MiB of normalized TOML remains v2; larger accepted snapshots retain only a canonical metadata envelope of at most 64 KiB, including a redacted summary of at most 4 KiB. Neither normalized TOML nor the external-source roster is retained in v3. Both formats share one newest-first, twenty-row sequence. Listing rejects a twenty-first recognized final before decoding any row (INTERNAL, storage unavailable or unsafe); rollback maps the same unsafe roster to FAILED_PRECONDITION. Hashes identify accepted history; they cannot recover omitted bytes or authorize use of commit-confirm cleanup residue.

For a supported transaction with an independent mutation, the plan carries one canonical committed candidate through token, runtime stage, persistence, and accepted history. It may materialize omitted RFC 8212 presence (epoch-1 omission as epoch 1 plus explicit false; epoch-2 omission as epoch 2 plus explicit true) without changing effective policy. Representation-only or posture-changing candidates are rejected before mutation. FIB transactions stage the full tables-plus-posture snapshot; determinate failures attempt to restore both. Lost persistence acknowledgement remains ambiguous after attempted restore; rollback or post-durable finalization failure is also ambiguous.

Index 0 is not necessarily the running or currently persisted config. V2 rows hash the accepted external-source identity, but do not archive the .rpol or dataset bytes. Rollback performs one detached load and requires the normalized TOML, complete manifest, and source digest to match exactly.

DiffRuntimeConfigResponse contains boolean summary fields, a plain-text human_text rendering, and diff_json using the rustbgpd --diff --json schema. Secret-bearing fields such as neighbor md5_password and tcp_ao.key material are redacted in renderings. The corresponding grpc_authz request summaries never log candidate_toml content; they record only redacted metadata such as request body size and token presence.

rbgp config plan and apply stream candidates by default. Plan prints its ephemeral plan token only when present and emits a JSON string or null; Apply accepts --plan-token, or obtains one by streaming a fresh plan for the same candidate and expected runtime snapshot. Plan and automatically planned Apply receive one unary compatibility attempt only when the streaming call returns the generated missing-method fingerprint (UNIMPLEMENTED with empty message and details). Explicit --plan-token Apply fails closed instead, because the unary RPC cannot preserve the reviewed candidate binding. Before that fallback, a fully encoded request above the 4,194,304-byte unary limit fails locally without a unary RPC or secret-bearing diagnostic. The proto and unary server behavior are unchanged. Both streaming methods use metadata version 1, which must appear first and once, followed by zero or more non-empty chunks no larger than 1 MiB, then one end frame carrying the exact aggregate length and raw 32-byte SHA-256, immediately followed by EOF. Aggregate input is capped at 384 MiB, idle input at 60 seconds, and the whole admission through planner reply at 30 minutes. One stream is active process-wide across all listeners. When it is busy, exactly one operator Plan or Apply may wait without preemption under that request's original 30-minute deadline. Observer/automation Plan, a second operator waiter, and lower-role calls while the waiter is registered return RESOURCE_EXHAUSTED; this is one bounded priority waiter, not a general queue. The listener must authenticate clients (mTLS, bearer token, or the owner-only UDS identity). The owner-only descriptor-relative spool file is unlinked immediately after open, before any candidate bytes are accepted, so candidate bytes never survive at a pathname. A crash between exclusive creation and unlink can leave an empty owner-only stub; unlink failure rejects the request before ingress. A COMMITTABLE streamed plan additionally carries plan_token, a UUIDv4 bound in memory to the runtime token, candidate digest, and length for 30 minutes. At most 256 bindings are live. Lower-role issuance evicts the oldest lower-role binding and fails with RESOURCE_EXHAUSTED if every binding is operator-issued; operator issuance evicts the oldest lower-role binding first, then the oldest operator binding. Consumption is role-agnostic, so an operator may apply an exact binding issued by observer/automation Plan. NOOP, REJECTED, and failed plans never issue one. Streamed Apply validates all framing and Apply metadata before atomically consuming the exact token/runtime/digest/length binding. A mismatch preserves the token; an expired, evicted, or successfully consumed token requires a new plan. After consume, client cancellation or deadline never cancels the existing state-changing apply future or restores the token.

ConfigTransactionPlanResponse wraps the same redacted diff with:

  • runtime_snapshot_token: an optimistic-concurrency token for the live runtime snapshot used during planning. It is an opaque, process-local change detector, not a cryptographic commitment or authorization credential; re-plan after a daemon restart.
  • supported_sections: sections the v1 transaction model can commit ([[fib_tables]], [[dynamic_neighbors]], static neighbor add/delete/modify, catalog-only [policy] / [peer_groups] changes with no effective impact on static neighbors or dynamic ranges, pure live policy-chain impact for static neighbors or accepted dynamic peers, and peer-group/session reshape impact — static members and live dynamic sessions).
  • unsupported_sections: hot-reloadable sections v1 refuses until an atomic executor exists. Mixed policy/session effective impact and dynamic-range peer-group reassignments continue to report effective neighbor inheritance impact.
  • restart_required_sections: sections that still require daemon restart.

redacted_diff.reload_applied.effective_neighbor_impact[] includes a machine-readable kind: policy_chain for a pure resolved import/export chain move that the live-policy executor can commit, or session_reshape when inherited peer-group/session state changes — committed by the session reshape executor (static members reconfigured in place; live dynamic sessions gracefully reset post-persist).

The planner is intentionally stricter than SIGHUP: "reload-applied" does not mean "transaction-committable" unless the section appears in supported_sections. ApplyConfigTransaction commits one pure runtime family at a time. Cross-family candidates, mixed policy/session effective impacts, and other valid-but-unsupported sections return REJECTED without mutation until their section executor lands.

Native plan/apply/rollback admit a full-snapshot family with external .rpol main/import files or [policy.datasets] snapshots declared only when the planner's fresh capture of those files is byte-identical to the accepted snapshot's recorded identity (ADR-0130) — the bytes are not covered by the transaction token and cannot be rolled back atomically with the TOML, so any drift rejects the transaction without mutation. Deploy changed TOML, .rpol graphs, and dataset snapshots together, then send SIGHUP. gNMI Set never verifies external inputs: a gNMI full-snapshot candidate remains rejected whenever they are present. A true no-op remains NOOP; a pure [[fib_tables]] transaction with unchanged external inputs remains committable because that executor substitutes only the targeted table set rather than adopting the full candidate config. diff_json.reload_applied.datasets_changed reports dataset binding/path edits. diff_json.summary.declared_datasets_count and diff_json.reload_applied.declared_datasets_count report how many datasets the candidate declares; the diff never compares their file contents, so a non-zero count means a reload re-reads files the preview did not evaluate.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d @ localhost:50051 rustbgpd.v1.ConfigService/DiffRuntimeConfig <<'JSON'
{
  "candidate_toml": "[global]\nasn = 65001\nrouter_id = \"10.0.0.1\"\nlisten_port = 179\n\n[global.telemetry]\nlog_format = \"json\"\n"
}
JSON

Plan a transaction without mutation:

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d @ localhost:50051 rustbgpd.v1.ConfigService/PlanConfigTransaction <<'JSON'
{
  "candidate_toml": "[global]\nasn = 65001\nrouter_id = \"10.0.0.1\"\nlisten_port = 179\n\n[[fib_tables]]\nname = \"edge\"\ntable_id = 1000\nmetric = 200\n"
}
JSON

Use the returned runtime_snapshot_token on the apply request. The daemon re-plans under the shared runtime-config coordinator before committing; a stale token fails without mutation. client_request_id and comment are audit metadata only and are not logged verbatim. To use a commit-confirmed workflow, include confirm_id on ApplyConfigTransaction; confirm_timeout_seconds defaults to 600 when omitted and is capped at 86400. While a confirmed transaction is applying or awaiting confirmation, other persisted runtime config mutators fail with FAILED_PRECONDITION. Call ConfirmConfigTransaction with the same confirm_id to make the change permanent, or AbortConfigTransaction to roll back immediately. If the timer expires first, the daemon re-applies the pre-commit runtime snapshot through the same transaction executor and persists the rollback. The confirm window is durable. Before commit, the v3 writer publishes the exact accepted normalized prior to <runtime_state_dir>/commit-confirm-v3-prior.toml, its provenance and file-identity metadata to commit-confirm-v3-metadata.json, then the sole boot authority to <absolute lexical config path>.commit-confirm-locator.json. Startup checks that locator before candidate contents and verifies the complete chain before restoring; unsafe, torn, or mismatched state fails closed. Confirm and successful rollback become terminal only after locator unlink and parent-directory fsync. Subsequent verified exact metadata/raw cleanup and pending-directory fsync are warning-only; locator-free residue cannot re-arm the transaction. Production reads and writes v3 authority only. A v2 locator or locator-free v1/v2 journal makes v0.65.0 and every later release refuse untouched. Recover with rustbgpd v0.64.0, or delete only after proving it terminal and intended. Status reports pending/terminal state. A failed abort/auto-revert rollback is not terminal: the transaction stays pending with an ABORT_FAILED/AUTO_REVERT_FAILED status and the mutation fence stays closed until the abort is retried successfully, the candidate is confirmed, or a restart boot-reverts from the retained journal.

Abort and timer rollback do not depend on the runtime snapshot token. The token is a Plan→Apply change detector for callers: it covers the runtime config and the live update-group membership, so a session going up or down changes it, and a caller's Plan, Apply, or RollbackConfigTransaction holding the older token still fails with FAILED_PRECONDITION and must re-plan. The rollback of a pending confirmed transaction instead restores the prior snapshot the transaction recorded, under the runtime-config coordinator and behind the mutation fence, whatever sessions did during the window. It depends on what any apply depends on: the peer manager and config persistence being available, and the prior snapshot still planning as committable.

V3 commit-confirm caps the current accepted normalized prior at 384 MiB. An oversized prior makes Apply return FAILED_PRECONDITION, including actual and limit byte counts, before authority publication or peer, persistence, and runtime mutation. Ordinary unconfirmed Apply remains available.

GetConfigTransactionStatus (and the apply response) carries a ConfigTransactionConfirmation:

Field Type Meaning
status ConfigTransactionConfirmationStatus Lifecycle state (see below).
confirm_id string The operator-supplied handle for the transaction.
timeout_seconds uint32 Effective confirm timeout applied (echoed).
deadline_unix_seconds uint64 Absolute live auto-revert deadline, minted after Apply commits and retained in the last terminal lifecycle record. Pre-commit durable authority carries a separate informational deadline derived at publication because boot recovery is unconditional.
committed_sections repeated string Sections the confirmed apply committed.
runtime_snapshot_token string Post-commit token for the pending change.
human_text string Redacted human-readable summary.

ConfigTransactionConfirmationStatus values:

Value Meaning
..._UNSPECIFIED Default zero value; not emitted in normal responses.
..._NONE No confirmed transaction is currently tracked.
..._PENDING Applied and awaiting confirmation before the timer expires. Also reported while an expired timer's rollback is still waiting for the runtime-config coordinator; human_text then names the missed deadline and the wait.
..._CONFIRMED Made permanent by ConfirmConfigTransaction.
..._ABORTED Rolled back by AbortConfigTransaction.
..._AUTO_REVERTED Timer expired; the pre-commit snapshot was re-applied.
..._AUTO_REVERT_FAILED Timer expired but the rollback re-apply failed; manual correction required.
..._ABORT_FAILED Abort requested but the rollback re-apply failed; manual correction required.

(Each value is prefixed CONFIG_TRANSACTION_CONFIRMATION_STATUS_ in the proto.) Confirmed lifecycle transitions are also exposed through the bounded Prometheus counter bgp_config_transaction_lifecycle_total{operation, outcome}: operation is confirm, abort, or auto_revert; outcome is success or failure. The metric deliberately does not include confirm_id, candidate TOML, peer labels, or error strings.

Apply a pure full-set [[fib_tables]] transaction:

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d @ localhost:50051 rustbgpd.v1.ConfigService/ApplyConfigTransaction <<'JSON'
{
  "candidate_toml": "[global]\nasn = 65001\nrouter_id = \"10.0.0.1\"\nlisten_port = 179\n\n[[fib_tables]]\nname = \"edge\"\ntable_id = 1000\nmetric = 200\n",
  "expected_runtime_snapshot_token": "<runtime-snapshot-token-from-plan>",
  "client_request_id": "deploy-2026-06-03-001",
  "comment": "roll edge FIB table definition"
}
JSON

Apply a pure static-neighbor modify transaction:

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d @ localhost:50051 rustbgpd.v1.ConfigService/ApplyConfigTransaction <<'JSON'
{
  "candidate_toml": "[global]\nasn = 65001\nrouter_id = \"10.0.0.1\"\nlisten_port = 179\n\n[[neighbors]]\naddress = \"192.0.2.10\"\nremote_asn = 65010\nhold_time = 45\n",
  "expected_runtime_snapshot_token": "<runtime-snapshot-token-from-plan>",
  "client_request_id": "deploy-2026-06-03-002",
  "comment": "adjust neighbor hold timer"
}
JSON

Apply a transaction with a confirm timer:

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d @ localhost:50051 rustbgpd.v1.ConfigService/ApplyConfigTransaction <<'JSON'
{
  "candidate_toml": "[global]\nasn = 65001\nrouter_id = \"10.0.0.1\"\nlisten_port = 179\n\n[[neighbors]]\naddress = \"192.0.2.10\"\nremote_asn = 65010\nhold_time = 45\n",
  "expected_runtime_snapshot_token": "<runtime-snapshot-token-from-plan>",
  "client_request_id": "deploy-2026-06-03-003",
  "comment": "safe neighbor timer deploy",
  "confirm_id": "deploy-2026-06-03-003",
  "confirm_timeout_seconds": 120
}
JSON

Inspect and confirm the pending transaction:

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{}' localhost:50051 rustbgpd.v1.ConfigService/GetConfigTransactionStatus

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"confirm_id":"deploy-2026-06-03-003"}' \
  localhost:50051 rustbgpd.v1.ConfigService/ConfirmConfigTransaction

Abort instead of waiting for the timer:

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"confirm_id":"deploy-2026-06-03-003"}' \
  localhost:50051 rustbgpd.v1.ConfigService/AbortConfigTransaction

The transaction executors share the same coordinator and persistence ordering as the targeted runtime CRUD paths: re-plan under the runtime snapshot token, durably stage the exact accepted candidate next to the config file, stage the live config snapshot, apply the live runtime change, publish the staged candidate with an acknowledgement, roll back on a publication failure, and only then release the coordinator lock. A stage the persister refuses — an unwritable or read-only config directory, a full filesystem, or a candidate the daemon cannot derive from its accepted config — fails the transaction with FAILED_PRECONDITION before any session, catalog, or policy state changes; a rename that fails after the stage is compensated, and an ambiguous publication or lost acknowledgement fences as described under RecoveryRequired. FIB transactions still require the FIB reconciler to already be running, so adding the first [[fib_tables]] entry to a daemon that started without any tables requires a restart.

Dynamic-neighbor transactions replace the complete [[dynamic_neighbors]] set from the candidate TOML. Static-neighbor transactions support add/delete/modify: modifies use the same delete/re-add session-reconfigure semantics as SIGHUP, then roll back on apply or persistence failure.

Catalog-only transactions can stage named policy definitions, policy neighbor_sets, peer groups, and global named policy-chain assignments before a static neighbor or dynamic range depends on them. They are full-snapshot commits: the daemon updates the live runtime config snapshot and persists the accepted TOML, but does not run SetPolicy / SetPeerGroup live mutation commands.

Policy/neighbor-set/peer-group/global-chain transactions that move an existing static neighbor's or accepted dynamic peer's resolved import/export PolicyChain are also committable when the impact is purely a chain move. The executor stages the candidate on disk and the snapshot, re-applies the resolved chains to affected live sessions, captures prior chains for rollback, publishes the staged candidate with an acknowledgement, and restores both live policy chains and the snapshot on a publication failure. Dynamic peers are selected by the canonical [[dynamic_neighbors]] range that accepted them, not by a public API field. Re-evaluating already-received routes under a new import chain requires Route Refresh, so every impacted Established peer must have negotiated the Route Refresh capability; otherwise the apply is rejected and rolled back without committing the candidate.

Peer-group/session reshape transactions commit peer-group field edits or static-neighbor peer-group reassignments that rebuild existing sessions. The executor stages the candidate on disk and the snapshot, reconfigures affected static peers with the same delete/re-add semantics as SIGHUP, captures prior peer configs for rollback, publishes the staged candidate with an acknowledgement, and restores both live peers and the snapshot on a publication failure. Live dynamic sessions accepted by an affected [[dynamic_neighbors]] range cannot be delete/re-added (they exist only because the remote dialed in), so after a successful persist the executor gracefully resets them with a Cease NOTIFICATION carrying an RFC 9003 shutdown communication; each remote's reconnect is re-accepted under the committed config, and the dynamic_neighbor_limit slot accounting stays owned by the normal session-idle reaping. The dynamic reset is post-persist and best-effort by contract: a failed transaction never flaps a dynamic peer, and a session that could not be signaled keeps its running config until it reconnects (reported in the apply response, never silently swallowed). Reassigning a range to a different peer group remains a [[dynamic_neighbors]] record edit outside the reshape family (ADR-0086).

CLI equivalent:

rbgp config diff /tmp/new-config.toml
rbgp --json config diff /tmp/new-config.toml
# The runtime snapshot token printed by plan is opaque — capture it and pass
# it back verbatim:
RUNTIME_SNAPSHOT_TOKEN="$(rbgp --json config plan /tmp/new-config.toml \
  | jq -r .runtime_snapshot_token)"
rbgp config apply /tmp/new-config.toml \
  --expected-runtime-snapshot-token "$RUNTIME_SNAPSHOT_TOKEN"
rbgp config apply /tmp/new-config.toml \
  --expected-runtime-snapshot-token "$RUNTIME_SNAPSHOT_TOKEN" \
  --client-request-id deploy-2026-06-03-003 \
  --confirm-id deploy-2026-06-03-003 \
  --confirm-timeout 120
rbgp config status
rbgp config confirm deploy-2026-06-03-003
rbgp config abort deploy-2026-06-03-003

NeighborService

Peer lifecycle management. Supports static peers from config and dynamic peers added at runtime.

RPC Description
AddNeighbor Add a peer dynamically through the presence-aware intent (starts session immediately); waits for the atomic config-file update and leaves no runtime change on persistence failure
DeleteNeighbor Remove a peer and tear down its session; waits for the atomic config-file update and rolls runtime back on persistence failure
ListNeighbors List all peers with session state and counters
GetNeighborState Get detailed state for a single peer
EnableNeighbor Re-enable a previously disabled peer
DisableNeighbor Administratively disable a peer (sends NOTIFICATION)
SoftResetIn Request inbound route refresh (RFC 2918/7313) for one or more families
RefreshOutbound Re-emit one peer's current exportable outbound inventory across all negotiated families, without resetting the session
ReplayOutbound Schedule one Established peer's negotiated IPv4/IPv6 unicast replay with terminal UPDATE EoRs; outside the v1 contract
ResetNeighbor Administratively reset one enabled peer's session (Cease/Administrative Reset with an optional shutdown communication); outside v1
AddDynamicNeighbor Add a [[dynamic_neighbors]] prefix range at runtime; persists the accepted change atomically before returning and rolls back runtime on persistence failure
DeleteDynamicNeighbor Remove a dynamic-neighbor range at runtime (stops future accepts; established peers drain on Idle); waits for the atomic config-file update and rolls runtime back on persistence failure
ListDynamicNeighbors List configured dynamic-neighbor ranges (prefix, peer group, remote ASN, description)
SetGracefulShutdown RFC 8326 initiator toggle — attach the GRACEFUL_SHUTDOWN community to outbound updates for one peer (or all peers when address is empty) and clear with clear = true

ListNeighbors and GetNeighborState expose stale=true when a peer's session-state observation is unavailable. The row remains in the inventory; its placeholder Idle state is not evidence that the session is down. Check stale before interpreting state. An observed Idle with stale=false is a different result.

ListNeighbors and GetNeighborState set is_dynamic and, for a dynamic peer, include accepted_dynamic_range with the canonical prefix and peer group captured when the connection was accepted. This is session provenance, not a query against the current matcher: deleting or editing that range does not rewrite the answer for an already-live session. The nested message is absent for static peers and from older daemons.

Add a neighbor

AddNeighborRequest.intent is field 2 and carries an inner NeighborConfig plus a required google.protobuf.FieldMask override_mask. Field 1 and the name config are reserved. A missing intent, inner config, or mask returns INVALID_ARGUMENT before persistence or runtime mutation; custom clients must migrate the former top-level config object under intent and send a mask naming each selected supported override (empty only when none are selected).

The bundled rbgp neighbor add command always sends intent with a present mask, even when no overrides are selected. It does not retry another payload.

The create override mask has a closed top-level path set: families, required_families, route_server_client, per_client_best, strict_role, add_path_receive, add_path_send, add_path_send_max, and paths_limit_receive_max. Wildcards, nested, duplicate, unknown, or partial Add-Path paths are rejected. The four Add-Path paths are one atomic block. Masked booleans preserve explicit false; masked family lists replace inherited lists and must be non-empty. Values outside the mask must retain their protobuf defaults.

CLI positive/negative pairs are mutually exclusive: --[no-]route-server-client, --[no-]per-client-best, and --[no-]strict-role. Any Add-Path option selects the quartet; explicit numeric zero remains an override, and --no-add-path emits the all-disabled tuple. Effective inherited prerequisites are validated by the server.

Unmasked fields stay absent in the persisted neighbor and inherit through the normal config resolver. This includes peer-group TTL security, Graceful Restart, route-reflector mode, prefix ORF, IPv6-only behavior, policies, and the complete Add-Path block. With no group and no family override, IPv4 neighbors resolve to IPv4 unicast while IPv6 neighbors resolve to IPv4 and IPv6 unicast.

NeighborConfig.required_families must be a subset of families; otherwise AddNeighbor returns INVALID_ARGUMENT. Empty inherits a non-empty peer-group list; when both are empty, partial negotiation is preserved. NeighborState.config.required_families reports the effective inherited list. Both fields accept the same twelve canonical labels as the configuration file: ipv4_unicast, ipv6_unicast, ipv4_flowspec, ipv6_flowspec, l2vpn_evpn, linkstate, linkstate_vpn, l3vpn_ipv4_unicast, l3vpn_ipv6_unicast, ipv4_labeled_unicast, ipv6_labeled_unicast, and rtc. Read responses return those same labels; the bgpls spellings remain metric labels rather than configuration aliases.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"intent": {"config": {"address": "10.0.0.2", "remote_asn": 65002, "description": "peer-2"}, "overrideMask": {"paths": []}}}' \
  localhost:50051 rustbgpd.v1.NeighborService/AddNeighbor

For an IPv6 link-local / unnumbered peer, include interface in NeighborConfig. Follow-up operations that address a scoped peer use the same address + interface pair.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"intent": {"config": {"address": "fe80::5054:ff:fe00:1", "interface": "eth1", "remote_asn": 65101}, "overrideMask": {"paths": []}}}' \
  localhost:50051 rustbgpd.v1.NeighborService/AddNeighbor

List all neighbors

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.NeighborService/ListNeighbors

Get a single neighbor's state

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.NeighborService/GetNeighborState

Disable a neighbor

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2", "reason": "maintenance"}' \
  localhost:50051 rustbgpd.v1.NeighborService/DisableNeighbor

Enable a neighbor

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.NeighborService/EnableNeighbor

Reset a neighbor's session

rbgp neighbor 10.0.0.2 reset --reason "maintenance"

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2", "reason": "maintenance"}' \
  localhost:50051 rustbgpd.v1.NeighborService/ResetNeighbor

ResetNeighbor is the one-shot session bounce (bgpctl neighbor <peer> clear, clear bgp <peer>): the session sends Cease / Administrative Reset (RFC 4486 subcode 4) carrying reason as an RFC 9003 shutdown communication (deliberately capped at 128 bytes for interoperability, truncating at a UTF-8 boundary like DisableNeighbor) and closes the TCP connection. The peer's enable/disable state is untouched, so no EnableNeighbor follow-up is needed. A static active-open peer reconnects on its normal schedule; if it is already Idle, reset clears any NOTIFICATION backoff and starts the connection immediately. An accepted dynamic peer follows the normal Idle reap path and must dial in again; its next inbound connection is resolved from the current dynamic-range configuration. Scoped link-local peers pass interface as for the sibling RPCs.

  • Unknown peers return NOT_FOUND; administratively disabled peers return FAILED_PRECONDITION (enable the peer instead of resetting it).
  • A session that is not Established is still accepted: Connect/Active drop back to Idle without a NOTIFICATION and follow the static or dynamic lifecycle above. For an enabled static peer already in Idle, reset clears the NOTIFICATION reconnect backoff and starts a connection immediately; no teardown event or session-down sample is emitted because no session exists.
  • When the peer negotiated RFC 8538 Notification Graceful Restart, the reset is sent as Cease / Hard Reset wrapping the Administrative Reset and its communication, so neither side retains stale routes across the bounce.
  • An Established active-primary teardown increments bgp_session_down_total{reason="local_notification"}. Pre-established resets do not create an Established-session down sample.
  • The sent NOTIFICATION appears in EventService with its on-wire form: code 6/subcode 4 normally, or code 6/subcode 9 when Notification GR wraps the Administrative Reset as a Hard Reset. The decoded inner communication is still exposed as shutdown_reason; no new event type is introduced.

Delete a neighbor

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.NeighborService/DeleteNeighbor

Trigger SoftResetIn

# Refresh all configured families (empty families list)
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.NeighborService/SoftResetIn

# Refresh only IPv4 unicast
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2", "families": ["ipv4_unicast"]}' \
  localhost:50051 rustbgpd.v1.NeighborService/SoftResetIn

Refresh one peer's outbound routes

rbgp neighbor 10.0.0.2 refresh-out

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.NeighborService/RefreshOutbound

scheduled: true means the RIB accepted the refresh pass; resulting updates were enqueued or retained for the ordinary dirty-retry path. It does not confirm writer drain or remote receipt. Unknown peers return NOT_FOUND; managed peers without an active outbound RIB registration return FAILED_PRECONDITION.

RIB queue admission and acknowledgement share a five-second budget. Readiness and operator reads remain admitted during that wait. A read already in progress finishes under its own deadline before the scheduling call returns; later mutations stay queued until both settle. A timeout does not prove that an admitted refresh had no effect.

This is an O(table) operation for the selected peer and can create a full-table UPDATE burst on a production session. Serialize operational use; the API intentionally has no all-peer or batch form.

Replay one peer's unicast routes with terminal EoRs

rbgp neighbor 10.0.0.2 replay-out

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.NeighborService/ReplayOutbound

ReplayOutbound is an experimental, mutating RPC outside the v1 contract. It schedules wire reannouncement of one Established peer's current exportable IPv4/IPv6 unicast routes through the canonical export and transport paths. Every negotiated unicast family receives a terminal UPDATE End-of-RIB (EoR), including empty families. The request has address and optional interface; there is no family filter or all-peer form. Existing RefreshOutbound behavior is unchanged. An older daemon returns UNIMPLEMENTED for the new RPC.

The session must negotiate only IPv4/IPv6 unicast families, with at least one family, and its IP address must be unique among managed peers. A single scoped IPv6 peer is supported; repeated link-local addresses on different interfaces are refused. These prerequisites are checked before monitoring reset or replay traffic, because the reset clears the entire cached peer inventory.

Eligible collectors have a monitor list that includes rib_out_post and omits rib_in_pre (for example monitor = ["rib_out_post"]; the default ["rib_in_pre"] is not eligible). Before replay, each receives a monitoring-only Peer Down (reason 5) followed by the current Peer Up, clearing its previous peer inventory. The BGP session stays established. Mixed inbound/outbound collectors are excluded because this operation cannot rebuild their inbound inventory. After enrollment, ordinary initial-table and peer-refresh unicast EoRs are not mirrored for the rest of this BGP writer generation, including for collectors excluded from enrollment. The actual BGP EoRs are still sent. Further complete BMP boundaries require another explicit replay; a new BGP session restores ordinary EoR mirroring.

The response contains only scheduled. A true value confirms admission for scheduling; it does not confirm completion, collector receipt, or remote BGP processing. Terminal BMP EoRs mark replay completion only after the session writer completes the preceding replay bytes and terminal wire EoRs. A caller disconnecting after scheduling does not cancel traffic already admitted.

While scheduling waits, readiness and operator reads remain admitted. If the five-second scheduling budget expires or its caller disconnects, an already admitted read finishes under its own deadline before the peer manager moves on to later mutations. These reads report live state; a session that cannot answer still has the read's normal timeout and failure behavior.

Unknown peers return NOT_FOUND. Unavailable sessions, another active replay, an empty or mixed-family session, a duplicate managed peer IP address, no eligible connected rib_out_post collector, or an export gate that prevents complete replay return FAILED_PRECONDITION. Dispatch, timeout, or lost acknowledgement failures return an error rather than a successful scheduling response. Later writer, peer, or collector generation failures suppress terminal BMP completion for affected streams. Absence of terminal BMP EoRs is incomplete evidence, even if scheduling succeeded.

This operation reannounces the selected peer's table on its live BGP session. Serialize full-table use and collect the complete BMP stream through its terminal EoRs when using the result as replay evidence.


PolicyService

Named policy definition CRUD plus global and per-neighbor chain assignment. Chain changes apply immediately for future route processing. Import-policy changes do not retroactively re-evaluate existing Adj-RIB-In state; use SoftResetIn if you need a full inbound refresh.

RPC Description
ListPolicies List all named policy definitions
GetPolicy Return one named policy definition
SetPolicy Create or replace a named policy definition
DeletePolicy Delete a named policy definition (rejected while referenced)
ListNeighborSets / GetNeighborSet List or fetch a named neighbor set
SetNeighborSet / DeleteNeighborSet Create/replace or delete a named neighbor set
GetGlobalPolicyChains Return global import/export chain assignments
SetGlobalImportChain / SetGlobalExportChain Replace global chain assignment. Sessions positively down (Idle/Connect/Active) accept the new chain in memory and install it (plus RIB outbound registration) at PeerUp; retained Adj-RIB-In under an active GR/LLGR window remains evaluated under the prior chain until re-sync/EOR. Ambiguous sessions (SessionGone / state-query timeout) fail closed with INTERNAL and full compensation. IPv4/IPv6-unicast Route Refresh still requires an Established session with Route Refresh negotiated
ClearGlobalImportChain / ClearGlobalExportChain Remove the global chain assignment
GetNeighborPolicyChains Return one neighbor's import/export chain assignments
SetNeighborImportChain / SetNeighborExportChain Replace one neighbor's chain assignment
ClearNeighborImportChain / ClearNeighborExportChain Remove one neighbor's chain assignment
ExplainImportPolicy Explain why a prefix was permitted / denied / withdrawn / evicted / stale / not-seen on import for a given neighbor, reading the per-session import-decision cache (ADR-0073). For .rpol chain members the statement trace names the deciding term and carries per-term trace lines (ADR-0096). A bounded read timeout returns DEADLINE_EXCEEDED, not synthetic NO_SESSION. Side-effect-free; IPv4/IPv6 unicast only. SensitiveRead tier.
ListRejectedRoutes List every rejected inbound route a peer's session has retained, each tagged with its canonical reject-reason token (policy_reject, otc_route_leak, next_hop_ownership, as_path_loop, rr_loop, treat_as_withdraw), a bounded sub-reason detail, and a best-effort attribute summary. A named .rpol deny identifies its deciding policy:term; named TOML/default denies retain policy-only detail. The enumeration complement to ExplainImportPolicy's point lookup. Retention is a bounded per-peer LRU ([policy.reject_retention]); the response reports retention_enabled, capacity, and optional evictions_since_reset (absent from older daemons) so completeness is explicit. A bounded read timeout returns DEADLINE_EXCEEDED, not NOT_FOUND. Side-effect-free; IPv4/IPv6 unicast only. CLI: rbgp rib received <peer> --rejected. SensitiveRead tier.
TestPolicy Dry-run a candidate .rpol policy (source sent in the request, compiled before RIB access) read-only over a version-fenced walk of the retained post-policy Adj-RIB-In (import) or Loc-RIB best routes (export). Import sees routes admitted by import policy when they were received or last re-evaluated: retained Adj-RIB-In under an active GR/LLGR window, or not yet re-evaluated after an import-policy change, can hold routes the installed chain would now reject, and routes a candidate would newly admit are not visible (ListRejectedRoutes lists recent rejections). Routes are evaluated in canonical (prefix, peer, path_id) order in pages capped at 1,000; family, limit, show_changes, counts, and term hits apply globally. A conservative mutation of the selected Received/Best table returns ABORTED with no partial response: retry the whole RPC from the beginning. Paging-generation exhaustion or an unavailable RIB backend returns UNAVAILABLE. No route, session, or counter impact; IPv4/IPv6 unicast (ADR-0096). CLI: rbgp policy test. SensitiveRead tier.
GetPolicyStats Read live per-term hit counters for installed policy chains (since chain install; direction import, export, or both — import chains also report their install generation). Explicit-peer validation plus export, import, and dataset reads share one absolute 2 s deadline. Fleet import reads use bounded concurrency, cancel publication collection on caller disconnect, and return no partial rows on deadline or unavailable errors. Chainless sessions contribute no row; unknown peers return NOT_FOUND. CLI: rbgp policy stats. SensitiveRead tier.
GetValidationPolicyPosture Conservatively classifies RPKI-invalid and ASPA-invalid routes as ENFORCED, UNENFORCED, or UNKNOWN for installed static/dynamic peers and one prospective row per accepted dynamic range. The bounded response reports complete and omitted; an incomplete aggregate is never ENFORCED. This proves policy disposition only, not validator readiness, connectivity, configured intent, FIB state, or runtime enforcement. SensitiveRead tier; outside the narrow v1-stable surface.

Import rows use the selected session's actual installed counters, labels and generation. Numeric fields are sampled during collection; neither a row nor the fleet response is one atomic snapshot, and derived ratios are not exact instantaneous fractions. Import error count and error detail are acquired together. A session that has not initialized its observation remains pending within the deadline; a closed session or invalid counter state returns UNAVAILABLE, without partial rows. Counter availability does not establish that the session command loop is responsive or replace the live readiness checks. A backend result observed at or after the shared absolute deadline returns DEADLINE_EXCEEDED, including an otherwise successful final dataset reply. See ADR-0133.

Sessions continue to service direct transport import-counter queries and neighbor-state queries during grouped unicast output, including waits for shared encoded chunks. Earlier queued session commands retain their ordering for those reads. GetPolicyStats bypasses that session command queue for its import stage; peer-manager admission and other backend stages still consume the shared two-second deadline.

GetPolicyStats adds bounded stage timing to the existing grpc_authz request_summary: peer_validation (targeted requests only), export, import, and datasets, in execution order. Each completed stage records elapsed_ms, its remaining shared budget_ms at entry, cumulative rpc_elapsed_ms, and the gRPC code. A waiting stage records state=waiting so cancellation retains the active API stage. A failed stage is the last completed entry; no later stages run. These additive diagnostic fields do not change RPC responses or the two-second deadline. They identify the API wait, not its underlying actor or session cause. RUST_LOG=info,policy_stats=debug also emits structured stage-completion events; no per-peer records are emitted for a fleet request.

The daemon installs each peer's complete effective export chain, including global inheritance. GetPolicyStats and rbgp policy stats report those installed peer chains; they do not include a separate global export row. The protobuf's global owner denotes a RIB fallback chain instance, not an aggregate of peer counters. Embedders may still install that fallback through RibManager::new; the daemon does not retain a startup fallback.

Policy statements support the same match surface as TOML config: prefix, ge, le, match_community, match_as_path, match_neighbor_set, match_route_type, match_as_path_length_ge/le, match_local_pref_ge/le, match_med_ge/le, match_next_hop, match_rpki_validation, match_aspa_validation, and match_evpn_route_type.

Explain an import decision

Answer "why didn't this prefix come in?" (or "what did the chain do to it?") from the per-session import-decision cache (ADR-0073). Side-effect-free — no RIB touch, no counter movement. Omit path_id to return every matching path. The cache is opt-in: without [policy.explain] enabled = true on the daemon the outcome is CACHE_DISABLED, which is deliberately distinct from NOT_SEEN. If the peer has no current session actor, the outcome is NO_SESSION; this is also distinct from a session that answered without a cached decision. CACHE_DISABLED and NO_SESSION are normal explain outcomes, not transport errors.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "peer_address": "10.0.0.2",
    "afi_safi": "IPV4_UNICAST",
    "prefix": "192.0.2.0",
    "prefix_length": 24
  }' \
  localhost:50051 rustbgpd.v1.PolicyService/ExplainImportPolicy

Each matches entry carries an outcome — PERMIT / DENY / WITHDRAWN / EVICTED / STALE / NOT_SEEN — plus decision attribution and the modifications applied. A Deny names its configured denying member; a Permit after a nonempty chain uses chain_default_permit, while an absent or empty chain stays inline. Compare a match's policy_generation to the response's current_policy_generation to spot a STALE decision recorded before a policy reload.

List a peer's rejected routes

Answer "show me everything of mine you filtered, and why" without knowing a prefix in advance — the looking-glass filtered-route surface. Side-effect-free. Returns NOT_FOUND when the peer has no live session (the session-local retention store is gone, which is honestly distinct from "nothing rejected").

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"peer_address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.PolicyService/ListRejectedRoutes

Each retained rejection carries the prefix identity, the canonical reason token with its bounded sub-reason detail (policy:term for a named .rpol deciding term, otherwise the matched policy name for policy_reject), the announcement's next hop, AS_PATH, communities, RPKI/ASPA validation states, and the rejection timestamp. retention_enabled: false means the empty listing is a configuration fact. Present evictions_since_reset: 0 proves no retained row was displaced this session; a positive count means the listing may be incomplete, while absent means the older serving daemon cannot answer. CLI: rbgp rib received <peer> --rejected.

Create or replace a named policy

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "name": "tag-internal",
    "definition": {
      "default_action": "permit",
      "statements": [
        {
          "action": "permit",
          "prefix": "10.0.0.0/8",
          "le": 16,
          "set_community_add": ["65001:100"]
        }
      ]
    }
  }' \
  localhost:50051 rustbgpd.v1.PolicyService/SetPolicy

Attach a global import chain

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"policy_names": ["reject-bogons", "tag-internal"]}' \
  localhost:50051 rustbgpd.v1.PolicyService/SetGlobalImportChain

Attach a per-neighbor export chain

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2", "policy_names": ["tag-ixp"]}' \
  localhost:50051 rustbgpd.v1.PolicyService/SetNeighborExportChain

Create a named neighbor set

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "name": "ix-clients",
    "definition": {
      "addresses": ["10.0.0.2", "10.0.0.3"],
      "remote_asns": [65002, 65003],
      "peer_groups": ["rs-clients"]
    }
  }' \
  localhost:50051 rustbgpd.v1.PolicyService/SetNeighborSet

Delete a named policy

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"name": "tag-internal"}' \
  localhost:50051 rustbgpd.v1.PolicyService/DeletePolicy

PeerGroupService

Peer-group CRUD plus neighbor membership assignment. Group definitions are full-replace and persist back to TOML. When an inherited setting changes, the daemon recomputes effective per-neighbor config and reconciles only the peers that reference that group. Read responses redact md5_password and expose only the non-secret has_md5_password presence flag. SetPeerGroup preserves the existing stored MD5 password when the field is omitted or set to true without a new md5_password; set has_md5_password = false with no md5_password to clear it explicitly. Use the configuration file or write-side source of truth to inspect credential material.

PeerGroupDefinition.required_families is the raw group list returned by peer-group CRUD. An empty neighbor list inherits it and cannot clear it; a non-empty neighbor list overrides it. Every effective requirement must be a subset of the member's effective configured families.

RPC Description
ListPeerGroups List all peer-group definitions
GetPeerGroup Return one peer-group definition
SetPeerGroup Create or replace a peer-group definition
DeletePeerGroup Delete a peer-group definition (rejected while referenced)
SetNeighborPeerGroup Assign one neighbor to a peer group
ClearNeighborPeerGroup Remove a neighbor's peer-group reference

Create or replace a peer group

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "name": "rs-clients",
    "definition": {
      "families": ["ipv4_unicast", "ipv6_unicast"],
      "required_families": ["ipv6_unicast"],
      "hold_time": 90,
      "route_server_client": true,
      "export_policy_chain": ["tag-ixp", "suppress-leaks"]
    }
  }' \
  localhost:50051 rustbgpd.v1.PeerGroupService/SetPeerGroup

Assign a neighbor to a peer group

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"address": "10.0.0.2", "peer_group": "rs-clients"}' \
  localhost:50051 rustbgpd.v1.PeerGroupService/SetNeighborPeerGroup

RibService

Query the routing information base. Its route surface is query-only; stream live route changes through EventService.WatchEvents or EventService.SubscribeFromEvent.

RPC Description
ListReceivedRoutes Adj-RIB-In: all routes received from peers
ListBestRoutes Loc-RIB: best route per prefix after path selection
ListAdvertisedRoutes Adj-RIB-Out: routes advertised to a specific peer
ExplainAdvertisedRoute Dry-run export decision for one prefix (or, with rd, one VPN identity) to one peer: the full gate ladder in live evaluation order (split horizon, RFC 4456 reflection, family, RFC 9494 LLGR, RFC 5291 ORF, RFC 4684 RT membership, export-policy rejection with per-term attribution or nonempty Permit with chain_default_permit, Adj-RIB-Out diff), produced by a dry run of the live staging body. For negotiated unicast Add-Path send, optional source { peer_address, path_id } selects one exact Adj-RIB-In candidate.
ExplainBestPath Show all candidates for a prefix with decisive comparison reasons; optional peer_address field scopes to that peer's Add-Path send view
LookupBestPath Outside-v1 global-only LPM: bounded ancestor probes return the closest installed Loc-RIB winner plus every alternative for that one matched prefix from one actor turn; old daemons fail with UNIMPLEMENTED
ListFlowSpecRoutes FlowSpec routes in Adj-RIB-In / Loc-RIB view
ListEvpnRoutes EVPN routes (RFC 7432 / RFC 9136) in Loc-RIB view, filterable by route type / source peer / RD
ListReceivedEvpnRoutes Bounded accepted post-policy EVPN Adj-RIB-In for one source neighbor, filterable by type / RD
ListAdvertisedEvpnRoutes Bounded committed EVPN Adj-RIB-Out for one destination neighbor, filterable by type / RD
ExplainEvpnRoute Exact typed EVPN candidate selection and optional destination export trace, with installed and committed state kept distinct
ListBgpLsRoutes BGP-LS / BGP-LS VPN routes (RFC 9552) in Loc-RIB view, exposed as opaque NLRI/TLV bytes and filterable by family, peer, and NLRI type
ListVpnRoutes RFC 4364/4659 VPNv4/VPNv6 routes — RD-scoped customer prefixes, Route Targets, and MPLS labels
ListLabeledRoutes RFC 8277 labeled-unicast (SAFI 4) routes in Loc-RIB view — MPLS label stack plus prefix reachability
ListRtcRoutes RFC 4684 RT-Constrain membership NLRI — which Route Targets each peer imports
ListTopologyNodes RFC 9107 ORR topology nodes built from the BGP-LS Adj-RIB-In union
ListTopologyLinks RFC 9107 ORR topology links — IGP adjacencies, link addresses, and metrics (the SPF input)
ListOrrStatus RFC 9107 ORR per-vantage status — configured vantages, their resolved topology nodes, and the peers bound to them
ListBlackholeDiscards RFC 7999 BLACKHOLE kernel-discard install status when [global] honor_blackhole = true and [global] install_blackhole_discard = true
ListFibRoutes ADR-0061 general unicast Linux FIB route status for configured [[fib_tables]]
ListFibTables List the committed [[fib_tables]] and whether the FIB reconciler was started with an open command channel; UNAVAILABLE if that channel has closed. Served without querying the reconciler, so runtime_available = true is not a responsiveness guarantee (sensitive_read)
SetFibTable Create-or-replace a [[fib_tables]] entry by name (upsert; full definition, not a patch) at runtime; hot-applies through the reconciler and persists. Requires the reconciler running (≥1 table at startup) else FAILED_PRECONDITION (mutating)
DeleteFibTable Remove a [[fib_tables]] entry by name at runtime; NOT_FOUND if absent (mutating)
ListRouteEvents Recent unicast route add / withdraw / best-change / export-policy-filtered event history from the bounded in-memory RIB ring

Limit-blocked ListBlackholeDiscards rows reuse REJECTED with stable reason text active_limit_exceeded or install_rate_limited; both are retryable.

The three unicast route-listing RPCs return raw ordered extended_communities, the ASPA verification string in aspa_state, and received_at_epoch_seconds on every Route (0 means unknown). Native rbgp --json rib preserves the raw extended-community values, omits an empty aspa_state from an older daemon but retains a genuine "unknown" state, and retains the zero receive-time sentinel. The same native route serializer is used by best, received, advertised, and embedded ExplainBestPath routes.

Unknown peers in peer-scoped views

A peer-scoped view that finds no rows returns NOT_FOUND with the message neighbor <address> not found when the address names no known peer. This covers ListReceivedRoutes with neighbor_address set, ListAdvertisedRoutes, received-mode ListFlowSpecRoutes, ListReceivedEvpnRoutes, ListAdvertisedEvpnRoutes, ExplainEvpnRoute with received_from or advertised_to when that side is absent, and BfdService.GetBfdSessions with peer_address set. A known peer is a configured neighbor, an accepted dynamic peer, or an address whose Adj-RIB-In still retains Graceful Restart or LLGR stale routes after its session ended; the first two clauses are the same managed-peer answer GetPolicyStats uses. The synthetic peer 0.0.0.0 that owns routes added through InjectionService is always known. A known peer that is down or has sent nothing still returns OK with an empty result. The daemon checks only when a view is empty, so a view with rows never pays for it. The whole check is bounded by one peer-manager read deadline, and exceeding it returns DEADLINE_EXCEEDED.

At startup the gRPC listeners serve before the configured-peer roster is installed, so for that brief window a configured neighbor is not yet known and these views, like GetNeighborState and GetPolicyStats, return NOT_FOUND for it. /readyz and systemd READY=1 are reported only after the roster is installed.

A continuation token does not outlive its peer. Removing a peer mutates the route table, so the next continuation returns ABORTED. Restarting from an empty token returns NOT_FOUND only once the address is neither managed nor retaining stale routes; while its Adj-RIB-In still retains Graceful Restart or LLGR stale routes, the restart returns OK with those rows.

Prefix-SID inspection on VPN and EVPN routes

VpnRouteEntry.prefix_sid (field 14) and EvpnRouteEntry.prefix_sid (field 17) are optional views of the retained Prefix-SID path attribute. VPN Loc-RIB listings and all EVPN route views, explain snapshots, and current or previous event snapshots use the same conversion. An absent attribute leaves the field absent; older daemons and stored event records decode with no view.

The view carries the complete attribute value in raw_value and the original path-attribute flags. services contains the first L3 (type 5) and first L2 (type 6) Service TLV, preserving service, SID Information, and SID Structure order. Each SID exposes its advertised IPv6 sid_value, numeric endpoint_behavior (including unknown codes), separate SID flags, and all six structure fields: locator block, locator node, function, argument, transposition length, and transposition offset. Reserved bytes, unknown TLVs, and ignored duplicate services remain available in raw_value.

rbgp --json rib vpn, EVPN route lists and explain output, and EVPN event JSON expose an optional prefix_sid object with lowercase hex raw_value. Text output adds an advertised SID summary; structure lists the six fields in the order above. JSON includes a nonempty decode_error and no decoded services if stored raw data fails structural inspection; the raw bytes remain available. Routes without Prefix-SID keep their existing JSON shape.

Each SID can also carry optional reconstructed_sid (field 5), restoring a Function from the high-order bits of the route's label: a single 20-bit VPN label or the corresponding 24-bit EVPN service field. MAC/IP L2 and L3 services use label 1 and label 2 respectively; L3 requires an IP address. Ethernet A-D per EVI uses its L2 label, IP Prefix uses its L3 label, and IMET uses a single ingress-replication PMSI label. The raw sid_value stays unchanged. Text labels the derived value reconstructed-sid; JSON omits the field when it is unavailable, including when reading an older daemon or event record.

Reconstruction requires exactly one SID Structure, a nonzero transposition wholly inside its Function, valid bounds, and zero advertised bits in the vacated slice. Missing or ambiguous labels/structures, no transposition, and nonzero Argument lengths leave it absent. Argument composition involving another route (such as Ethernet A-D per ES plus IMET) is outside this view.

This is attribute inspection. It does not validate endpoint behavior against the route family, select a service, originate SRv6 routes, or program forwarding. EVPN remains alpha.

Runtime observability surfaces

Runtime visibility is intentionally split by access pattern rather than forced through one RPC shape.

Need Surface Shape Retention / loss behavior
Live unicast route deltas EventService.WatchEvents with EVENT_CATEGORY_ROUTE Streaming event feed Live-only; WatchEvents emits stream_lagged when slow subscribers miss events
Recent unicast route timeline ListRouteEvents / rbgp events Bounded history query In-memory 4096-event ring, process-local, oldest entries evicted
Live session lifecycle EventService.WatchEvents with EVENT_CATEGORY_SESSION Streaming event feed Live-only; no replay after reconnect
Recent session lifecycle EventService.ListSessionEvents / rbgp events sessions Bounded history query In-memory 4096-event ring, process-local, oldest entries evicted
Live policy mutation summaries EventService.WatchEvents with EVENT_CATEGORY_POLICY Streaming event feed Live-only; slow subscribers can lag and miss events
Recent policy mutation summaries EventService.ListPolicyEvents / rbgp events policy Bounded history query In-memory 4096-event ring, process-local, oldest entries evicted
Live EVPN route best-path deltas EventService.WatchEvents with EVENT_CATEGORY_EVPN Streaming event feed Live-only; slow subscribers can lag and miss events
Recent EVPN route timeline EventService.ListEvpnEvents / rbgp events evpn Bounded history query In-memory 4096-event ring, process-local, oldest entries evicted
RFC 7999 discard programming ListBlackholeDiscards / rbgp rib blackholes Snapshot Current reconcile snapshot only; retryable limit blocks are REJECTED with active_limit_exceeded or install_rate_limited
ADR-0061 general Linux FIB programming ListFibRoutes / rbgp rib fib Snapshot Current reconcile snapshot plus persisted owned-state semantics
ADR-0061 FIB route apply outcomes EventService.WatchEvents with EVENT_CATEGORY_DATAPLANE and BGP_EVENT_TYPE_DATAPLANE_ROUTE_* / rbgp events watch --category dataplane Streaming event feed Live via WatchEvents; durable replay via SubscribeFromEvent when [event_history].enabled = true; no bounded List* history API
EVPN L2/L3 dataplane readiness and managed-netdev ownership status EvpnService (ListEvpnInstances, ListEvpnNexthops, ListEthernetSegments, ListIpVrfs, ListManagedNetdevs) / rbgp evpn ... Snapshot Latest daemon or dataplane report snapshot
BGP-LS topology objects ListBgpLsRoutes / rbgp rib bgpls Snapshot Current Loc-RIB only; raw BGP-LS NLRI/TLV bytes preserved
ADR-0067 BFD session state BfdService.GetBfdSessions / rbgp bfd Snapshot Current BFD actor snapshot
Live BFD session state changes EventService.WatchEvents with EVENT_CATEGORY_BFD and BGP_EVENT_TYPE_BFD_SESSION_* / rbgp events watch --category bfd Streaming event feed Live-only; opt-in (not in the default route+session set); slow subscribers can lag
Alerting / counters Prometheus /metrics Cumulative counters and gauges Process lifetime, scrape-dependent

Use a live stream when you need a tail, ListRouteEvents when you need recent route context after the fact, and status RPCs for current ownership/readiness state. WatchEvents does not replay ListRouteEvents; clients that need both context and a live tail should query history first, then subscribe.

List received routes (Adj-RIB-In)

# All received routes
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.RibService/ListReceivedRoutes

# From a specific peer
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"neighbor_address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.RibService/ListReceivedRoutes

List best routes (Loc-RIB)

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.RibService/ListBestRoutes

List advertised routes (Adj-RIB-Out)

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"neighbor_address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.RibService/ListAdvertisedRoutes

Explain one advertised route decision

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"peer_address": "10.0.0.2", "prefix": "203.0.113.0", "prefix_length": 24}' \
  localhost:50051 rustbgpd.v1.RibService/ExplainAdvertisedRoute

This dry-runs the current export decision for a single prefix and peer. The response includes the final decision, decisive reasons, selected best-route identity, and any export modifications that would be applied.

For an IPv4/IPv6 unicast peer with negotiated Add-Path send, set the optional presence-bearing source message to explain one exact Adj-RIB-In candidate:

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"peer_address":"10.0.0.2","prefix":"203.0.113.0","prefix_length":24,"source":{"peer_address":"198.51.100.7","path_id":0}}' \
  localhost:50051 rustbgpd.v1.RibService/ExplainAdvertisedRoute

The echoed source.path_id remains the inbound Add-Path identity. The top-level response path_id is the independent, compact outbound rank rustbgpd would assign after eligibility and export policy. That outbound rank is 0 for a pre-selection/policy denial or a candidate beyond add_path_send_max. A later OTC or exact-wire denial retains the attempted non-zero rank, and the adj_rib_out rung compares that exact rank. Inbound ID 0 remains selectable because message presence, not a sentinel, distinguishes selection from the legacy winner-oriented query. Unknown source identity returns NOT_FOUND; a source selector without negotiated unicast Add-Path send returns FAILED_PRECONDITION. source is mutually exclusive with rd and labeled.

Best-path explain is also available via ExplainBestPath RPC — it returns all candidates for a prefix with the decisive comparison reason for each. Set peer_address on the request to scope the response to that peer's Add-Path send view: candidates that the peer would actually receive (export-policy permitted + sendable-family + not suppressed by split-horizon or iBGP / RFC 4456 route-reflector rules + within the peer's effective add_path_send_max) get a non-zero advertised_path_id reflecting the rank they would carry on the wire; everything else stays at advertised_path_id = 0. The response echoes peer_address and the effective add_path_send_max so the operator can read advertisement intent without cross-referencing the peer config. Empty peer_address returns the v0.7.0 global Loc-RIB view unchanged. Unknown peer_addressNOT_FOUND. Import explain is available via PolicyService.ExplainImportPolicy (ADR-0073), including structured statement/term traces for matched policies.

SRv6 semantic ineligibility is distinct from an import-policy rejection. ExplainBestPath retains these candidates with vs_best_reason = "srv6_sid_invalid", multipath = "none", and advertised_path_id = 0. If no eligible candidate remains, the RPC succeeds with an absent best_route and empty best_reason; the CLI prints the retained candidates. A prefix with no retained candidates still returns NOT_FOUND. only_path means one eligible candidate, even if other retained candidates are ineligible. EVPN received views and exact explain likewise retain the received route and its eligibility reason. ListVpnRoutes exposes selected VPN routes only; no received-VPN query is available.

Longest-prefix lookup in the global Loc-RIB

rbgp rib lookup 203.0.113.99
rbgp --json rib lookup 2001:db8::7/128

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"prefix":"203.0.113.99","prefix_length":32}' \
  localhost:50051 rustbgpd.v1.RibService/LookupBestPath

rbgp rib lookup <IP|CIDR> issues exactly one LookupBestPath request. Bare IPv4 and IPv6 addresses use /32 and /128; an explicit CIDR keeps its mask. The daemon performs bounded ancestor probes and returns the closest installed global Loc-RIB winner plus every alternative for that matched prefix from one actor turn. The response therefore uses the existing best-path explanation shape, and its prefix may be less specific than the query. There is no peer-scoped mode and no client-side listing fallback. A syntactically invalid target is rejected locally, no covering route returns NOT_FOUND, and a daemon without the outside-v1 method returns UNIMPLEMENTED (not supported by this daemon in the CLI).

Address family filtering

Route-listing RPCs and the route-event surfaces (ListReceivedRoutes, ListBestRoutes, ListAdvertisedRoutes, ListRouteEvents, and EventService.WatchEvents) accept an afi_safi field to filter by address family. Supported values are IPV4_UNICAST (1), IPV6_UNICAST (2), or unspecified (0, returns both unicast families). Route watch events include the address family of each route change. FlowSpec routes use ListFlowSpecRoutes with IPV4_FLOWSPEC (3), IPV6_FLOWSPEC (4), or unspecified (0). EVPN routes use ListEvpnRoutes and its EVPN-specific filters. BGP-LS routes use ListBgpLsRoutes with ADDRESS_FAMILY_BGP_LS (6), ADDRESS_FAMILY_BGP_LS_VPN (7), or unspecified (0).

Pagination

The unicast route-listing RPCs ListReceivedRoutes, ListBestRoutes, and ListAdvertisedRoutes support pagination via page_size and page_token. ListFibRoutes also supports optional pagination; unlike the route-listing RPCs, page_size = 0 preserves the legacy behavior and returns the full filtered FIB status snapshot. ListBlackholeDiscards, ListFlowSpecRoutes, and ListBgpLsRoutes do not support pagination.

Their shared ListRoutesRequest also accepts optional recorded-route predicates: typed rpki_validation (VALID, INVALID, or NOT_FOUND), typed aspa_validation (VALID, INVALID, or UNKNOWN), and presence-aware as_path_contains for one exact nonzero ASN. UNSPECIFIED means no verdict filter; unknown numeric enum values and ASN 0 return INVALID_ARGUMENT. Predicates are identical across Received, Best, and Advertised, compose AND-wise with the existing prefix/origin/community filters, apply to both total_count and bounded pages, and are part of the opaque continuation-token identity. AS-path membership covers exact numeric members of represented AS_SEQUENCE and AS_SET segments only; it is not a regex or policy evaluation. RFC 9774 rejection of newly received AS_SET and AS_CONFED_SET forms remains unchanged.

# First page (2 routes)
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"page_size": 2}' \
  localhost:50051 rustbgpd.v1.RibService/ListBestRoutes

# Next page (pass the previous response's next_page_token verbatim)
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"page_size": 2, "page_token": "<next_page_token>"}' \
  localhost:50051 rustbgpd.v1.RibService/ListBestRoutes

page_token is opaque: always pass the next_page_token from the previous response (empty = listing complete), and only reuse it with the same RPC, neighbor, route scope, and semantically equivalent route filters. Changing a scope or filter returns gRPC INVALID_ARGUMENT; changing only page_size is safe. Tokens are process-local and mutation-fenced: Received and Best share one conservative table generation, while Advertised has an independent generation. Any mutation in that class makes the next request fail with gRPC ABORTED; a peer-specific listing can therefore restart after an unrelated peer in the same class changes. The server retains no route snapshot or cursor registry. page_size is capped server-side at 1000 rows per page (0 = default of 100).

Every ListRoutesResponse, including an empty or terminal page, carries a page_version message with an epoch and generation. The pair exposes the same process-local consistency fence used by continuation tokens. A client may compare the complete pair across Received and Best pages in one logical capture; a changed value means the entire capture must be discarded. The values are opaque, may repeat after daemon restart, and are not a RIB snapshot generation. In particular, page_version.generation must never be compared with or substituted for a producer-local rbgp-ribsnap/1 header generation. Older daemons omit this additive message field.

Every response also carries total_count: the exact filtered count for the whole selected view, regardless of page size. This is the contract behind rbgp rib --count and rbgp rib received|advertised <PEER> --count, which request a single-row page and read only total_count. For an address that names no known peer the count fails with NOT_FOUND rather than reporting zero (unknown peers).

The same contract backs rbgp rib --limit N, rbgp rib received PEER --limit N, and rbgp rib advertised PEER --limit N for N from 1 through 1000. A limited query issues one RPC and exposes the exact total_count plus whether the returned page is complete; it never follows a continuation into a potentially changing table. Unbounded CLI listings retain the version-fenced full-walk behavior described above.

Route.validation_state is the route's recorded RPKI origin-validation verdict, not a configuration/readiness sentinel. not_found means the validation table applied to that route had no covering VRP. It is therefore the natural value for every route when no [rpki] sources are configured, but on a configured and ready deployment it is a real per-route uncovered-origin result. Consumers must use the RPKI cache/readiness surfaces when they need to distinguish those deployment states; this route field cannot do so by itself. The rpki_validation request filter selects that recorded fact and therefore inherits the same distinction. aspa_validation likewise selects the recorded per-route ASPA fact rather than cache readiness.

Watch route changes (streaming)

Live route deltas stream through EventService.WatchEvents with EVENT_CATEGORY_ROUTE (see Watch unified events); durable cursor replay goes through EventService.SubscribeFromEvent.

# Live route stream (streams until interrupted)
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"categories": ["EVENT_CATEGORY_ROUTE"]}' \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

# Watch changes for a specific peer and family
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"categories": ["EVENT_CATEGORY_ROUTE"], "neighbor_address": "10.0.0.2", "afi_safi": "IPV4_UNICAST"}' \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

Each route event arrives as a BgpEvent whose route payload is the same RouteEvent message that ListRouteEvents returns. Event types: ROUTE_EVENT_TYPE_ADDED, ROUTE_EVENT_TYPE_WITHDRAWN, ROUTE_EVENT_TYPE_BEST_CHANGED, and ROUTE_EVENT_TYPE_POLICY_FILTERED. Policy-filtered events are route-level export-policy denials: peer_address is the source route peer, target_peer_address is the outbound peer whose export policy denied the route, and reason is currently policy_denied.

Each RouteEvent carries an event_id, a monotonic process-local cursor that is assigned before the event is written to history and broadcast to live subscribers. The cursor resets on daemon restart and is not reused within one process.

WatchEvents does not backfill recent events for new subscribers. Clients that need both context and a live tail should call ListRouteEvents first, then open a live stream for subsequent deltas (rbgp events watch --backfill does exactly this), or use SubscribeFromEvent when event history is enabled. WatchEvents emits BGP_EVENT_TYPE_STREAM_LAGGED with a StreamLagEvent payload when a subscriber falls behind the bounded route broadcast.

List recent route events

# Return the recent route-event timeline (oldest-to-newest within the window)
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"limit": 100}' \
  localhost:50051 rustbgpd.v1.RibService/ListRouteEvents

# Filter by peer and IPv4 unicast
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"neighbor_address": "10.0.0.2", "afi_safi": "IPV4_UNICAST", "limit": 50}' \
  localhost:50051 rustbgpd.v1.RibService/ListRouteEvents

# Drill into one exact prefix
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"prefix": "203.0.113.0", "prefix_length": 24, "limit": 20}' \
  localhost:50051 rustbgpd.v1.RibService/ListRouteEvents

ListRouteEvents reads the same unicast route events that feed WatchEvents, but from a bounded 4096-event in-memory ring. Peer filters match peer_address, previous_peer_address, and target_peer_address, so a peer-scoped query includes withdraws, best-path moves away from that peer, and routes filtered by that peer's export policy. Prefix filters are exact-match only and can be combined with peer, family, and limit filters. The filter does not do containment or longest-prefix matching, so a query for 203.0.113.0/16 will not return an event recorded for 203.0.113.0/24. event_id values are monotonic within the running daemon and can be used by clients to de-duplicate a history window against a live stream. They are not persisted or reused within one process. The history is process-local and resets on daemon restart.

List FlowSpec routes

# List all FlowSpec routes
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.RibService/ListFlowSpecRoutes

# List only IPv6 FlowSpec routes
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"afi_safi": "ADDRESS_FAMILY_IPV6_FLOWSPEC"}' \
  localhost:50051 rustbgpd.v1.RibService/ListFlowSpecRoutes

rbgp --json flowspec retains the legacy formatted components array and also emits ordered component_details records with the API component type, prefix, value, and offset. The explicit offset preserves the RFC 8956 IPv6 prefix-match semantics, including zero and nonzero values.

The ordinary ListFlowSpecRoutes response remains the selected Loc-RIB view in routes. Set received_peer_address to an IP address to inspect that peer's retained, post-import-policy candidates instead, including nonselected and infeasible rules. afi_safi narrows either view. Invalid peer addresses are rejected before the query reaches the RIB.

Received mode returns received_routes and sets received_view: true, even when empty; ordinary mode leaves both unset. An empty received view for an address that names no known peer returns NOT_FOUND instead (unknown peers). Clients must check this acknowledgement because older servers ignore the additive request field. rbgp flowspec received PEER [-a ipv4_flowspec|ipv6_flowspec] performs that check and reports an unsupported operation instead of displaying an older server's selected routes as received candidates.

Each received row contains the existing route projection plus path_id, selected, validation, reason, and pending. Validation is disabled, local, feasible, or infeasible; pending is the validation value only when no result has completed for this candidate. Otherwise a pending revalidation preserves the last completed value and reason, with pending: true. Selection is reported independently: a completed feasible result does not mean the candidate currently owns the selected entry.

Infeasibility reasons are missing_destination, nonzero_destination_offset, no_covering_unicast, local_covering_unicast, missing_as_path, unsupported_as_path, originator_mismatch, leftmost_as_mismatch, unknown_neighbor_as, and conflicting_more_specific. An empty reason means no completed failure. local_covering_unicast names an eBGP rule whose best-match cover is a locally injected unicast route without an AS_PATH, which cannot supply the leftmost AS that RFC 9117 section 4.2 requires. Received means retained after import policy, not a historical record of every UPDATE. See the feasibility decision for opt-in validation and convergence semantics.

List EVPN routes

ListEvpnRoutes retains its existing unpaginated best-route view. The additive ListReceivedEvpnRoutes and ListAdvertisedEvpnRoutes methods require neighbor_address and accept route_type_filter (0 or 1–5), rd_filter, page_size, and page_token. They are outside the narrow v1 contract; older servers return UNIMPLEMENTED without falling back to a unicast table.

Received means accepted post-policy Adj-RIB-In, including candidates that are not best. An absent route does not prove the neighbor never sent it; historical EVPN import rejection details are not retained. Advertised means committed Adj-RIB-Out for the requested destination, after export gates and successful outbound enqueue. It does not confirm remote receipt or installation. Each row's peer_address remains its original source peer, which may differ from the requested destination. Use exact EVPN explain for candidate-selection and destination export diagnostics.

These peer methods return one page with routes, total_count, next_page_token, and page_version. The default page size is 100; the maximum is 1000, and larger requests return INVALID_ARGUMENT. Rows are ordered by typed EVPN route identity and source peer. Use the returned token with the same neighbor, direction, and filters to continue. Changed scope or filters, malformed or modified tokens return INVALID_ARGUMENT; table mutations return ABORTED, requiring a restart from an empty token. An empty page for an address that names no known peer returns NOT_FOUND (unknown peers). Tokens expire on daemon restart. Version counters are conservative across peers and families in the same table class, so an unrelated mutation can also invalidate a walk. Types 3/4, attribute changes, retained-route lifecycle changes, and peer teardown participate.

Each page scans the selected table: O(table size × log page size) work and O(page size) retained route references/copies. Pagination bounds response memory; it does not provide an index or a snapshot retained across mutations.

rbgp evpn received 192.0.2.1 --route-type 2 --rd 65000:100 --page-size 100
rbgp evpn advertised 192.0.2.2 --rd 65000:100 --page-size 100

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"neighbor_address":"192.0.2.2","route_type_filter":2,"rd_filter":"65000:100","page_size":100}' \
  localhost:50051 rustbgpd.v1.RibService/ListAdvertisedEvpnRoutes

The CLI prints one page and its continuation metadata; pass --page-token '<token>' to resume, quoting the opaque token for the shell. --json returns a page object for these new views. The existing commands below retain their best-route output and syntax.

# All EVPN routes in Loc-RIB
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.RibService/ListEvpnRoutes

# Only Type 2 (MAC/IP) routes
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"route_type_filter": 2}' \
  localhost:50051 rustbgpd.v1.RibService/ListEvpnRoutes

# Filter by Route Distinguisher
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"rd_filter": "65000:100"}' \
  localhost:50051 rustbgpd.v1.RibService/ListEvpnRoutes

route_type_filter accepts 0 (no filter) or 1..=5 matching the RFC 7432 route type numbers. peer_filter is an optional typed IP-address match (for example, expanded and compressed IPv6 spellings are equivalent); rd_filter is an optional typed route-distinguisher match (for example, "65000:100", "10.0.0.1:100", or "4200000000:100" per RFC 4364 RD types 0/1/2, plus the displayed 0x/16-hex-digit fallback for unknown types). Empty strings disable each filter. Invalid filters fail with INVALID_ARGUMENT before the RIB actor is queried.

Explain an exact EVPN route

RibService.ExplainEvpnRoute takes a required EvpnRouteSelector in key: its rd and exactly one route-specific selector identify a single NLRI. Optional received_from looks up one retained source, and advertised_to evaluates one destination. Both are neighbor IP addresses. This additive, sensitive_read RPC is outside the narrow v1 contract; older servers return UNIMPLEMENTED. Existing unicast explain requests keep their current meaning.

The CLI uses rbgp evpn explain <selector> --rd <RD> with optional --received-from <PEER> and --advertised-to <PEER>. Put these options after the selector. Each selector is exact, with no wildcard or longest-prefix match:

CLI selector Proto selector Required key fields beyond RD Ethernet Tag
ead-per-es ead_per_es --esi CLI fixes it to MAX_ET; RPC requires 4294967295
ead-per-evi ead_per_evi --esi, --ethernet-tag Required; MAX_ET is rejected
mac-ip mac_ip --mac; optional --ip --ethernet-tag defaults to 0
imet imet --originator-ip --ethernet-tag defaults to 0
es es --esi, --originator-ip Not part of the Type 4 key
ip-prefix ip_prefix --prefix in canonical CIDR form --ethernet-tag defaults to 0; nonzero tags are supported

For Type 2, omitting --ip (an empty RPC ip) selects the MAC-only key; it does not match every host IP attached to that MAC. Type 5 prefixes must have zero host bits. ESI uses ten colon-separated hex octets; MAC uses six. Labels, next hop, and gateway are route payload, not selector fields. Invalid selectors return INVALID_ARGUMENT before querying the RIB.

# Exact MAC-only route, with source and destination diagnostics
rbgp evpn explain mac-ip --rd 65000:100 --mac 02:00:00:00:00:11 \
  --received-from 10.0.0.1 --advertised-to 10.0.0.2

# The same MAC with a host IP is a distinct key
rbgp evpn explain mac-ip --rd 65000:100 --mac 02:00:00:00:00:11 \
  --ip 192.0.2.11 --advertised-to 10.0.0.2

# Type 5 supports an exact IPv6 prefix and a nonzero Ethernet Tag
rbgp evpn explain ip-prefix --rd 65000:100 --ethernet-tag 100 \
  --prefix 2001:db8:100::/64 --advertised-to 10.0.0.2

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"key":{"rd":"65000:100","imet":{"ethernet_tag":0,"originator_ip":"10.0.0.1"}},"received_from":"10.0.0.1","advertised_to":"10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.RibService/ExplainEvpnRoute

The response is a read-only snapshot for that exact key. It walks the peer roster using exact lookups and returns a bounded number of routes, rather than copying every candidate or scanning the full EVPN table. --json exposes the same distinctions as the text output:

Field Meaning
received Accepted post-policy Adj-RIB-In route from received_from, if retained. Omitted when no source is requested or no accepted route is retained.
candidate_count Number of currently retained accepted candidates across sources.
best Installed Loc-RIB route.
selection_best Winner of a fresh comparison over the retained candidates, without installing it.
compared Requested retained source when it loses; otherwise the runner-up when the source wins or none was requested. Absent when the requested source is not retained or there is no comparison.
selection_reason Decisive reason why selection_best beats compared; absent without a pair.
selection_deferred EVPN selection is currently deferred; the installed best may differ from fresh selection or be absent.
export Present only when advertised_to was requested.

Selection uses the live EVPN comparator: freshness ranks precede Type 2 sticky and MAC Mobility sequence preferences, followed by the remaining BGP criteria. A fresh comparison can still select a stale route if that is the best retained candidate. Missing input is not an import rejection explanation: historical EVPN import rejection details are not retained, and absence does not prove that a peer never sent this key.

export.decision, reasons, gates, and modifications describe a current dry run through the shared export staging path, using installed best. export.staged is the post-policy candidate that clears that path, while export.advertised is the committed local Adj-RIB-Out route for the requested destination. Each route's peer_address remains its source; export.peer_address names the destination. A missing outbound session is reported as a stopping destination_unavailable gate.

already_advertised means staging found an identical committed route and suppressed a duplicate advertisement. outbound_dirty is a destination-wide resynchronization flag, not proof that this particular route is pending. A retained exact-encoder rejection can stop an otherwise eligible candidate. The query neither sends nor freshly encodes an UPDATE, and committed state is not proof of remote receipt, acceptance, or installation.

List BGP-LS routes

# All BGP-LS and BGP-LS VPN routes in Loc-RIB
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.RibService/ListBgpLsRoutes

# BGP-LS only, from one peer, for Node NLRI
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"afi_safi": "ADDRESS_FAMILY_BGP_LS", "peer_filter": "10.0.0.2", "nlri_type_filter": 1}' \
  localhost:50051 rustbgpd.v1.RibService/ListBgpLsRoutes

ListBgpLsRoutes is the ADR-0077 controller-facing BGP-LS surface. It preserves BGP-LS routes as opaque RFC 9552 objects: raw Route Distinguisher bytes for BGP-LS VPN, raw NLRI descriptor/payload bytes, and raw BGP-LS Attribute (type 29) bytes when present. Negotiated BGP-LS routes can also be reflected to eligible peers through the normal route-reflector pipeline and feed the RFC 9107 ORR topology used for per-vantage best-path selection. The daemon does not synthesize BGP-LS from a local LSDB or negotiate BGP-LS Add-Path. GR / LLGR stale preservation for BGP-LS and BGP-LS VPN is implemented as part of the RR-family stale pipeline. Its afi_safi filter accepts only unspecified, BGP-LS, or BGP-LS VPN. As with EVPN, VPN, labeled-unicast, and RT-Constrain route listings, a non-empty peer_filter must parse as an IP address and matches by address identity rather than display spelling.

List BLACKHOLE discard status

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.RibService/ListBlackholeDiscards

Returns one row per currently observed best route carrying the RFC 7999 BLACKHOLE community when the opt-in FIB reconciler is active. state is a BlackholeDiscardState enum (BLACKHOLE_DISCARD_STATE_INSTALLED, BLACKHOLE_DISCARD_STATE_REJECTED, or BLACKHOLE_DISCARD_STATE_FAILED); reason carries values such as installed, owned, broad_prefix, not_ebgp, active_limit_exceeded, install_rate_limited, foreign_route_exists, lookup_failed, remove_failed, or the kernel install error string. Both limit reasons are retryable and may transition to installed when active capacity or a token becomes available. An empty list means either the reconciler is disabled or no BLACKHOLE-marked best routes are currently visible.

List general FIB route status

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.RibService/ListFibRoutes

rbgp rib fib          # human table
rbgp rib fib --json   # JSON array for scripts
rbgp rib fib --table edge --state rejected --reason route_limit_exceeded
rbgp rib fib --prefix 203.0.113.0/24 --neighbor 198.51.100.2
rbgp rib fib --page-size 100

Returns one row per desired route, daemon-owned route, or one-pass reconciliation outcome in the ADR-0061 general unicast Linux FIB runtime. The runtime is default-off and only starts when at least one [[fib_tables]] block is configured. state is a FibRouteState enum (FIB_ROUTE_STATE_INSTALLED, FIB_ROUTE_STATE_REJECTED, FIB_ROUTE_STATE_FAILED, or FIB_ROUTE_STATE_UNRESOLVED); reason carries values such as owned, foreign_route_exists, next_hop_family_unsupported, peer_not_allowed, route_limit_exceeded, owned_route_drifted, next_hop_unresolved, dump_failed:DETAIL, or a kernel apply error such as install_failed:DETAIL. When a pre-kernel planning query fails, this RPC preserves the last successful status snapshot; use bgp_dataplane_reconcile_planning_failures_total and the matching structured warning for the current failure.

ListFibRoutesRequest supports optional filters for table_name, state, reason, exact prefix + prefix_length, and peer_address; filters compose with AND semantics. The prefix filter is an exact route-key match, not longest-prefix or containment matching, so 203.0.113.0/24 does not match 203.0.113.128/25. Empty strings and FIB_ROUTE_STATE_UNSPECIFIED mean "no filter"; for direct gRPC callers, prefix_length must be 0 when prefix is empty. rbgp rib fib exposes the same filters as --table, --state, --reason, --prefix, and --neighbor. page_size and page_token enable optional pagination over the filtered status rows; page_size = 0 keeps the legacy full-snapshot response, and page_token is valid only when page_size > 0. The response includes next_page_token and total_count; CLI JSON output remains a route array unless --page-size is greater than 0, in which case it emits an object with routes, next_page_token, total_count, and optional sampling metadata.

table_id, metric, prefix, prefix_length, next_hop, and next_hops describe the route identity and forwarding value. next_hop is the selected best route's representative gateway; next_hops is the canonical installed set for ECMP / multipath rows. The CLI human table renders Table, Metric, Prefix, Next hop, State, and Reason; JSON output uses table_name, table_id, metric, prefix, next_hop, next_hops, peer_address, state, reason, and optional sampling. Sampling is present on high-cardinality rows such as route_limit_exceeded; it reports the number of surfaced sample rows, suppressed rows, total rows in that table/metric/reason set, the configured max_routes, the status sample cap, and whether the sample is complete. Pagination still applies only to surfaced rows: total_count does not include suppressed rows. A pre-existing kernel row in a configured table is reported as foreign_route_exists; RTPROT_BGP is not ownership proof by itself because another daemon can use the same protocol marker. A row rustbgpd previously owned but later finds changed by another writer is reported as owned_route_drifted; the daemon releases ownership and does not delete that replacement on a later withdraw.


EventService

Unified typed live event stream. Current categories are route events, session events (peer lifecycle plus BGP NOTIFICATION sent/received metadata), policy mutation summary events, EVPN route best-path events, dataplane status-row summary changes for the daemon-owned FIB / BLACKHOLE discard reconcilers, live ADR-0061 per-route FIB apply outcomes, and BFD session state changes.

RPC Description
WatchEvents Server-streaming: unified typed event stream sourced from structured daemon events
SubscribeFromEvent Server-streaming: durable event-history cursor replay from the local outbox, then live
ListSessionEvents Unary: recent session lifecycle events from the peer manager's bounded in-memory history
ListPolicyEvents Unary: recent policy / neighbor-set / peer-group / chain mutation events from the peer manager's bounded in-memory history
ListEvpnEvents Unary: recent EVPN route add / withdraw / best-change events from the RIB's bounded in-memory history

Watch unified events

# Watch the default live route + session events
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

# Watch only route adds for one exact prefix
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"categories": ["EVENT_CATEGORY_ROUTE"], "event_types": ["BGP_EVENT_TYPE_ROUTE_ADDED"], "prefix": "203.0.113.0", "prefix_length": 24}' \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

# Watch session establishment/loss for one peer
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"categories": ["EVENT_CATEGORY_SESSION"], "event_types": ["BGP_EVENT_TYPE_SESSION_ESTABLISHED", "BGP_EVENT_TYPE_SESSION_LOST"], "neighbor_address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

# Watch BGP NOTIFICATIONs for one peer
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"categories": ["EVENT_CATEGORY_SESSION"], "event_types": ["BGP_EVENT_TYPE_NOTIFICATION_SENT", "BGP_EVENT_TYPE_NOTIFICATION_RECEIVED"], "neighbor_address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

# Watch policy / peer-group / chain mutation summaries
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"categories": ["EVENT_CATEGORY_POLICY"], "event_types": ["BGP_EVENT_TYPE_POLICY_CHANGED"]}' \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

# Query recent session establishment/loss history for one peer
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"event_types": ["BGP_EVENT_TYPE_SESSION_ESTABLISHED", "BGP_EVENT_TYPE_SESSION_LOST"], "neighbor_address": "10.0.0.2", "limit": 20}' \
  localhost:50051 rustbgpd.v1.EventService/ListSessionEvents

# Query recent peer-scoped policy mutation history
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"event_types": ["BGP_EVENT_TYPE_POLICY_CHANGED"], "neighbor_address": "10.0.0.2", "limit": 20}' \
  localhost:50051 rustbgpd.v1.EventService/ListPolicyEvents

# Watch FIB / BLACKHOLE dataplane status-row summary changes
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"categories": ["EVENT_CATEGORY_DATAPLANE"], "event_types": ["BGP_EVENT_TYPE_DATAPLANE_STATUS_CHANGED"]}' \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

# Watch per-route FIB install failures for one prefix
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"categories": ["EVENT_CATEGORY_DATAPLANE"], "event_types": ["BGP_EVENT_TYPE_DATAPLANE_ROUTE_FAILED"], "prefix": "203.0.113.0", "prefix_length": 24}' \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

# Watch EVPN route best-path changes for one peer
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"categories": ["EVENT_CATEGORY_EVPN"], "event_types": ["BGP_EVENT_TYPE_EVPN_ROUTE_ADDED", "BGP_EVENT_TYPE_EVPN_ROUTE_WITHDRAWN", "BGP_EVENT_TYPE_EVPN_ROUTE_BEST_CHANGED"], "neighbor_address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.EventService/WatchEvents

# Replay all retained durable events, then continue live
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"from_event_id": 0}' \
  localhost:50051 rustbgpd.v1.EventService/SubscribeFromEvent

# Query recent Type 2 EVPN route events for one RD
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"route_type_filter": 2, "rd_filter": "65000:100", "limit": 20}' \
  localhost:50051 rustbgpd.v1.EventService/ListEvpnEvents

WatchEvents is a live stream only: it does not replay the bounded ListRouteEvents history and it does not persist events. The rbgp events watch --backfill N command composes both RPCs client-side for route events by subscribing live first, printing recent ListRouteEvents results through the same BgpEvent renderer used by the live stream, and suppressing live route events whose event_id was already printed. The command prints a history block followed by the live tail; it does not server-side merge the two streams by wall-clock timestamp.

SubscribeFromEvent is the restart-safe durable cursor surface backed by [event_history]'s local SQLite outbox. Its from_event_id field uses explicit presence: absent means live-only, 0 replays every retained event before joining live, and N > 0 replays events with event_id > N before joining live. If N is older than the retention floor, the first response is a BGP_EVENT_TYPE_STREAM_LAGGED event whose missed_count describes the global outbox gap; the stream then continues from the earliest retained event. If retention evicts events ahead of a replay still in progress, another such event precedes the next replayed event and counts exactly the evicted ids, so a collector must accept this event at any position in the stream. Empty category and type filters select every EHM-fed category on this RPC: route, EVPN, session lifecycle, session notifications, policy, dataplane, and BFD. Dataplane summary and per-route FIB apply events remain live on WatchEvents and are also replayable through SubscribeFromEvent when event history is enabled. When event history is disabled or startup fell back to live-only operation, the RPC returns FAILED_PRECONDITION at admission; legacy live and List*Events RPCs are unaffected. That admission result takes precedence over filter validation.

Pre-admission loss is the baseline; later loss wins delivery races and ends with DATA_LOSS. Cursors cannot replay pre-commit loss, so reconcile state first.

Filters compose AND-wise across category, type, peer, family, and exact prefix. Repeated categories are ORed, repeated types are ORed, and the two dimensions are ANDed. A request is accepted when any selected category/type pair is real; no intersection returns INVALID_ARGUMENT. An empty category or type dimension is a wildcard for compatibility validation (the live default/opt-in selection rules below still apply).

Synthetic BGP_EVENT_TYPE_STREAM_LAGGED control frames bypass ordinary category, type, peer, family, and prefix filters after compatibility validation.

Live WatchEvents compatibility:

Category Compatible event types
Route BGP_EVENT_TYPE_ROUTE_ADDED, BGP_EVENT_TYPE_ROUTE_WITHDRAWN, BGP_EVENT_TYPE_ROUTE_BEST_CHANGED, BGP_EVENT_TYPE_ROUTE_POLICY_FILTERED, BGP_EVENT_TYPE_STREAM_LAGGED
Session BGP_EVENT_TYPE_SESSION_STATE_CHANGED, BGP_EVENT_TYPE_SESSION_ESTABLISHED, BGP_EVENT_TYPE_SESSION_LOST, BGP_EVENT_TYPE_PEER_ADDED, BGP_EVENT_TYPE_PEER_REMOVED, BGP_EVENT_TYPE_PEER_ENABLED, BGP_EVENT_TYPE_PEER_DISABLED, BGP_EVENT_TYPE_MAX_PREFIX_WARNING, BGP_EVENT_TYPE_NOTIFICATION_SENT, BGP_EVENT_TYPE_NOTIFICATION_RECEIVED, BGP_EVENT_TYPE_STREAM_LAGGED
Policy BGP_EVENT_TYPE_POLICY_CHANGED (never BGP_EVENT_TYPE_STREAM_LAGGED)
Dataplane BGP_EVENT_TYPE_DATAPLANE_STATUS_CHANGED, BGP_EVENT_TYPE_DATAPLANE_ROUTE_INSTALLED, BGP_EVENT_TYPE_DATAPLANE_ROUTE_WITHDRAWN, BGP_EVENT_TYPE_DATAPLANE_ROUTE_FAILED, plus BGP_EVENT_TYPE_STREAM_LAGGED only for the per-route source (not the peerless summary source)
EVPN BGP_EVENT_TYPE_EVPN_ROUTE_ADDED, BGP_EVENT_TYPE_EVPN_ROUTE_WITHDRAWN, BGP_EVENT_TYPE_EVPN_ROUTE_BEST_CHANGED, BGP_EVENT_TYPE_STREAM_LAGGED
BFD BGP_EVENT_TYPE_BFD_SESSION_UP, BGP_EVENT_TYPE_BFD_SESSION_DOWN, BGP_EVENT_TYPE_BFD_SESSION_STATE_CHANGED, BGP_EVENT_TYPE_STREAM_LAGGED

Durable SubscribeFromEvent compatibility:

If the storage actor becomes unavailable after admission and no producer loss has been observed, the stream ends with gRPC UNAVAILABLE. Resume from the last received top-level event_id after restoring the daemon. Once the storage thread has stopped, admission itself returns UNAVAILABLE, and open streams end with DATA_LOSS because producer events are refused until restart. Allocator pass-through remains FAILED_PRECONDITION; post-admission producer loss retains DATA_LOSS precedence.

Category Compatible event types
Route BGP_EVENT_TYPE_ROUTE_ADDED, BGP_EVENT_TYPE_ROUTE_WITHDRAWN, BGP_EVENT_TYPE_ROUTE_BEST_CHANGED, BGP_EVENT_TYPE_ROUTE_POLICY_FILTERED, global BGP_EVENT_TYPE_STREAM_LAGGED
Session BGP_EVENT_TYPE_SESSION_STATE_CHANGED, BGP_EVENT_TYPE_SESSION_ESTABLISHED, BGP_EVENT_TYPE_SESSION_LOST, BGP_EVENT_TYPE_PEER_ADDED, BGP_EVENT_TYPE_PEER_REMOVED, BGP_EVENT_TYPE_PEER_ENABLED, BGP_EVENT_TYPE_PEER_DISABLED, BGP_EVENT_TYPE_MAX_PREFIX_WARNING, BGP_EVENT_TYPE_NOTIFICATION_SENT, BGP_EVENT_TYPE_NOTIFICATION_RECEIVED, global BGP_EVENT_TYPE_STREAM_LAGGED
Policy BGP_EVENT_TYPE_POLICY_CHANGED, BGP_EVENT_TYPE_OTC_ROUTE_BLOCKED, global BGP_EVENT_TYPE_STREAM_LAGGED
Dataplane BGP_EVENT_TYPE_DATAPLANE_STATUS_CHANGED, BGP_EVENT_TYPE_DATAPLANE_ROUTE_INSTALLED, BGP_EVENT_TYPE_DATAPLANE_ROUTE_WITHDRAWN, BGP_EVENT_TYPE_DATAPLANE_ROUTE_FAILED, global BGP_EVENT_TYPE_STREAM_LAGGED
EVPN BGP_EVENT_TYPE_EVPN_ROUTE_ADDED, BGP_EVENT_TYPE_EVPN_ROUTE_WITHDRAWN, BGP_EVENT_TYPE_EVPN_ROUTE_BEST_CHANGED, global BGP_EVENT_TYPE_STREAM_LAGGED
BFD BGP_EVENT_TYPE_BFD_SESSION_UP, BGP_EVENT_TYPE_BFD_SESSION_DOWN, BGP_EVENT_TYPE_BFD_SESSION_STATE_CHANGED, global BGP_EVENT_TYPE_STREAM_LAGGED

OTC_ROUTE_BLOCKED is durable Policy only. Durable STREAM_LAGGED describes a global committed-stream gap and is therefore compatible with every category. Route events are sourced from the structured RIB broadcast, including export-policy denial events (route_policy_filtered) for unicast routes present in Loc-RIB but filtered from an outbound peer; session events are sourced from the peer manager's session broadcast and cover both lifecycle transitions and metadata-only BGP NOTIFICATION sent/received events; policy events are sourced from the peer manager after a runtime policy / neighbor-set / peer-group / chain mutation is accepted and are also retained in the bounded ListPolicyEvents process-local history ring; dataplane summary events are status-row count changes from the existing ListFibRoutes and ListBlackholeDiscards snapshots. ADR-0061 per-route FIB dataplane events are emitted directly by the FIB runtime when a route is installed, withdrawn, or fails to apply, and are replayable through SubscribeFromEvent when [event_history].enabled = true (they are not replayed by ListFibRoutes). EVPN events are sourced from the RIB's EVPN best-path broadcast and are also retained in a bounded ListEvpnEvents process-local history ring. The FIB rejected count reflects surfaced status rows; high-cardinality route_limit_exceeded rows carry ListFibRoutes sampling metadata with suppressed-row totals, so the event count itself is not a global suppressed-route total. Empty category and type filters subscribe to the default route + session live stream. A non-empty type filter narrows the stream; BGP_EVENT_TYPE_POLICY_CHANGED, BGP_EVENT_TYPE_DATAPLANE_STATUS_CHANGED, an ADR-0061 per-route dataplane event type, an EVPN route event type, or a BGP_EVENT_TYPE_BFD_SESSION_* type with empty categories selects the corresponding opt-in stream, and EVENT_CATEGORY_POLICY, EVENT_CATEGORY_DATAPLANE, EVENT_CATEGORY_EVPN, or EVENT_CATEGORY_BFD selects those streams explicitly. Transport sessions send ordinary state-change lifecycle events over a bounded channel that is separate from the lossless TCP collision-coordination path, so high churn can drop observability events without risking collision handling. Live lag warnings emit for route, session, per-route dataplane, EVPN, and BFD sources, but not policy or peerless dataplane; durable lag is global. Prefix and family filters match unicast route events and per-route FIB dataplane events; session, policy, EVPN, BFD, and peerless dataplane summary events do not match requests that set prefix or afi_safi. Peer filters match route, session, EVPN, BFD, and per-route FIB dataplane events; route events also match target_peer_address for route_policy_filtered. Peer filters do not match peerless dataplane summary events. BgpEvent repeats common fields such as peer, target peer, prefix, type, and severity at the top level even when the payload also carries them so category-agnostic clients can render or filter events without unpacking the oneof.

ListSessionEvents accepts neighbor_address, lifecycle-only event_types, and limit filters. Valid event_types are the eight session lifecycle types: BGP_EVENT_TYPE_SESSION_STATE_CHANGED, BGP_EVENT_TYPE_SESSION_ESTABLISHED, BGP_EVENT_TYPE_SESSION_LOST, BGP_EVENT_TYPE_PEER_ADDED, BGP_EVENT_TYPE_PEER_REMOVED, BGP_EVENT_TYPE_PEER_ENABLED, BGP_EVENT_TYPE_PEER_DISABLED, and BGP_EVENT_TYPE_MAX_PREFIX_WARNING. Empty event_types means all eight lifecycle types. BGP_EVENT_TYPE_MAX_PREFIX_WARNING is one crossing of a configured max-prefix warning threshold (max_prefix_warning_percent, or the bound itself under max_prefix_action = "warning"): it carries EVENT_SEVERITY_WARNING, empty old and new states because nothing transitioned, and a reason naming the scope, usage, bound, and threshold percentage. It is never a teardown. NOTIFICATION sent/received types are not retained in the history ring and are rejected here with INVALID_ARGUMENT; subscribe to WatchEvents with BGP_EVENT_TYPE_NOTIFICATION_SENT / BGP_EVENT_TYPE_NOTIFICATION_RECEIVED for live NOTIFICATION metadata. Route event types are likewise rejected; use RibService.ListRouteEvents for route history. The history ring holds at most 4096 events; limit = 0 requests that full bounded daemon window, and larger values are clamped to the same ceiling. Responses contain the most recent matching events, ordered oldest-to-newest within that selected recent window.

ListPolicyEvents accepts neighbor_address, BGP_EVENT_TYPE_POLICY_CHANGED, and limit filters. Empty event_types means all policy event types, which is currently equivalent to BGP_EVENT_TYPE_POLICY_CHANGED; route, session, dataplane, and stream_lagged event types are rejected with INVALID_ARGUMENT. A peer filter matches only peer-scoped policy mutations such as neighbor import/export chain changes; global policy, neighbor-set, peer-group, and global-chain mutations are peerless and do not match a neighbor_address filter. The history ring holds at most 4096 events, is process-local, and resets on daemon restart.

ListEvpnEvents accepts neighbor_address, EVPN route-event event_types, route_type_filter, rd_filter, and limit. Valid event types are BGP_EVENT_TYPE_EVPN_ROUTE_ADDED, BGP_EVENT_TYPE_EVPN_ROUTE_WITHDRAWN, and BGP_EVENT_TYPE_EVPN_ROUTE_BEST_CHANGED; empty event_types means all three. The peer filter matches both the current and previous best-path peer so withdrawals and best-path moves away from a peer remain visible to peer-scoped dashboards. route_type_filter accepts RFC 7432 / RFC 9136 route types 1 through 5, and rd_filter uses the same display format as ListEvpnRoutes. The history ring holds at most 4096 events, is process-local, and resets on daemon restart.

Slow live-stream consumers do not block the daemon. If a WatchEvents subscriber falls behind the bounded broadcast channel, missed events are skipped and bgp_event_stream_lagged_total{service,source} records the missed count. WatchEvents emits an in-band stream_lagged warning for route, session, per-route dataplane, EVPN, and BFD sources. Policy and peerless dataplane lag is metric-only and discarded without an in-band warning. Use bgp_event_stream_subscribers{service,source} to see active stream readers and bgp_route_event_history_depth / bgp_route_event_history_capacity to understand how much recent unicast route history is available through ListRouteEvents. See docs/reference/operations.md for alerting guidance that combines these stream metrics with ADR-0064 authorization decision volume.

Unified event types:

Type Meaning
BGP_EVENT_TYPE_ROUTE_ADDED Best path for a prefix was added
BGP_EVENT_TYPE_ROUTE_WITHDRAWN Best path for a prefix was withdrawn
BGP_EVENT_TYPE_ROUTE_BEST_CHANGED Best path for a prefix changed
BGP_EVENT_TYPE_SESSION_STATE_CHANGED BGP FSM state changed; payload carries old/new state and session role
BGP_EVENT_TYPE_SESSION_ESTABLISHED FSM reached Established
BGP_EVENT_TYPE_SESSION_LOST FSM left Established; severity is WARNING
BGP_EVENT_TYPE_PEER_ADDED Peer entered the authoritative managed set; payload carries new state idle and the exact scoped peer label
BGP_EVENT_TYPE_PEER_REMOVED Peer left the authoritative managed set after session retirement; payload carries no FSM state or role and preserves the exact scoped peer label
BGP_EVENT_TYPE_PEER_ENABLED Operator enabled a configured peer
BGP_EVENT_TYPE_PEER_DISABLED Operator disabled a configured peer
BGP_EVENT_TYPE_NOTIFICATION_SENT rustbgpd sent a BGP NOTIFICATION; payload carries direction, code, subcode, description, session role, and optional RFC 9003 shutdown reason
BGP_EVENT_TYPE_NOTIFICATION_RECEIVED rustbgpd received a BGP NOTIFICATION from the peer; payload carries the same metadata
BGP_EVENT_TYPE_BFD_SESSION_UP BFD session transitioned to Up
BGP_EVENT_TYPE_BFD_SESSION_DOWN BFD session transitioned to Down
BGP_EVENT_TYPE_BFD_SESSION_STATE_CHANGED BFD session changed state; payload carries peer, state, diagnostic, and strict flag

NOTIFICATION events are metadata-only. Raw NOTIFICATION packet data remains limited to BMP peer-down handling; WatchEvents does not retain or replay it. NotificationEvent.shutdown_reason (RFC 9003) is peer-supplied free text delivered verbatim over the API — it can legally contain ANSI escape sequences as valid UTF-8, so consumers rendering it to a terminal must sanitize it; rustbgpd's own CLI and log paths already escape it.

Stream health event types:

Type Meaning
BGP_EVENT_TYPE_STREAM_LAGGED Live: source-scoped route, session, per-route dataplane, EVPN, or BFD loss. Durable: a global outbox gap compatible with every category.

Policy event types:

Type Meaning
BGP_EVENT_TYPE_POLICY_CHANGED Runtime policy, neighbor-set, peer-group, or chain mutation accepted by the peer manager. Payload carries operation, target type, target, optional peer address, and affected peer count. This is a runtime-applied audit signal; config-file persistence is a separate path.

Dataplane event types:

Type Meaning
BGP_EVENT_TYPE_DATAPLANE_STATUS_CHANGED FIB / BLACKHOLE installed, rejected, or failed status-row count changed; the legacy three-bucket summary intentionally ignores unresolved FIB rows, which are exposed by ListFibRoutes and bgp_fib_routes_unresolved
BGP_EVENT_TYPE_DATAPLANE_ROUTE_INSTALLED ADR-0061 FIB runtime successfully installed or replaced one route
BGP_EVENT_TYPE_DATAPLANE_ROUTE_WITHDRAWN ADR-0061 FIB runtime successfully removed one owned route
BGP_EVENT_TYPE_DATAPLANE_ROUTE_FAILED ADR-0061 FIB runtime failed to apply one route operation; severity is WARNING

InjectionService

Programmatic route injection and withdrawal. Injected routes appear as locally originated (peer address 0.0.0.0) and are advertised to all peers (subject to export policy).

RPC Description
AddPath Inject a route with specified attributes
DeletePath Withdraw a previously injected route
AddFlowSpec Inject a FlowSpec rule with actions
DeleteFlowSpec Withdraw a previously injected FlowSpec rule
AddEvpnRoute Inject an EVPN Type 2 (MAC/IP), Type 3 (IMET), or Type 5 (IP Prefix) route; Type 5 may be interface-less or carry an overlay-index gateway
DeleteEvpnRoute Withdraw a previously injected EVPN route by its EVPN route key

Inject an IPv4 route

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "prefix": "10.99.0.0",
    "prefix_length": 24,
    "next_hop": "10.0.0.1",
    "communities": [4259905793]
  }' \
  localhost:50051 rustbgpd.v1.InjectionService/AddPath

Inject an IPv6 route

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "prefix": "2001:db8:ff::",
    "prefix_length": 48,
    "next_hop": "fd00::1",
    "origin": 0,
    "as_path": [65001],
    "local_pref": 100
  }' \
  localhost:50051 rustbgpd.v1.InjectionService/AddPath

Optional fields: as_path, origin, local_pref, med, communities, extended_communities, large_communities, path_id.

The prefix and next_hop fields accept both IPv4 and IPv6 addresses. Prefix length is validated against the address family (max 32 for IPv4, 128 for IPv6). path_id defaults to 0 (default path) when omitted.

Withdraw a route

# IPv4
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"prefix": "10.99.0.0", "prefix_length": 24}' \
  localhost:50051 rustbgpd.v1.InjectionService/DeletePath

# IPv6
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"prefix": "2001:db8:ff::", "prefix_length": 48}' \
  localhost:50051 rustbgpd.v1.InjectionService/DeletePath

Inject a FlowSpec rule

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "afi_safi": "ADDRESS_FAMILY_IPV4_FLOWSPEC",
    "components": [
      { "type": 1, "prefix": "203.0.113.0/24" },
      { "type": 4, "value": "=80" }
    ],
    "actions": [
      { "traffic_rate": { "rate": 0.0 } }
    ]
  }' \
  localhost:50051 rustbgpd.v1.InjectionService/AddFlowSpec

Withdraw a FlowSpec rule

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "afi_safi": "ADDRESS_FAMILY_IPV4_FLOWSPEC",
    "components": [
      { "type": 1, "prefix": "203.0.113.0/24" },
      { "type": 4, "value": "=80" }
    ]
  }' \
  localhost:50051 rustbgpd.v1.InjectionService/DeleteFlowSpec

FlowSpec component lists must be non-empty, in ascending type-code order, and limited to the supported RFC 8955 / RFC 8956 unicast component set. Each injected or withdrawn rule must also encode to at most 4095 NLRI payload bytes; larger rules are rejected with InvalidArgument.

Inject an EVPN Type 2 (MAC/IP) route

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "route_type": 2,
    "rd": "65000:100",
    "ethernet_tag": 0,
    "mac": "02:00:00:aa:bb:cc",
    "ip": "10.0.0.5",
    "label": 100,
    "next_hop": "10.0.0.2",
    "route_targets": ["65000:100"]
  }' \
  localhost:50051 rustbgpd.v1.InjectionService/AddEvpnRoute

disable_vxlan_encap defaults to false — the RFC 8365 §5.1.2 VXLAN Encapsulation extended community (tunnel-type=8) is attached automatically. Set disable_vxlan_encap: true for MPLS-over-GRE deployments. The injection API supports route_type 2 (MAC/IP), 3 (IMET), and 5 (IP Prefix). Type 5 injection can use the default interface-less gateway-zero shape or an explicit overlay-index Gateway Address. Native Gate 9 Type 5 origination from [[evpn_ip_vrfs]] shipped in v0.18.0 (slice 6 PR A #77): the daemon dumps kernel routes per IP-VRF table_id, classifies them (connected/static/manual only — routes installed by other routing daemons or whose output device is the L3 VXLAN are filtered), and originates a Type 5 per surviving prefix when the IP-VRF's readiness probe says Ready. Remote Type 5 import + L3 FIB programming (kernel route + neighbor + L3VXLAN FDB) shipped in v0.18.0 (slice 6 PR B #78) through the transactional L3OwnedState model with four-phase apply ordering, Router MAC conflict detection, and foreign-state preservation; RTNLGRP_IPV4_ROUTE / RTNLGRP_IPV6_ROUTE multicast (#79) drives sub-second withdraw on tenant ip addr del. Native Type 1/4 multi-homing origination is driven by [[ethernet_segments]]; the injection API does not expose those route types yet (the RR still reflects them when received from peers).

Inject an EVPN Type 3 (IMET) route

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "route_type": 3,
    "rd": "65000:100",
    "ethernet_tag": 0,
    "ip": "10.0.0.2",
    "next_hop": "10.0.0.2"
  }' \
  localhost:50051 rustbgpd.v1.InjectionService/AddEvpnRoute

Inject an EVPN Type 5 (IP Prefix) route

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "route_type": 5,
    "rd": "65000:5000",
    "ethernet_tag": 0,
    "prefix": "10.50.0.0",
    "prefix_length": 24,
    "label": 5000,
    "next_hop": "192.0.2.10",
    "router_mac": "02:00:00:00:50:00",
    "route_targets": ["65000:5000"]
  }' \
  localhost:50051 rustbgpd.v1.InjectionService/AddEvpnRoute

By default, Type 5 injection is interface-less: ESI and Gateway IP are encoded as zero, label carries the L3VNI in the RFC 8365 VXLAN label slot, ethernet_tag must be 0, and next_hop is the VTEP loopback. Set optional gateway to inject a controller-supplied overlay-index Type 5 route with a non-zero unicast Gateway Address; the prefix, gateway, and next-hop must use the same IP family. Non-zero ESI overlay-index injection is not exposed. router_mac is required when VXLAN encapsulation is enabled (the default) and is advertised as the RFC 9135 Router MAC extended community. Omit it when disable_vxlan_encap is true. At least one route_targets entry is required for Type 5 injection.

Withdraw an EVPN route

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "route_type": 2,
    "rd": "65000:100",
    "ethernet_tag": 0,
    "mac": "02:00:00:aa:bb:cc",
    "ip": "10.0.0.5"
  }' \
  localhost:50051 rustbgpd.v1.InjectionService/DeleteEvpnRoute

The withdrawal key (route type + RD + ethernet tag + MAC + optional IP for Type 2; route type + RD + ethernet tag + originator IP for Type 3; route type + RD + ethernet_tag=0 + prefix/prefix length for Type 5) matches the EVPN route identity used by rustbgpd. Type 5 gateway is payload, not part of the local route key. Omit ip when withdrawing a MAC-only Type 2 route or the key will not match. Requests that include key fields from another route type are rejected with INVALID_ARGUMENT. Returns NOT_FOUND if no such route was previously injected.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{
    "route_type": 5,
    "rd": "65000:5000",
    "ethernet_tag": 0,
    "prefix": "10.50.0.0",
    "prefix_length": 24
  }' \
  localhost:50051 rustbgpd.v1.InjectionService/DeleteEvpnRoute

ControlService

Daemon lifecycle, health checks, and metrics.

RPC Description
CheckLiveness Empty authenticated request/response; Read tier. A response means only the gRPC handler answered, without actor readiness or topology. Outside the narrow v1 contract; older servers return UNIMPLEMENTED.
GetHealth Returns health status, uptime, active peers, total routes; core actor probes are bounded by the same 200 ms deadline as HTTP /readyz
GetMetrics Returns Prometheus metrics as text
Shutdown Initiates graceful shutdown
TriggerMrtDump Triggers an on-demand MRT TABLE_DUMP_V2 dump

Health check

GetHealth.healthy=true means the core peer-manager and RIB snapshot was obtained. It does not assert that every BGP session or the dataplane is healthy. Failure to obtain the snapshot returns an RPC error, rather than a successful healthy=false response. active_peers counts only non-stale peers observed Established; unavailable observations are excluded even if their last known state was Established. The count can therefore omit live sessions. Use ListNeighbors and its stale flag to distinguish unavailable state from an observed session state.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.ControlService/GetHealth

For a handler-liveness probe that needs only the Read tier, call CheckLiveness (an empty response) or run rbgp health --liveness, which prints alive. It does not check actor readiness.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.ControlService/CheckLiveness
rbgp health --liveness

If [global.telemetry] prometheus_addr is configured, the same HTTP listener also exposes unauthenticated probe endpoints:

curl -fsS http://127.0.0.1:9179/livez
curl -fsS http://127.0.0.1:9179/readyz

/livez only proves the process is accepting HTTP connections. /readyz returns 200 ready when PeerManager and RIB respond within 200 ms total, or 503 not ready: <reason> when either core actor is unavailable, drops its reply, or times out. During an actor-owned export-policy transition the dedicated read-only RIB lane stays healthy for bounded progress, then returns 503 not ready: RIB export-policy transition stalled if ownership reaches 30 seconds; commit or cleaned-up fallback restores readiness immediately. A selection-deferral release shares that bound and returns 503 not ready: RIB selection-deferral release stalled once it runs for 30 seconds; readiness returns when the release finishes. It does not require peers or routes to exist.

Authoritative export-policy replacement and rollback service this same RIB readiness lane between construction, per-peer work, and cleanup steps. These operations preserve Loc-RIB cardinality, so readiness replies report its exact count while ordinary route queries and mutations remain queued. Nested replacements retain the original command's 30-second transition-age limit.

Get Prometheus metrics via gRPC

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.ControlService/GetMetrics

Graceful shutdown

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"reason": "maintenance window"}' \
  localhost:50051 rustbgpd.v1.ControlService/Shutdown

Trigger MRT dump

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.ControlService/TriggerMrtDump

EvpnService

Local EVPN state and bounded controls for this VTEP. Empty read responses are normal when the daemon is acting purely as an EVPN route reflector — RR mode does not declare local instances. The same [[evpn_instances]] table that this service exposes is the input to the Linux kernel reconciler (Gate 7b, ADR-0054 — programs remote-MAC FDB entries downward), the local-MAC originator + Type 3 IMET emitter (Gate 7b+1, ADR-0055 — emits Type 2 / Type 3 routes upward from kernel-learned state), the Gate 8 segment/DF orchestrator, and the Gate 9 Type 5 / IP-VRF path. The originators and dataplane actors bypass this gRPC surface; they translate kernel/RIB events directly into reconcile inputs and RibUpdate::InjectEvpn / WithdrawEvpn against the RIB. See ADR-0052 for the original boundary, ADR-0054/ADR-0055 for the dataplane + origination boundaries, and ADR-0063 for the runtime mutation semantics used by both ApplyEvpnRuntime and SIGHUP reload.

RPC Description
GetEvpnRuntime Return the committed EVPN runtime generation, lifecycle, mutation state, configured EVI/IP-VRF/ES counts, and a concise status message
ListEvpnInstances List configured local EVPN instances sorted by VNI (vni, rd, resolved route_targets including any auto-derived RT, local_vtep_ip, optional bridge, optional local bridge_vlan, advertise_svi_mac flag, originated_local_macs_count, L2 dataplane readiness_state, and not_ready_reason when NotReady)
ListEvpnNexthops List Linux dataplane reconciler-owned ADR-0059 FDB nexthop groups (per-VNI groups with ESI / Ethernet Tag / kernel group ID, per-VTEP member nexthop IDs + gateways, MAC refs) plus top-level L2 and L3 orphan-NH and pending-delete counts and the drift_recovery_disabled latch — read-only operator visibility
ListEthernetSegments List configured Ethernet Segments sorted by ESI, joined with live multi-homing state: composed drain reasons, per-member DF role and BUM forwarding action, same-ESI local-bias eligibility, whole-port AC-gate state/interface, and matching FDB-NHG group / MAC-ref counts — read-only ADR-0083/0085 diagnose visibility
ListIpVrfs List configured IP-VRFs / L3VNI tenants (name, l3vni, rd, resolved route_targets including any auto-derived RT, local_vtep_ip, router_mac, optional evpn_instance link, readiness state, originated_routes_count, installed_routes_count, remote_prefix_drop_counts) — Gate 9 / ADR-0058
ListManagedNetdevs List configured ADR-0091 managed EVPN bridge, fixed-VNI VXLAN, SVD / collect-metadata VXLAN, VLAN upper, VRF, and L3VXLAN rows joined with the latest Linux link snapshot, plus rustbgpd-stamped orphan/unsafe rows for the configured owner. Reports class, name, desired flag, ownership stamp, state (desired-absent, owned-safe, foreign-present, owned-unsafe, orphaned, or unknown), observed ifindex, bridge vlan_filtering, VXLAN/SVD/L3VXLAN vni / local / dstport / learning / collect-metadata / vnifilter / master attributes, VLAN upper vlan, VRF table_id, L3VXLAN router_mac, observed rustbgpd ownership stamps, and reason text. Bridge, fixed-VNI VXLAN, SVD VXLAN, VLAN upper, VRF, and L3VXLAN lifecycle execution is active in the dataplane actor; this RPC remains read-only status.
GetIpVrf Detail view of a single IP-VRF including the seven readiness predicates (not_ready_reasons) when readiness_state != Ready and scoped remote Type 5 projection-drop counts
ListDuplicateMacQuarantines List at most 4096 active duplicate-MAC local-origin quarantine keys from one actor-published snapshot, ordered by VNI and raw MAC. omitted is exact and complete is true exactly when no rows were omitted.
ClearDuplicateMacQuarantine Clear one RFC 7432 §15.1 duplicate-MAC local-origin quarantine by (vni, mac). Returns cleared=false when no active quarantine exists; read-only listeners reject it.
ApplyEvpnRuntime Validate or apply a full candidate EVPN runtime model through the ADR-0063 coordinator. validate_only=true returns the plan without mutation; no-op applies succeed. Supported live changes include single L2VNI/IP-VRF/Ethernet-Segment add/delete/redefine, additive build-up, atomic tenant teardown, ip_vrf relink, and decomposable mixed edits ordered as deletes -> redefines -> ip_vrf relinks -> adds. When a segment actor already exists, L2VNI add/delete republishes the current instance table so later ES add/redefine can bind a VNI added at runtime; ES-member L2VNI redefine rebuilds the segment actor's Type 1/4 routes from the candidate instance snapshot. L3VNI/device/table IP-VRF identity changes remain restart-required by design. Unsupported dependency cycles fail closed before commit; residual mid-sequence convergence failures fail-stop after already committed generations and are surfaced by evpn_runtime_decomposed_fail_stops_total.

Instance mutation (AddEvpnInstance / DeleteEvpnInstance) remains out of scope. GetEvpnRuntime now reports the daemon-owned ADR-0063 coordinator generation. At startup this is generation 1, lifecycle active, and mutation state idle when the coordinator is available. ApplyEvpnRuntime accepts a full candidate rustbgpd TOML document so the daemon can reuse the normal config parser, validator, and EVPN table resolution. Because that TOML may contain unrelated credentials, audit logs record only its byte length and mode. Runtime mutation is not a shared-table swap: it drives the live IMET controller, the MAC-only/MAC+IP/SVI Type 2 originators, the Type 5/IP-VRF originator, and the Linux dataplane supervisor through ordered convergence commands with rollback on partial failure.

Supported non-noop shapes converge live and commit the next generation: single L2VNI/IP-VRF/Ethernet-Segment add/delete/redefine, additive build-up, atomic tenant teardown, ip_vrf relink, and decomposable mixed edits. The mixed-edit decomposer applies primitive commits in a fixed order — deletes, redefines, ip_vrf relinks, then adds — so operators may observe multiple runtime generations for one SIGHUP or ApplyEvpnRuntime request. ES add/redefine can reference a member VNI added by an earlier live L2VNI add when the segment actor is already running; ES-member L2VNI redefine rebuilds Type 4 / EAD-per-ES / EAD-per-EVI routes from the candidate instance snapshot while retaining the stable ESI label. L3VNI/device/table IP-VRF identity changes remain restart-required by design. Unsupported dependency cycles fail closed before commit. If a later primitive step or actor command fails after earlier steps committed, the sequence fail-stops there: the committed generations stay visible, the coordinator pins mutation_state=Failed, an ERROR log names the step, and evpn_runtime_decomposed_fail_stops_total increments for the mid-sequence stop.

Operators configure instances via the [[evpn_instances]] TOML block. SIGHUP reload submits EVPN table edits through the same coordinator for supported ADR-0063 shapes. Unsupported dependency cycles, L3VNI/device/table IP-VRF identity changes, or missing EVPN actors fail closed before commit. Actor convergence failures inside a decomposed sequence fail-stop on the last committed generation and keep the drift visible instead of silently advancing the config snapshot.

Get EVPN runtime status

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.EvpnService/GetEvpnRuntime

Or via CLI:

rbgp evpn runtime           # human format
rbgp evpn runtime --json    # JSON output

List local EVPN instances

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.EvpnService/ListEvpnInstances

Or via CLI:

rbgp evpn instances           # human format
rbgp evpn instances --json    # JSON output
rbgp evpn diagnose            # instance / Type 2 / Type 3 / metric summary

The human CLI includes readiness=ready|not-ready|unbound|unknown, originated-local-macs=N, and bridge-vlan=N when the instance has a local bridge VLAN binding; not-ready rows include the single L2 readiness probe reason. JSON exposes the same fields as readiness, originated_local_macs_count, not_ready_reason, and bridge_vlan (null when absent); gRPC exposes the enum as readiness_state and the local VLAN binding as optional bridge_vlan. Unbound means no bridge is configured for that L2VNI. Unknown means the instance is bridge-bound but no dataplane verdict is on file yet, usually cold start before the first reconcile report or an RR-only / dataplane-disabled deployment. originated_local_macs_count counts MAC-only Type 2 routes currently originated by this daemon for the instance and accepted by the RIB. bridge_vlan is local Linux attribution only: EVPN Type 2 / Type 3 / EAD-per-EVI routes still use Ethernet Tag ID 0. When present, it selects ADR-0089's traditional VNI-per-broadcast-domain VLAN-aware path: the probe requires vlan_filtering=1, exactly one VXLAN member for the instance VNI, and the configured VLAN on both the bridge and that VXLAN member; remote-MAC FDB rows are then programmed with NDA_VLAN. A vlan_filtering=1 bridge without bridge_vlan remains NotReady.

List EVPN FDB nexthop groups

ADR-0059 operator-visibility surface. Returns the Linux dataplane reconciler's owned FDB nexthop-group state: one row per group with VNI, ESI, Ethernet Tag, kernel group ID, per-VTEP member nexthop IDs, and MAC refs. The response also includes orphan tagged nexthop count, pending-delete count, and whether periodic drift recovery latched off after a permanent dump failure. orphan_nexthops_count and pending_delete_count cover L2 FDB-NHG nexthops; l3_orphan_nexthops_count and l3_pending_delete_count report the same state for L3 (all-active Type 5) FDB nexthops, which share the retry queue.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.EvpnService/ListEvpnNexthops

Or via CLI:

rbgp evpn nexthops          # human format
rbgp evpn nexthops --json   # JSON output

An empty groups list is normal on RR-only deployments, single-homed VTEPs, or multi-homed VNIs with apply_aliasing_ecmp = false — the top-level count fields and drift_recovery_disabled are always populated regardless.

List Ethernet Segments / multi-homing state

ADR-0083 / ADR-0085 diagnose surface. Returns one row per configured [[ethernet_segments]] entry, optionally filtered by ESI. Each row joins the committed ES config with the latest segment/dataplane snapshots: drain reasons (operator / link), DF role and BUM action per member VNI, same-ESI local-bias eligibility, the single-active whole-port AC-gate state/interface, and matching owned FDB-NHG group / MAC-ref counts. Empty runtime fields mean the segment actor or dataplane has not published that snapshot yet; the RPC itself is read-only.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"esi":"00:11:22:33:44:55:66:77:88:99"}' \
  localhost:50051 rustbgpd.v1.EvpnService/ListEthernetSegments

Or via CLI:

rbgp evpn es list
rbgp evpn es list 00:11:22:33:44:55:66:77:88:99 --json

List IP-VRFs / L3VNI tenants

Gate 9 / ADR-0058 surface. Returns one row per [[evpn_ip_vrfs]] entry with the readiness verdict the EVPN reconcile actor most recently published, plus the Type 5 origination / install counters and current scoped remote Type 5 projection-drop counts. The drop counts reuse the bounded reason labels from evpn_ip_vrf_remote_prefix_drops{vrf,reason} and omit per-route prefixes, gateways, next-hops, MACs, RDs, and RTs.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.EvpnService/ListIpVrfs

Or via CLI:

rbgp evpn vrfs                  # human format
rbgp evpn vrfs --json           # JSON output
rbgp evpn vrfs vrf1             # single-VRF detail (matches GetIpVrf)

List managed EVPN netdevs

ADR-0091 managed-netdev lifecycle/status surface. Returns configured [managed_netdevs] bridge, fixed-VNI VXLAN, SVD / collect-metadata VXLAN, VLAN upper, VRF, and L3VXLAN rows joined with the latest Linux link snapshot, plus rustbgpd-stamped orphan links observed by the dataplane actor. This is read-only status: a row can be desired-absent, foreign-present, owned-unsafe, owned-safe, orphaned, or unknown. Bridge, fixed-VNI VXLAN, SVD VXLAN, VLAN upper, VRF, and L3VXLAN rows are active lifecycle intent inside the dataplane reconciler. The RPC itself never mutates links.

Bridge rows expose observed vlan_filtering. Fixed-VNI and SVD VXLAN rows expose observed vni, local, dstport, learning-disabled, collect-metadata, vnifilter, and bridge-master fields when the Linux link snapshot reports them. VRF rows expose observed table_id and up. L3VXLAN rows expose observed vni, local, dstport, learning-disabled, collect-metadata, vnifilter, vrf master, up, and router_mac. VLAN upper rows expose observed parent bridge, VLAN id, and link-up state.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.EvpnService/ListManagedNetdevs

Or via CLI:

rbgp evpn managed-netdevs
rbgp evpn managed-netdevs --json

Get IP-VRF detail

Returns the same row as ListIpVrfs plus, when readiness_state is not Ready, the not_ready_reasons list — one entry per failing ADR-0058 §3 predicate (e.g., vrf_table_id_mismatch, l3vxlan_router_mac_mismatch). remote_prefix_drop_counts reports the current bounded receive-side Type 5 projection drops for this IP-VRF.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"name": "vrf1"}' \
  localhost:50051 rustbgpd.v1.EvpnService/GetIpVrf

Clear duplicate-MAC quarantine

Clears local-origin suppression for one (VNI, MAC) after an operator has confirmed the loop condition is gone. The RPC does not clear every quarantine and does not remove Loc-RIB/RR visibility; if the MAC is still locally present, the originator immediately replays the live MAC-only or MAC+IP state through the normal recovery path.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"vni": 100, "mac": "aa:bb:cc:dd:ee:ff"}' \
  localhost:50051 rustbgpd.v1.EvpnService/ClearDuplicateMacQuarantine

Or via CLI:

rbgp evpn clear-duplicate-mac --vni 100 --mac aa:bb:cc:dd:ee:ff
rbgp evpn clear-duplicate-mac --vni 100 --mac aa:bb:cc:dd:ee:ff --json
rbgp evpn duplicate-mac-quarantines
rbgp evpn duplicate-mac-quarantines --json

Apply EVPN runtime candidate

Validates a full candidate rustbgpd TOML document against the committed EVPN runtime model and returns a plan summary. Use validate_only=true to inspect added/deleted/redefined/unchanged EVPN instances, IP-VRFs, and Ethernet Segments — plus the ip_vrf_references_changed flag, which is the only non-empty plan signal for a pure ip_vrf relink — without changing the committed generation. A dry-run rejects exactly what a commit of the identical candidate would reject, including the actor-availability preconditions: an L2VNI add validated on an RR-only daemon (which spawns no EVPN dataplane actors) fails with the same FAILED_PRECONDITION the commit would return.

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d @ localhost:50051 rustbgpd.v1.EvpnService/ApplyEvpnRuntime <<'JSON'
{
  "candidate_toml": "[global]\nasn = 65000\nrouter_id = \"10.0.0.1\"\nlisten_port = 179\n\n[global.telemetry]\nlog_format = \"json\"\n\n[security.grpc]\nenforcement = \"tier\"\n\n[[evpn_instances]]\nvni = 100\nrd = \"65000:100\"\nroute_targets = [\"65000:100\"]\nlocal_vtep_ip = \"10.0.0.1\"\n",
  "validate_only": true
}
JSON

A non-validate_only request commits when the candidate is a no-op, a single L2VNI add, a single L2VNI delete that is not an Ethernet Segment member, a single L2VNI redefine with unchanged ip_vrf link metadata, a single IP-VRF add, a single standalone IP-VRF delete with no L2VNI links, a single IP-VRF redefine with unchanged L3VNI/device/table identity, a single Ethernet Segment add/delete/redefine, additive build-up, an atomic tenant teardown, or an ip_vrf relink against the committed model; the response carries the committed generation and outcome. L3VNI/device/table IP-VRF identity changes remain restart-required by design. Unsupported dependency cycles, an ES referencing an unknown member VNI, or an ES apply with no running segment actor are rejected with FAILED_PRECONDITION before commit; decomposable mixed edits may commit multiple generations and fail-stop on a later primitive convergence failure as described above.


BfdService

Read-only inspection of single-hop and multihop BFD sessions (ADR-0067, RFC 5880/5881/5883). Sessions themselves are configured via [[bfd_profiles]] + [neighbors.bfd] (see CONFIGURATION.md). Member attachments apply on SIGHUP; profile definitions require a restart. There is no mutating BFD RPC, and config transactions do not apply BFD attachment changes.

RPC Description
GetBfdSessions List BFD sessions (peer address, state, last diagnostic, strict flag, remote-AdminDown cause, multihop mode flag), optionally filtered to one peer_address; a filter that matches no session and names no known peer returns NOT_FOUND (unknown peers)
# All BFD sessions
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  localhost:50051 rustbgpd.v1.BfdService/GetBfdSessions

# One peer
grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"peer_address": "10.0.0.2"}' \
  localhost:50051 rustbgpd.v1.BfdService/GetBfdSessions

state is a BfdSessionState (BFD_SESSION_STATE_{ADMIN_DOWN,DOWN,INIT,UP}). remote_administrative_down is an explicit-presence boolean: true explains that the peer disabled BFD and RFC 5882 section 4.1 permits BGP even though the local BFD state remains DOWN; false means a current daemon observed no such cause. An absent field means the serving daemon predates this visibility and the cause is unknown—clients must not default absence to false. Live state-change events are available on EventService.WatchEvents with EVENT_CATEGORY_BFD (opt-in). RFC 5882 BGP coupling — strict (withhold BGP until BFD Up) and non-strict (tear BGP down on BFD-down before the hold timer) — is driven by PeerManager from these sessions; it is not exposed as a separate RPC.


RpkiService

ValidateRouteOrigin is a sensitive_read, outside-v1 diagnostic over the latest authoritative VrpTable. It accepts a bare IP address, an explicitly present prefix length, and a nonzero origin ASN. Host bits are normalized in the response. CIDR text in prefix, omitted or out-of-family prefix lengths, malformed addresses, and AS0 queries return INVALID_ARGUMENT.

rbgp rpki validate 192.0.2.129/24 64496

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{"prefix":"192.0.2.129","prefix_length":24,"origin_asn":64496}' \
  localhost:50051 rustbgpd.v1.RpkiService/ValidateRouteOrigin

The valid, invalid, or not_found verdict is computed from the complete table before diagnostic truncation. covering_vrps contains at most 256 effective duplicate-collapsed rows, authorizers first, followed by deterministic non-authorizers. complete and exact omitted describe that listing. AS0 VRPs remain visible but always have authorizes: false. Before the first authoritative snapshot the RPC returns FAILED_PRECONDITION; a received empty snapshot is authoritative and returns not_found with a complete empty list. This surface does not expose cache provenance, readiness, raw RTR data, or cache management.

ListCaches exposes configured RTR endpoints in canonical address order, current transport connectivity, and the latest atomically accepted End of Data epoch. accepted is present for an accepted empty table and absent before the first accepted epoch or after expiry/fatal flush. Protocol version, session ID, and serial are optional for compatibility with legacy embedded update producers. Counts are derived from the retained per-cache contribution, and age_seconds is monotonic whole seconds. Results are capped at 256 rows with exact complete/omitted; an unconfigured daemon returns a complete empty list. A closed actor returns UNAVAILABLE, while the two-second whole-query budget returns DEADLINE_EXCEEDED.

rbgp rpki caches

grpcurl -plaintext -import-path . -proto proto/rustbgpd.proto \
  -d '{}' localhost:50051 rustbgpd.v1.RpkiService/ListCaches

ASPA provider and path diagnostics

LookupAspa and VerifyAsPath are sensitive_read methods outside the narrow v1 contract. Each call clones the current authoritative validation snapshot once, releases the watch borrow, and uses that immutable ASPA table throughout. No snapshot epoch, per-cache provenance, or freshness claim is attached: those metadata are not present in the merged table.

rbgp rpki aspa 64497
rbgp --json rpki verify-path --role peer --neighbor-asn 64496 "64496 64497"

LookupAspa takes a nonzero customer_asn. It returns found plus at most 256 sorted, deduplicated provider_asns from the effective merged table; complete and exact omitted describe truncation. A missing customer has found: false; a present empty provider set has found: true. AS0 providers remain visible. An authoritative empty table yields a complete missing result.

VerifyAsPath takes segments of kind SEQUENCE or SET, an explicit nonzero neighbor_asn, and local_role. The role describes the receiving speaker: CUSTOMER selects downstream verification; PROVIDER, PEER, ROUTE_SERVER, RS_CLIENT, and explicit NONE select upstream verification. Only RS_CLIENT exempts the first-AS neighbor comparison. UNSPECIFIED and unknown roles or segment kinds are invalid arguments. There is no caller-controlled exemption.

The request is limited to 4096 nonzero ASNs across at most 4096 nonempty segments. An empty segment list represents an empty path. Empty paths, any AS_SET, and nonexempt first-AS mismatches produce INVALID without an invalid_hop. The validation result otherwise comes directly from the shared ASPA verifier; invalid_hop, when present, is its first proven NotProviderPlus customer/provider pair. Consecutive prepends retain the verifier's existing compression semantics. The verdict uses the complete table, independently of the provider-lookup display limit.

This is hypothetical eBGP IPv4/IPv6-unicast verification of an effective four-octet AS_PATH, after AS4 reconstruction. It does not evaluate a complete UPDATE, peer admission, other address families, or import policy. Live ingress may treat an AS_SET or first-AS mismatch as a withdrawal before reaching ASPA. An absent authoritative ASPA table returns FAILED_PRECONDITION from both methods; an authoritative empty table still runs the verifier, so structural invalidity is not hidden as UNKNOWN. Malformed or oversized requests return INVALID_ARGUMENT before table lookup. Successful diagnostic verdicts, including invalid, produce CLI exit 0; malformed CLI input exits 2 before connecting, and operational RPC failures exit 1.

The CLI accepts whitespace-separated decimal ASNs and {ASN ASN} sets in a quoted literal, with a 65536-byte text limit. Both calls use its complete 30-second unary response deadline. Add --json for machine-readable results.


Proto File

The full proto definition is at proto/rustbgpd.proto. You can generate typed clients for Python, Go, Rust, Node.js, or any language with protobuf/gRPC support.