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>
25 lines
886 B
Rust
25 lines
886 B
Rust
//! A message body behind a correct header, so that the fuzzer spends its
|
|
//! time in the OPEN, UPDATE and NOTIFICATION parsers and not on guessing
|
|
//! the marker and length. The first byte picks the AS number width, the
|
|
//! message type and whether the peer is internal.
|
|
#![no_main]
|
|
|
|
use libfuzzer_sys::fuzz_target;
|
|
|
|
/// The longest body that fits a BGP message (4096 less the 19-byte header).
|
|
const MAX_BODY: usize = 4096 - 19;
|
|
|
|
fuzz_target!(|data: &[u8]| {
|
|
let Some((selector, body)) = data.split_first() else {
|
|
return;
|
|
};
|
|
let body = &body[..body.len().min(MAX_BODY)];
|
|
let msg_type = 1 + (selector >> 1) % 5;
|
|
|
|
let mut wire = vec![0xff; 16];
|
|
wire.extend_from_slice(&((19 + body.len()) as u16).to_be_bytes());
|
|
wire.push(msg_type);
|
|
wire.extend_from_slice(body);
|
|
bgp_fuzz::decode_and_check(selector & 1 == 1, selector & 0x80 != 0, &wire);
|
|
});
|