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
@@ -1,3 +1,13 @@
|
||||
pub mod test_cache;
|
||||
pub mod test_peer;
|
||||
pub mod test_server;
|
||||
|
||||
/// The policy a peer has with nothing configured, but for one thing: the
|
||||
/// peers of these tests are all at 127.0.0.1, which no route can have as
|
||||
/// its next hop, so their routes name another.
|
||||
pub fn default_policy() -> bgp_server::policy::ImportPolicy {
|
||||
bgp_server::policy::ImportPolicy {
|
||||
accept_third_party_next_hop: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::default_policy;
|
||||
use bgp_integration_tests::test_cache::TestCache;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
@@ -154,8 +155,7 @@ fn aspas() -> Vec<(u32, Vec<u32>)> {
|
||||
#[tokio::test]
|
||||
async fn test_a_leaked_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let Setup { server, .. } =
|
||||
server_with(&cache, Some(PeerRole::Peer), ImportPolicy::default()).await?;
|
||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
@@ -211,7 +211,7 @@ async fn test_a_route_that_has_crossed_a_peering_is_taken_from_a_provider_only()
|
||||
(None, true),
|
||||
] {
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let Setup { server, .. } = server_with(&cache, role, ImportPolicy::default()).await?;
|
||||
let Setup { server, .. } = server_with(&cache, role, default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||
let expected = if taken {
|
||||
@@ -232,7 +232,7 @@ async fn test_paths_from_a_route_server_do_not_begin_with_its_as() -> Result<()>
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let import = ImportPolicy {
|
||||
accept_any_first_as: true,
|
||||
..ImportPolicy::default()
|
||||
..default_policy()
|
||||
};
|
||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::RouteServer), import).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
@@ -250,8 +250,7 @@ async fn test_paths_from_a_route_server_do_not_begin_with_its_as() -> Result<()>
|
||||
#[tokio::test]
|
||||
async fn test_routes_follow_the_aspas() -> Result<()> {
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let Setup { server, .. } =
|
||||
server_with(&cache, Some(PeerRole::Peer), ImportPolicy::default()).await?;
|
||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121]).await?;
|
||||
@@ -311,7 +310,7 @@ async fn test_a_peer_can_be_exempt_from_path_verification() -> Result<()> {
|
||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||
let import = ImportPolicy {
|
||||
accept_aspa_invalid: true,
|
||||
..ImportPolicy::default()
|
||||
..default_policy()
|
||||
};
|
||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), import).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
@@ -337,8 +336,7 @@ async fn test_a_route_is_held_until_origin_and_path_are_both_in_order() -> Resul
|
||||
asn,
|
||||
};
|
||||
let cache = TestCache::start_with(vec![google(13335)], aspas()).await;
|
||||
let Setup { server, .. } =
|
||||
server_with(&cache, Some(PeerRole::Peer), ImportPolicy::default()).await?;
|
||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||
table_becomes(&server, &[], 1).await?;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::default_policy;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::message::{
|
||||
@@ -175,7 +176,7 @@ async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
|
||||
add_path: None,
|
||||
import: ImportPolicy {
|
||||
max_prefixes: Some(100),
|
||||
..ImportPolicy::default()
|
||||
..default_policy()
|
||||
},
|
||||
role: None,
|
||||
require_role: false,
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::default_policy;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||
@@ -118,7 +119,7 @@ fn set(prefixes: &[&str]) -> BTreeSet<String> {
|
||||
/// through us. Everything a peer sent used to be taken.
|
||||
#[tokio::test]
|
||||
async fn test_routes_that_no_peer_should_announce_are_refused() -> Result<()> {
|
||||
let (server, _listener) = server_with(ImportPolicy::default()).await?;
|
||||
let (server, _listener) = server_with(default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
// One UPDATE with good and bad prefixes: the good ones are taken.
|
||||
@@ -156,7 +157,7 @@ async fn test_routes_that_no_peer_should_announce_are_refused() -> Result<()> {
|
||||
/// and the new one is not acceptable.
|
||||
#[tokio::test]
|
||||
async fn test_a_refused_route_replaces_the_one_that_was_taken() -> Result<()> {
|
||||
let (server, _listener) = server_with(ImportPolicy::default()).await?;
|
||||
let (server, _listener) = server_with(default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
announce(&mut peer, &["193.0.0.0/21", "193.0.16.0/24"], &[PEER_ASN]).await?;
|
||||
@@ -176,7 +177,7 @@ async fn test_a_peer_is_held_to_its_prefix_list_and_origins() -> Result<()> {
|
||||
let (server, _listener) = server_with(ImportPolicy {
|
||||
prefixes: Some(prefixes),
|
||||
origin_asns: Some([PEER_ASN, 2121].into()),
|
||||
..ImportPolicy::default()
|
||||
..default_policy()
|
||||
})
|
||||
.await?;
|
||||
let mut peer = established(&server).await?;
|
||||
@@ -206,7 +207,7 @@ async fn test_a_peer_over_its_limit_is_cut_off_and_kept_down() -> Result<()> {
|
||||
let (server, listener) = server_with(ImportPolicy {
|
||||
max_prefixes: Some(3),
|
||||
hold_down: Duration::from_millis(1500),
|
||||
..ImportPolicy::default()
|
||||
..default_policy()
|
||||
})
|
||||
.await?;
|
||||
// The server dials the peer as well; that connection is left unanswered
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::default_policy;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
||||
@@ -18,7 +19,7 @@ use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::LocalRoute;
|
||||
use bgp_server::config::PeerConfig;
|
||||
use bgp_server::family::FamilySet;
|
||||
use bgp_server::policy::{ImportPolicy, PeerRole};
|
||||
use bgp_server::policy::PeerRole;
|
||||
use eyre::{Result, bail};
|
||||
use serde_json::Value;
|
||||
use tokio::net::TcpListener;
|
||||
@@ -54,7 +55,7 @@ async fn server_with(role: Option<PeerRole>, require_role: bool) -> Result<Setup
|
||||
local_address: None,
|
||||
families: FamilySet::supported(),
|
||||
add_path: None,
|
||||
import: ImportPolicy::default(),
|
||||
import: default_policy(),
|
||||
role,
|
||||
require_role,
|
||||
}];
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::default_policy;
|
||||
use bgp_integration_tests::test_cache::TestCache;
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
@@ -60,9 +61,10 @@ async fn server_with(
|
||||
c.accept_unconfigured_peers = false;
|
||||
c.rpki = Some(RpkiConfig {
|
||||
caches: vec![cache],
|
||||
// Data never expires sooner than a refresh and a retry take.
|
||||
refresh: expire.map(|expire| expire / 2),
|
||||
retry: Some(Duration::from_millis(100)),
|
||||
expire,
|
||||
..RpkiConfig::default()
|
||||
});
|
||||
c.peers = vec![PeerConfig {
|
||||
name: "r1".into(),
|
||||
@@ -167,7 +169,7 @@ async fn table_becomes(server: &TestServer, expected: &[&str], held: u64) -> Res
|
||||
#[tokio::test]
|
||||
async fn test_an_invalid_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
||||
let Setup { server, .. } = server_with(&cache, None, ImportPolicy::default()).await?;
|
||||
let Setup { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
@@ -222,7 +224,7 @@ async fn test_an_invalid_route_is_refused_and_the_rest_are_taken() -> Result<()>
|
||||
async fn test_routes_follow_the_validation_data() -> Result<()> {
|
||||
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
||||
let cache = TestCache::start(vec![ripe]).await;
|
||||
let Setup { server, .. } = server_with(&cache, None, ImportPolicy::default()).await?;
|
||||
let Setup { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
// Nothing is known about 8.8.8.0/24, so it is taken. 193.0.4.0/24 is
|
||||
@@ -265,7 +267,7 @@ async fn test_routes_follow_the_validation_data() -> Result<()> {
|
||||
async fn test_a_held_route_the_peer_withdrew_does_not_come_back() -> Result<()> {
|
||||
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
||||
let cache = TestCache::start(vec![ripe]).await;
|
||||
let Setup { server, .. } = server_with(&cache, None, ImportPolicy::default()).await?;
|
||||
let Setup { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
announce(&mut peer, &["193.0.4.0/24", "193.0.5.0/24"], 15169).await?;
|
||||
@@ -302,7 +304,7 @@ async fn test_a_peer_can_be_exempt_from_validation() -> Result<()> {
|
||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
||||
let import = ImportPolicy {
|
||||
accept_rpki_invalid: true,
|
||||
..ImportPolicy::default()
|
||||
..default_policy()
|
||||
};
|
||||
let Setup { server, .. } = server_with(&cache, None, import).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
@@ -325,12 +327,8 @@ async fn test_a_peer_can_be_exempt_from_validation() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_held_routes_are_taken_when_the_validation_data_expires() -> Result<()> {
|
||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
||||
let Setup { server, .. } = server_with(
|
||||
&cache,
|
||||
Some(Duration::from_millis(1500)),
|
||||
ImportPolicy::default(),
|
||||
)
|
||||
.await?;
|
||||
let Setup { server, .. } =
|
||||
server_with(&cache, Some(Duration::from_millis(1500)), default_policy()).await?;
|
||||
let mut peer = established(&server).await?;
|
||||
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
||||
table_becomes(&server, &[], 1).await?;
|
||||
@@ -340,3 +338,64 @@ async fn test_held_routes_are_taken_when_the_validation_data_expires() -> Result
|
||||
assert!(server.rpki.as_ref().unwrap().current().vrps.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A hijack that was possible: on a session with two-octet AS numbers a
|
||||
/// route has two paths, AS_PATH and AS4_PATH, and a peer can make them
|
||||
/// say different things. Here AS_PATH is the peer alone, and an AS4_PATH
|
||||
/// too long to be used ends at the AS that holds the addresses. The route
|
||||
/// is the peer's and invalid, when it arrives and when it is looked at
|
||||
/// again; nothing may take the unused path's word for where it comes from.
|
||||
#[tokio::test]
|
||||
async fn test_an_unused_as4_path_does_not_make_a_route_valid() -> Result<()> {
|
||||
use bgp_packet::message::AsPathSegment;
|
||||
const HOLDER: u32 = 15169;
|
||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, HOLDER)]).await;
|
||||
let Setup { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||
|
||||
// No capabilities: AS numbers are two octets wide on this session.
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90, vec![])
|
||||
.await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.registry.list().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
assert!(!server.registry.list()[0].four_octet_asn);
|
||||
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
||||
PathAttribute::As4Path(AsPathAttribute {
|
||||
segments: vec![AsPathSegment {
|
||||
ordered: true,
|
||||
path: vec![3356, HOLDER],
|
||||
}],
|
||||
}),
|
||||
],
|
||||
announced_routes: vec![prefix("193.0.4.0/24").into()],
|
||||
}))
|
||||
.await?;
|
||||
table_becomes(&server, &[], 1).await?;
|
||||
|
||||
// The data changes, and every held route is looked at again.
|
||||
cache.set(vec![
|
||||
vrp("193.0.0.0/21", 24, HOLDER),
|
||||
vrp("198.51.100.0/24", 24, 64500),
|
||||
]);
|
||||
let rpki = server.rpki.clone().unwrap();
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while rpki.current().vrps.len() != 2 {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
// Time enough for the look, and then some.
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
table_becomes(&server, &[], 1).await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user