Harden import, the kernel task and the RTR client after review
Three independent reviews (hijack resistance, reliability, code health) of the whole tree. This is what they found that could let a bad route in or take the daemon down. Hijacks: - A route had two paths on a two-octet session, AS_PATH and AS4_PATH, read in two places by two rules. A peer could make the origin checked on import differ from the one checked at revalidation, and get an RPKI-invalid route into the table. AS4_PATH is now merged into AS_PATH once, on receipt (RFC 6793 §4.2.3), and nothing else knows of it. - The next hop is judged on import: it must be usable, agreed on (RFC 8950), and by default the peer itself. - Our own originated prefixes are not taken from other networks. - A route for a network the machine is on is never put in the kernel. - Paths with an AS_SET are refused (RFC 9774). - A configured peer gets one session, not one per BGP Identifier. - More IPv6 special-purpose prefixes are bogons. Reliability: - A batch of requests the kernel refused could overflow the netlink socket and end the daemon. The socket has room now, a lost answer or a silent kernel means asking what is there, and neither is fatal. - Working the routes out again within the grace period after a restart no longer drops the routes being kept. - That work can no longer be put off for ever by interface changes. - Peers whose sessions keep failing are slowed down. - Unused attribute sets are swept when they pile up, not only each minute. - Log lines a peer can cause are said once in a while. - An RTR answer is bounded in size and time; data cannot expire between two refreshes. - The shard of a key is taken from a stirred hash: neighbouring bits of the hash proved alike for prefixes in a row. The test that showed it, failing one run in sixteen, is deterministic now. The lab has a phase, hijacks, in which FRR tries the first three. route::apply is a struct with methods instead of nested closures, and the kernel task's loop is shorter. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd2248feb5
commit
9ca86ede2f
34 files changed
+1696
-470
No files matched your search
@@ -59,10 +59,6 @@ fn scan(mut wire: &[u8]) -> Option<SelectionKey> {
|
||||
origin: 2,
|
||||
origin_as: 0,
|
||||
};
|
||||
// The origin as AS4_PATH has it, if there is one: on a session with
|
||||
// two-octet AS numbers the path's own last AS may be a stand-in
|
||||
// (RFC 6793).
|
||||
let mut wide_origin = None;
|
||||
let number = |value: &[u8]| Some(u32::from_be_bytes(<[u8; 4]>::try_from(value).ok()?));
|
||||
while !wire.is_empty() {
|
||||
let (attribute, rest) = split_attribute(wire).ok()?;
|
||||
@@ -76,15 +72,11 @@ fn scan(mut wire: &[u8]) -> Option<SelectionKey> {
|
||||
key.neighbor_as = path.first;
|
||||
key.origin_as = path.last;
|
||||
}
|
||||
attr_type::AS4_PATH => wide_origin = Some(scan_as_path(value)?.last),
|
||||
attr_type::MULTI_EXIT_DISC => key.med = Some(number(value)?),
|
||||
attr_type::LOCAL_PREF => key.local_pref = Some(number(value)?),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(origin) = wide_origin {
|
||||
key.origin_as = origin;
|
||||
}
|
||||
Some(key)
|
||||
}
|
||||
|
||||
@@ -198,35 +190,20 @@ impl AttrSet {
|
||||
/// origin's last, sets and all. Returns whether the path has a set in
|
||||
/// it.
|
||||
///
|
||||
/// This is the path as it really is: where AS4_PATH has the numbers
|
||||
/// that a two-octet session could not carry, they are in their places
|
||||
/// This is the whole path: what a session with two-octet AS numbers
|
||||
/// brought in AS4_PATH was put in its place when the UPDATE was read
|
||||
/// (RFC 6793 §4.2.3). It reads the stored bytes and builds nothing
|
||||
/// else, for those who ask it of every route.
|
||||
pub fn as_path(&self, out: &mut Vec<u32>) -> bool {
|
||||
out.clear();
|
||||
let (mut path, mut wide): (&[u8], &[u8]) = (&[], &[]);
|
||||
let mut wire = &*self.wire;
|
||||
while let Ok((attribute, rest)) = split_attribute(wire) {
|
||||
match attribute.type_code {
|
||||
attr_type::AS_PATH => path = attribute.value,
|
||||
attr_type::AS4_PATH => wide = attribute.value,
|
||||
_ => {}
|
||||
if attribute.type_code == attr_type::AS_PATH {
|
||||
return read_as_path(attribute.value, out);
|
||||
}
|
||||
wire = rest;
|
||||
}
|
||||
let mut has_set = read_as_path(path, out);
|
||||
let narrow = out.len();
|
||||
has_set |= read_as_path(wide, out);
|
||||
let wide = out.len() - narrow;
|
||||
if wide <= narrow {
|
||||
// AS4_PATH is the path's tail; what comes before it is
|
||||
// AS_PATH's.
|
||||
out.drain(narrow - wide..narrow);
|
||||
} else {
|
||||
// An AS4_PATH longer than the path is not used.
|
||||
out.truncate(narrow);
|
||||
}
|
||||
has_set
|
||||
false
|
||||
}
|
||||
|
||||
/// The attributes, parsed, and the next hop if it came in
|
||||
@@ -569,29 +546,11 @@ mod tests {
|
||||
assert!(!set.as_path(&mut path));
|
||||
assert_eq!(path, [3333, 3333, 1299]);
|
||||
|
||||
// A two-octet path whose tail AS4_PATH has in full.
|
||||
// A set is said to be there.
|
||||
let sequence = |path: Vec<u32>| AsPathSegment {
|
||||
ordered: true,
|
||||
path,
|
||||
};
|
||||
let mut narrow = attributes(vec![3333, 23456, 23456]);
|
||||
narrow.push(PathAttribute::As4Path(AsPathAttribute {
|
||||
segments: vec![sequence(vec![210036, 4_200_000_001])],
|
||||
}));
|
||||
let set = store.intern(&narrow, None).unwrap();
|
||||
assert!(!set.as_path(&mut path));
|
||||
assert_eq!(path, [3333, 210036, 4_200_000_001]);
|
||||
|
||||
// An AS4_PATH longer than the path is not used.
|
||||
let mut too_wide = attributes(vec![3333]);
|
||||
too_wide.push(PathAttribute::As4Path(AsPathAttribute {
|
||||
segments: vec![sequence(vec![1, 2])],
|
||||
}));
|
||||
let set = store.intern(&too_wide, None).unwrap();
|
||||
assert!(!set.as_path(&mut path));
|
||||
assert_eq!(path, [3333]);
|
||||
|
||||
// A set is said to be there.
|
||||
let mut with_set = attributes(vec![]);
|
||||
with_set[1] = PathAttribute::AsPath(AsPathAttribute {
|
||||
segments: vec![
|
||||
|
||||
@@ -625,12 +625,19 @@ fn write(shard: &RwLock<Shard>) -> RwLockWriteGuard<'_, Shard> {
|
||||
///
|
||||
/// That table takes a key's slot from the low bits of the hash and, from
|
||||
/// the top seven, the tag it compares before it looks at the key itself
|
||||
/// (hashbrown). The bucket is taken from bits in between: taken from
|
||||
/// either end, the keys of one bucket would all share those bits, and the
|
||||
/// table inside would lose them.
|
||||
/// (hashbrown). Taken from either end, the keys of one bucket would all
|
||||
/// share those bits, and the table inside would lose them. Nor is it
|
||||
/// enough to take the bits in between: the hash is one multiplication,
|
||||
/// which for keys that are numbers in a row (prefixes are) can leave
|
||||
/// neighbouring bits alike. So the hash is stirred once more, and the
|
||||
/// bucket taken from the top of that, which every bit of the hash has a
|
||||
/// part in.
|
||||
fn spread(hash: u64, count: usize) -> usize {
|
||||
let middle = (hash << 7) >> 32;
|
||||
((middle * count as u64) >> 32) as usize
|
||||
// 2^64 divided by the golden ratio: the multiplier of Fibonacci
|
||||
// hashing.
|
||||
const STIR: u64 = 0x9E37_79B9_7F4A_7C15;
|
||||
let stirred = hash.wrapping_mul(STIR) >> 32;
|
||||
((stirred * count as u64) >> 32) as usize
|
||||
}
|
||||
|
||||
/// Adds to a vector without the doubling that `push` does: a row of a few
|
||||
|
||||
@@ -699,12 +699,35 @@ proptest! {
|
||||
/// differ where that table looks.
|
||||
#[test]
|
||||
fn keys_of_one_shard_differ_in_the_bits_their_table_uses() {
|
||||
let hasher = RandomState::default();
|
||||
// With keys of the hash that are the same every time: a test that
|
||||
// fails one run in a thousand tells nobody anything.
|
||||
for seed in 0..32u64 {
|
||||
keys_of_one_shard_differ(foldhash::fast::FixedState::with_seed(seed));
|
||||
}
|
||||
// Always one of the buckets there are.
|
||||
for hash in [0, 1, u64::MAX, u64::MAX / 2, 1 << 63] {
|
||||
for count in [1, 3, 64, 1000] {
|
||||
assert!(spread(hash, count) < count, "{hash} of {count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn keys_of_one_shard_differ(hasher: impl std::hash::BuildHasher) {
|
||||
let shards = 64;
|
||||
let mut counts = vec![0usize; shards];
|
||||
let mut tags = vec![std::collections::HashSet::new(); shards];
|
||||
// Keys as the table has them, and as regular as they come: every /24
|
||||
// of a /8.
|
||||
for n in 0..64_000u32 {
|
||||
let hash = hasher.hash_one(n);
|
||||
let [_, a, b, c] = n.to_be_bytes();
|
||||
let key: Nlri = IpPrefix::new(
|
||||
bgp_primitives::constants::IpVersion::Ipv4,
|
||||
[100 + a, b, c, 0],
|
||||
24,
|
||||
)
|
||||
.unwrap()
|
||||
.into();
|
||||
let hash = hasher.hash_one(key);
|
||||
let shard = spread(hash, shards);
|
||||
counts[shard] += 1;
|
||||
// The seven bits hashbrown keeps as a tag.
|
||||
@@ -724,8 +747,6 @@ fn keys_of_one_shard_differ_in_the_bits_their_table_uses() {
|
||||
tags[shard].len()
|
||||
);
|
||||
}
|
||||
assert_eq!(spread(u64::MAX, 3), 2);
|
||||
assert_eq!(spread(0, 3), 0);
|
||||
}
|
||||
|
||||
/// A peer that sends several routes for a prefix can send more than fit a
|
||||
|
||||
Reference in new issue
Block a user