Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,67 @@ validity states.

The `e2e-flowspec` GitHub workflow runs both scripts on demand
(workflow_dispatch).

## BMP reconciliation simulations

`python3 scripts/bmp-peer-churn.py` builds and runs isolated real daemons, then
acts as a BMP exporter over loopback TCP. No FRR, containers, external data, or
third-party Python packages are required. It exercises the automatic recovery
for [issue #11](https://github.com/FastNetMon/netom/issues/11).

```sh
python3 scripts/bmp-peer-churn.py --binary target/debug/netom
python3 scripts/bmp-peer-churn.py --binary target/debug/netom --cycles 20
```

The exporter keeps an independent list of actually established peers. After a
collector-initiated disconnect, it reconnects and replays only those peers and
their routes, including End-of-RIB. Retired identities are not replayed.

Seven scenarios run by default; select one with `--scenario NAME`:

- `matching`: correctly addressed Peer Down, ordinary cleanup control.
- `mismatched`: Peer Down with `::`, then a new link-local peer address.
- `missing`: a new peer address with no preceding Peer Down.
- `parallel`: legitimate parallel peers coexist in startup; adding another
later causes one reconciliation, and all three survive the new snapshot
beyond its startup window without another anomaly reset.
- `silent`: a peer vanishes without any further BMP message; periodic renewal
removes it after the authoritative empty replay.
- `partial`: the same disappearance with a stalled, incomplete BMP frame.
- `limit`: startup churn reaches the peer-state cap; reconnect restores the
sole actually established peer.

The script shortens reconciliation timings in the test configuration and GC
intervals in the test daemon's environment. It verifies current peer addresses,
retained peer entries and route records after GC, then checks complete cleanup
when the exporter closes. The table is constant-size during churn. A timeout,
missing metric, failed replay, unexpected parallel-session reset, or retained
state makes the script exit 1; success exits 0. These are object-count checks,
not RSS thresholds: allocator caching can retain pages after objects are freed.

Rust regressions are part of the normal suite:

```sh
cargo test --lib units::bmp_tcp_in
```

They cover deadline coalescing, startup replay, periodic jitter, capacity,
configuration validation, peer identity scopes, policy/ADD-PATH teardown,
idle/partial transport cancellation, and active-connector redial. Live network
tests require permission to bind loopback sockets.

See [automatic reconciliation](docs/bmp-tcp-in.md#automatic-reconciliation-and-stale-peer-retention)
for the RFC 7854 references, defaults, and monitoring-gap/replay caveat.

Validation on 2026-10-02 (implementation `0ec9bd6`, regressions `a816a12`):

- `cargo test --lib --offline`: 354 passed, 31 ignored, no failures.
- `cargo build --bin netom --offline`: passed (existing unused `set_rib` warning).
- `python3 scripts/bmp-peer-churn.py --binary target/debug/netom --cycles 20`:
all seven scenarios passed. Each churn scenario ended with one peer and one
record after GC; the parallel replay retained all three peers/records; silent
and partial-frame recovery reclaimed all obsolete peers/records; capacity
recovery restored the one current peer. Every final disconnect cleaned up.
- Changed Rust files passed rustfmt checks; the Python driver passed syntax
compilation; `git diff --check` passed.
92 changes: 91 additions & 1 deletion docs/bmp-tcp-in.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,99 @@ connect, handshake, idle reads and retry delays. Downstream backpressure can
still delay cleanup; the daemon's shutdown/drain rules apply.

The existing filtering and tracing settings apply to active input as well.
`ignore_post_policy_routes` and `forward_raw_updates` changes apply to the next
`ignore_post_policy_routes`, `forward_raw_updates`, and `reconciliation` changes apply to the next
connection. Listening address changes preserve existing accepted connections.

## Automatic reconciliation and stale-peer retention

Reconciliation is enabled for every BMP input, for both `listen` and `connect`.
No opt-in is needed. The former experimental `recover_unnumbered_peers` heuristic
has been replaced: Netom does not infer that a shared BGP identifier authorizes
it to delete another peer.

An exporter can omit Peer Down or send it with an address that no longer matches
Peer Up. [Issue #11](https://github.com/FastNetMon/netom/issues/11) describes FRR
reporting `::` after losing an unnumbered peer's old link-local address. The
collector otherwise retains that peer as connected, including all of its routes.
GC cannot reclaim state that still appears live.

Netom repairs the observation by closing **that speaker's BMP transport** and
rebuilding from a fresh snapshot on reconnection. It uses normal disconnect
processing to withdraw every peer, policy view and ADD-PATH child owned by the
connection. Their disconnected register entries and records are reclaimed by
normal RIB GC if they are not rebound. Other speakers' state is not withdrawn.

This follows [RFC 7854 §3.2](https://www.rfc-editor.org/rfc/rfc7854.html#section-3.2):
a collector restarts monitoring by dropping the TCP connection. Under
[§3.3](https://www.rfc-editor.org/rfc/rfc7854.html#section-3.3), reconnection starts
with Initiation, Peer Up for each established monitored peer, and its current
Adj-RIB-In. [§5](https://www.rfc-editor.org/rfc/rfc7854.html#section-5) describes
that snapshot and per-peer End-of-RIB markers. BMP does not provide a collector
request to refresh one peer, or a global marker proving all peers have been
listed.

Refresh triggers and default bounds:

| Condition | Behavior |
| --- | --- |
| Accepted Peer Down cannot match a logical peer (ignoring policy flags) | Schedule reconciliation, no earlier than 60 seconds after connection start. Repeated anomalies do not postpone it. |
| A new link-local address has the same ASN, BGP identifier, peer type and distinguisher as an existing link-local peer | After the startup window, reconcile the whole speaker; do not guess which session is obsolete. |
| Peer Ups arrive within the first 300 seconds | Accept parallel identities as part of startup/replay. Possible-replacement detection is suppressed; unmatched Peer Down and capacity protection still apply. |
| No detectable anomaly, including a completely silent connection | Renew every 6 hours, plus independently selected 0–10% jitter. This bounds the age of an unreconciled observation. |
| The connection reaches 65,536 FSM peer states, including policy views | Close immediately, even within the minimum session lifetime. This is a per-connection peer-state limit, **not a total process-memory or route-count limit**. |

The first anomaly after the minimum lifetime can close the connection immediately.
Anomalies early in a new connection are coalesced until that minimum. Replay
collisions within the startup window do not cause a new reset, so legitimate
parallel sessions in an ordinary fresh snapshot survive. A timer also interrupts
an idle transport or a partial BMP frame; a cancelled partial read is discarded
with its transport and is never resumed.

The defaults can be tuned without disabling reconciliation:

```toml
[units.bmp-in]
type = "bmp-tcp-in"
listen = "0.0.0.0:11019"
reconciliation = { interval_secs = 21600, min_session_secs = 60, replay_grace_secs = 300, max_peer_states = 65536 }
```

Validation requires `0 < min_session_secs <= replay_grace_secs < interval_secs
<= 604800` and `max_peer_states > 0`. The maximum interval is seven days before
jitter. Changes take effect on the next BMP connection. Unmatched messages
rejected by a Roto filter do not trigger anomaly reconciliation; periodic and
capacity guards remain active.

### Operational caveat

This recovery has a cost: it withdraws the speaker's routes from Netom and its
downstream consumers until replay restores them. It does **not** reset the
router's BGP sessions, but it causes a monitoring gap and a full-table replay,
which consumes exporter CPU, bandwidth, and collector work. Consumers may see
withdrawal/reannouncement events. This is not an atomic or gap-free snapshot
replacement, and downstream backpressure can delay withdrawal/GC processing.

For passive inputs, the exporter must reconnect. For active inputs, Netom's
normal retry/backoff reconnects. Replay depends on the exporter supplying a
current snapshot; an incremental-only exporter cannot restore missing routes,
and an exporter that replays obsolete peers cannot be corrected by reconciliation.
No timeout can distinguish an idle live peer from a dead peer whose Peer Down
was never sent, so Netom never deletes a peer merely for having no updates.
A missed disappearance during startup may remain visible until periodic renewal.

The startup window is a conservative time allowance, not proof that replay has
finished. Size it for exporters with slow or very large initial dumps; parallel
Peer Ups arriving after it can cause an additional refresh. A legitimate feed
above the peer-state limit will repeatedly hit that limit until capacity is
raised or the feed is split. The hard limit overrides cooldown; the active
connector/exporter's reconnect backoff must still be respected. Neither the
peer limit nor periodic renewal promises a fixed byte limit for all collector
memory. Physical reclamation also waits for the normal RIB GC sweeps.

Each refresh logs its reason and speaker address. `scripts/bmp-peer-churn.py`
exercises current-state replay, disappearance, parallel peers, partial frames,
and capacity protection; see [TESTING.md](../TESTING.md).

## History and initial data

TCP and TLS use the same BMP parser, peer identities, ADD-PATH handling and
Expand Down
Loading
Loading