bmp-in: record why a monitored router's BGP session went down - #13
Draft
amtypaldos wants to merge 4 commits into
Draft
amtypaldos wants to merge 4 commits into
amtypaldos wants to merge 4 commits into
Conversation
A router sends a Peer Down Notification (RFC 7854 §4.9) when one of its own BGP sessions goes down. Besides the per-peer header, it carries a reason code and, depending on it, the BGP NOTIFICATION the router sent (reason 1) or received (reason 3), or the FSM event that closed the session (reason 2). peer_down() read only the per-peer header and threw the rest away, so a BMP-monitored peer could only ever be shown as Idle: a max-prefix teardown, an operator shutdown and a hold timer expiry all looked the same. Decode the message into a PeerDownInfo (time, reason, NOTIFICATION code and subcode, RFC 8203/9003 shutdown communication, FSM event, and a one-line description in the same Debug wording as bgp-tcp-in's last_error) and store it as `last_down` on every view of the peer while marking it Disconnected. The record survives the next PeerUp, because update_info only merges fields that are set; a synthesized view does not inherit it from the view it was copied from. /api/v1/ingresses shows the structured record; /api/v1/bgp/neighbors fills `lastError` and a new `lastDownTime` for BMP-monitored peers. The reason code is read from the message itself because routecore's PeerDownReason has no numeric value and folds RFC 9069's code 6 into Unknown, and NOTIFICATION data is bounded by the NOTIFICATION's length because routecore's data() runs to the end of the BMP message. Limits: routers only report sessions that reached Established, so a session that never comes up (e.g. bad peer AS) is not covered, and the record goes away with the ingress when the rib GC reaps a peer that never returns. Test: decoding of every reason, shutdown communication edge cases, the record on every view, kept across PeerUp, not inherited by synthesized views; full library suite 357 passed, 31 ignored. Signed-off-by: Alex Typaldos <alex@supertrace.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`show ip bgp neighbors` already printed `Last error` for sessions netom terminates itself. BMP-monitored peers now carry the same field, filled from the router's Peer Down Notification, plus `lastDownTime`; show the latter as `Last down: <time> (<age> ago)`, reusing bmp.rs's uptime_from(), so an operator can tell a session that dropped a minute ago from one that has been down for a week. The neighbor and ingress fixtures gain a Disconnected BMP peer that went down on a Cease Administrative Shutdown with a shutdown communication. Test: netom-cli suite 115 passed. Signed-off-by: Alex Typaldos <alex@supertrace.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add bmp_state_num_peer_down_notifications, a per-router counter of BMP Peer Down Notifications labelled with the RFC 7854 §4.9 reason (localNotification, remoteNotification, localFsm, ...). It is bumped once per message, not once per view of the peer it takes down, and a Peer Down for a peer that was never up is rejected before it counts. Every reason is emitted, zero included, so rate() works from the first scrape and an alert on rising localNotification (a router tearing sessions down, typically on a prefix limit) needs no special casing. The label set is the fixed list of reasons, so cardinality stays bounded. Test: counted per reason, unknown peer not counted; full library suite 358 passed, 31 ignored. Signed-off-by: Alex Typaldos <alex@supertrace.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feeder's Peer Down now carries reason 3 with a Cease / Administrative Shutdown NOTIFICATION and an RFC 8203 shutdown communication instead of reason 4, and the driver asserts that netom records it: `last_down` on the session in /api/v1/ingresses (reason, code/subcode, shutdown communication, a time stamped by netom since the feeder sends a zero timestamp) and `lastError` / `lastDownTime` in /api/v1/bgp/neighbors. The existing "Peer Down emitted exactly once" checks on the bmp-out side are unchanged. Test: e2e-addpath-bmp: OK. Signed-off-by: Alex Typaldos <alex@supertrace.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Remote-controlled terminal text is unsanitized, and the metric omits parsed notifications contrary to its documented semantics.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Records and exposes BMP Peer Down reasons across APIs, CLI output, metrics, and documentation.
Changes:
- Decodes and persists Peer Down reason details and timestamps.
- Exposes details through neighbor APIs, CLI output, and metrics.
- Adds unit, fixture, and end-to-end coverage.
| File | Description |
|---|---|
test-data/cli/ingresses.json |
Adds structured Peer Down fixture data. |
test-data/cli/bgp-neighbors.json |
Adds a disconnected BMP neighbor fixture. |
src/units/bmp_tcp_in/state_machine/tests.rs |
Tests persistence, views, and metrics. |
src/units/bmp_tcp_in/state_machine/status_reporter.rs |
Increments per-reason counters. |
src/units/bmp_tcp_in/state_machine/peer_down.rs |
Decodes Peer Down details. |
src/units/bmp_tcp_in/state_machine/mod.rs |
Registers the decoder module. |
src/units/bmp_tcp_in/state_machine/metrics.rs |
Defines and exports the counter. |
src/units/bmp_tcp_in/state_machine/machine.rs |
Records reasons during peer teardown. |
src/units/bgp_tcp_in/http_ng.rs |
Exposes reason and time in neighbor APIs. |
src/tests/util.rs |
Adds BMP/BGP notification builders. |
src/ingress/register.rs |
Stores persistent Peer Down information. |
src/bin/netom-cli/commands/bmp.rs |
Shares timestamp-age formatting. |
src/bin/netom-cli/commands/bgp.rs |
Renders last error and down time. |
scripts/e2e-addpath-bmp.sh |
Documents expanded E2E scope. |
scripts/e2e-addpath-bmp.py |
Verifies API Peer Down output. |
docs/rib-query-api.md |
Documents new API fields. |
docs/cli.md |
Documents CLI output. |
docs/bmp-tcp-in.md |
Documents Peer Down handling and metrics. |
doc/netom-cli.1 |
Updates the CLI manual. |
Changelog.md |
Announces the feature. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+726
to
+729
| self.status_reporter.peer_down_notification( | ||
| self.router_id.clone(), | ||
| last_down.reason, | ||
| ); |
Comment on lines
+89
to
+91
| Some(text) => format!( | ||
| "{side} NOTIFICATION: {details:?} \"{text}\"" | ||
| ), |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
When one of a monitored router's own BGP sessions goes down, the router sends a
BMP Peer Down Notification (RFC 7854 §4.9) with a reason code and, depending on
the reason, the BGP NOTIFICATION it sent or received, or the FSM event that
closed the session. netom read only the per-peer header and discarded the rest.
This PR records it:
/api/v1/ingresses: each view of the peer (pre-/post-policy) gets astructured
last_downwithtime,reason/reason_code,notification_code/notification_subcode,shutdown_communication(RFC 8203/9003),
fsm_eventand a one-linedescription./api/v1/bgp/neighbors: BMP-monitored peers getlastError(thedescription, in the same
{:?}-of-Detailswording that bgp-tcp-in alreadyuses for native sessions) and a new
lastDownTime.netom-cli show ip bgp neighbors: showsLast errorandLast down: <time> (<age> ago)for those peers./metrics: newbmp_state_num_peer_down_notifications{router,reason}counter, one per Peer Down message. Every reason is emitted so
rate()worksfrom the first scrape.
The record is kept after the peer comes back up (
update_infoonly merges setfields and a PeerUp never sets it), so you can still see why a session last
dropped. A synthesized view does not inherit it from the view it was copied
from.
Live, from a gobgp router whose transit shut the session:
Why
Without this, every BMP-monitored session that drops is just
Idle: amax-prefix teardown, an operator shutdown and a hold-timer expiry look the same,
and finding out which means logging into the router. The router already tells
the collector; netom only had to keep it.
Two notes on routecore (no changes needed there):
PeerDownReasonhas nonumeric value and maps RFC 9069's code 6 to
Unknown.NotificationMessage::data()runs to the end of the BMP message.Out of scope, by nature of BMP: a session that never reaches Established (e.g.
an OPEN rejected for a bad peer AS) is never reported by the router, so it
doesn't appear. Documented in
docs/bmp-tcp-in.md.Validation
python3 pkg/check-build-sources.py: OKpython3 -B pkg/test-release-metadata.py: OKcargo build --locked: OKcargo test --locked --lib: 358 passed, 31 ignored, 0 failed (was 345passed on main)
cargo test --locked --bin netom-cli: 115 passedscripts/e2e-addpath-bmp.sh(extended to send reason 3 with a Cease /Administrative Shutdown NOTIFICATION and assert
/ingressesand/bgp/neighbors): OKpython -m sphinx -n -W --keep-going -b html docs docs/_build/html: OKcargo clippy --lib --bin netom-cli: no new warningsrustfmt --check: no new diffs in touched hunks (several touched files werealready not fmt-clean on main; those hunks are left alone)
git diff --check: cleanpkg/Dockerfile+Dockerfile(smoke test OK),fed by a gobgp 4.9 router over BMP:
neighbor disableon the far side)./bgp/neighborsreportslastError: remote NOTIFICATION: Cease(AdministrativeShutdown)andlastDownTime;/ingresseshaslast_downwith reason 3 and code/subcode 6/2.netom-clishows bothlines, and the counter shows
reason="remoteNotification".admin-down, and its BMP encoder reports that as reason 4 rather than
reason 1 with Cease / Maximum Prefixes, so netom shows
remote closed without NOTIFICATION. That's what the router sent. The reason 1 +Cease(MaximumPrefixesReached)path is covered by unit tests.Unrelated, seen while testing on macOS: on main,
units::bmp_tcp_in::transport::tests::tls_handshake_has_a_deadlinesometimeshangs indefinitely in a full
cargo test --librun (paused tokio clock withreal TCP). It reproduced on unmodified
main; it passes when run alone.Not in this PR
bmp-tcp-outstill sends a hard-coded reason 4 when it restreams a PeerDown; it could forward the recorded reason and NOTIFICATION.
bgp-tcp-insessions'last_errordoesn't include the RFC 8203shutdown communication; the decoder here could be shared.