speaker-cleanup
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a8aba52562 |
Review pass on the codec and the table: bugs fixed, encoders simplified
A review of the wire codec and the routing table, with the fixes. Bugs in the codec: - A NOTIFICATION with a code or subcode we have no name for (subcode 0, "unspecific", is common) failed to decode and was answered as a bad message. Every pair now reads, as BgpError::Other. - Optional parameters in the extended form of RFC 9072 were read and written with one-octet parameter lengths; they are two. - An ORF capability listing more than one address family made the whole OPEN malformed. - Bytes after an OPEN's optional parameters were accepted. - Extended communities were split into fields by rules that were wrong for the common types. They are kept as their eight octets. - Several length fields were computed in u16 and could wrap on encode. Simplifications in the codec: - Lengths are filled in after the value is written, for messages, attributes, capabilities and the UPDATE's two fields. The parallel wire_len functions, which had to be kept in step by hand, are gone. Message::to_wire no longer encodes into a second buffer. - Type codes for messages, attributes and capabilities are each defined once. The strum discriminant enums and their hand-written conversions are removed. - One function splits an attribute off a buffer; the UPDATE decoder, the plain attribute parser and the table's key reader all use it. - The UPDATE decoder's 185-line loop is an AttributeReader with one method that decides what becomes of an attribute. Duplicate detection is a bitset, not a HashSet allocated per UPDATE. - ParserContext::four_octet_asn is a bool. The "context not set" error path, which treated a programming error as a malformed route, is gone. - Codec functions take a context only if they read it, and return a Result only if they can fail. - MP_REACH_NLRI and MP_UNREACH_NLRI hold an AfiSafi. - AddressFamilyId is IpVersion, which is what it was. - Deserialize is no longer derived on codec types: nothing used it, and it bypassed IpPrefix's invariants. IpPrefix::to_wire, which was broken and unused, is removed. Bugs in the table: - The index of a row's best route was sixteen bits. A peer using ADD-PATH could send more routes for a prefix than that counts, and the index wrapped. It is 32 bits, at no cost in row size. - Shards and attribute-store stripes were picked from the top bits of the hash, which are the bits hashbrown keeps as the tag it compares first. With 64 shards each shard's table had 2 of 128 tag values. They are now picked from bits the table does not use. - remove_peer held the peer list's lock for the whole scan of the table, so no session could establish while a peer was removed. - When more prefixes went at once than were left, the forced trim dropped a fixed few entries, reset every reader, and re-ran the tidy on every later write. It drops the older half. - Readers were woken by every change, including a repeated announcement and changes their journal does not record. - Reader::poll with a budget of zero never made progress. Simplifications in the table: - The two journals of a shard are one Journals value written through three calls; the five places that kept them in step by hand are gone. Whether an entry is still live is decided in one place. - The shortcuts of route selection moved to select.rs, next to the invariant they depend on, which is now written down. - announce and withdraw take routes with or without path identifiers; the _paths variants are removed. - The selection key is read off the wire form only; the second implementation over parsed attributes is removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d49bab58a5 |
Add ADD-PATH (RFC 7911): several routes for a prefix from one peer
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> |
||
|
|
31aa3a4563 |
Use the sharded table in the server: sessions choose best routes
The server now keeps what it learns in table::RibTable. The Adj-RIB-In actor is gone, and with it the queue every UPDATE from every session went through. - A session takes a number in the table when it is established and writes its UPDATEs into the table itself. Its routes are removed when it ends, on a blocking thread since that looks at every prefix; a session torn down without cleaning up still has them removed. - For every prefix the best route is chosen (RFC 4271 §9.1.2.2) as routes come and go. Nothing acts on the choice yet beyond showing it. - The UPDATE decoder hands over the bytes of the attributes it kept, so the table finds the attribute set from them without writing the attributes out again. On a session with two-octet AS numbers the bytes are not in the table's form, and the parsed attributes are used. - /api/routes/received marks the best route of each prefix and returns at most `limit` routes (default 1000): the table can hold hundreds of millions. /api/status has the totals, and the status page says how many it is showing. The time a route was received is no longer kept or shown. - The event stream reports changes of best route (best_changed, best_withdrawn) in place of every route received. Each client follows the table's journal with a reader of its own, from the moment it connects: table::RibTable::reader_from_now. - Metrics: bgp_rib_routes, bgp_rib_prefixes, bgp_rib_attribute_sets and bgp_rib_journal_entries replace bgp_adj_rib_in_routes. - A line is no longer logged at INFO for every UPDATE sent or received; a full table is a million of them. They are at DEBUG. - A table with no room for another peer ends the session with Cease / Out of Resources. Four peers sending a synthetic full table each over TCP, 4.9 million routes: 0.80 s, against 4.76 s before. One peer: 0.45 s against 0.69 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
6197a7ad74 |
Handle errors in UPDATE messages as RFC 7606 says, not by resetting
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>
|
||
|
|
39632e3c38 |
Property tests and fuzz targets for the parsers, and the bugs they found
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> |