A peer can now send several routes for one prefix, told apart by a path
identifier, and each is a route of its own in the table.
Wire:
- The ADD-PATH capability lists several address families. It was read as
exactly one, so the OPEN of a router offering it for IPv4 and IPv6 was
malformed and the session never came up.
- Prefixes in UPDATEs are `Nlri`: a prefix and a path identifier. The
identifier is on the wire, in the body and in MP_REACH/MP_UNREACH, for
the families the session uses ADD-PATH for in that direction. Sending
and receiving are settled separately, so a session has one codec context
for each.
Negotiation:
- `add_path` on a peer in the configuration: off (the default), receive,
send or both, offered for every family offered to the peer.
- In use for a family and a direction when one side offered to send and
the other to receive. Shown on /statusz and in /api/peers.
Table:
- A route is known by its peer and path identifier together. Identifiers
are stored only in rows that have a non-zero one, so routes of peers
that send one route each cost nothing more.
- Equal routes from one peer are chosen between by the lower identifier.
- /api/routes/received gives each route's path_id.
Journal:
- A second journal in each shard records every change to any route, for
followers that want all the routes of a prefix and not only the best:
RibTable::reader_of_all_routes and Event::Routes. This is what sending
several routes to a peer will read. It costs nothing while nobody
follows it.
Sending: prefixes go out with a path identifier where that was agreed, but
there is still one route for each prefix to send: only originated routes
are announced.
Tested with the table and journal model tests extended to path identifiers
and to followers of all routes, the codec property tests and fuzz targets
extended to ADD-PATH sessions, and end to end in both directions and for
the case where only one side offers it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Any malformed UPDATE used to reset the session, withdrawing every route
learned over it. One bad attribute, typically originated ASes away and
passed on by a peer that did not look at it, could take down a session
carrying a full table.
UpdateMessage::decode now applies RFC 7606 and returns the UPDATE as it is
to be acted on, plus a report of what was wrong:
- Treat-as-withdraw: a malformed or misflagged attribute, a missing
mandatory one, or an attribute that overruns the attribute list. The
routes the UPDATE announced (in the body and in MP_REACH_NLRI) are
withdrawn.
- Attribute discard: malformed ATOMIC_AGGREGATE, AGGREGATOR, AS4_PATH and
AS4_AGGREGATOR; an attribute sent twice (the first is kept); LOCAL_PREF,
ORIGINATOR_ID and CLUSTER_LIST from an external peer; AS4_PATH between
four-octet speakers.
- Session reset remains where the prefixes are in doubt: the lengths that
frame the message, an unreadable prefix, a malformed or repeated
MP_REACH_NLRI or MP_UNREACH_NLRI, an unrecognised well-known attribute.
Each now sends its own subcode instead of always Malformed Attribute
List.
The session logs each handled error and counts it in
bgp_update_errors_total{handling}. The parser context says whether the
peer is internal (replacing a field nothing read).
Stricter than before, because the checks did not exist:
- An announcement must carry ORIGIN and AS_PATH, NEXT_HOP for routes in the
body, and LOCAL_PREF from an internal peer.
- An AS_PATH segment must be an AS_SET or AS_SEQUENCE and not empty.
- An attribute value must be used up entirely by its parser; a NEXT_HOP of
six octets, say, was accepted before.
Bugs found on the way: we sent ATOMIC_AGGREGATE flagged optional and
AGGREGATOR flagged non-transitive. The flags now come from one table used
for both writing and checking.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Property tests (run by `cargo test`):
- crates/packet/tests/properties.rs: every kind of message, generated,
must be written, be as long as wire_len says, and read back equal; and
noise, noise behind a valid header, and valid messages with bytes damaged
must never do more than return an error. Anything the decoder accepts
must be showable and survive being written and read again.
- IpPrefix: text round trip, noise as text, `contains` against a bitwise
model.
- The session's framing: a stream of messages yields the same messages
however it is cut into reads.
Fuzz targets (fuzz/, cargo-fuzz on nightly, a minute each in CI): raw
messages, message bodies behind a valid header, prefix text, the
configuration file.
Bugs found, each with a named regression test:
- A prefix longer than its address family (e.g. an IPv4 NLRI of length 99)
was accepted, and showing it panicked. The session logs every route, so
one UPDATE from a peer ended the session task. Such a prefix is now
malformed, and showing a prefix cannot fail.
- Bits past a prefix's length were kept, so the same prefix sent with stray
bits was a different key in the RIB. Prefixes now have one form.
- An opaque extended community was read as seven octets instead of eight,
putting every community after it out of step.
- AGGREGATOR could be neither read from nor written to a peer with
two-octet AS numbers.
- The Outbound Route Filter and Long-Lived Graceful Restart capabilities
were written differently from how they are read.
- A capability value or unknown attribute too long for its length field
was written with the length wrapped around; it is now an error, or for
the attribute the two-octet length.
Also: ROUTE-REFRESH carries its AFI and SAFI instead of being modelled as
an empty message, and one with a body of the wrong length is refused.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>