The test files had grown their own copies of the same helpers: prefix() twelve times, established() seven, an UPDATE builder six, a seventeen field PeerConfig literal in fifteen places, a timeout constant in twenty one. Each is in crates/integration-tests/src once now: peer_config() and TestServer::with_peer for a configured peer, Announcement for building UPDATEs, established(), the waits (wait_until, wait_for, wait_for_routes, wait_for_sessions), get_json, and TIMEOUT. A new field on PeerConfig is one line to add, not fifteen. TestPeer::recv has a deadline, so a test that gets nothing fails instead of hanging; is_closed, which read one byte and took a KEEPALIVE for an open connection, is closed_within and closed_unanswered. Net 788 lines fewer; every test checks what it did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
208 lines
7.2 KiB
Rust
208 lines
7.2 KiB
Rust
//! End-to-end tests for errors in UPDATE messages (RFC 7606): most of them
|
|
//! cost the routes in the UPDATE, not the session.
|
|
|
|
use std::net::Ipv4Addr;
|
|
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::{ReceivedRoute, TestServer};
|
|
use bgp_integration_tests::{prefix, wait_for};
|
|
use bgp_packet::errors::{BgpError, UpdateMsgSubcode};
|
|
use bgp_packet::message::{LocalPrefPathAttribute, PathAttribute};
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_server::metrics::UPDATE_ERRORS;
|
|
use eyre::Result;
|
|
|
|
const LOCAL_ASN: u32 = 65000;
|
|
const PEER_ASN: u32 = 65001;
|
|
|
|
const WELL_KNOWN: u8 = 0x40;
|
|
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
|
|
|
|
fn attr(flags: u8, type_code: u8, value: &[u8]) -> Vec<u8> {
|
|
let mut wire = vec![flags, type_code, value.len() as u8];
|
|
wire.extend_from_slice(value);
|
|
wire
|
|
}
|
|
|
|
fn origin() -> Vec<u8> {
|
|
attr(WELL_KNOWN, 1, &[0])
|
|
}
|
|
|
|
/// AS_SEQUENCE of the peer's AS, four octets wide.
|
|
fn as_path() -> Vec<u8> {
|
|
attr(WELL_KNOWN, 2, &[2, 1, 0, 0, 0xfd, 0xe9])
|
|
}
|
|
|
|
fn next_hop() -> Vec<u8> {
|
|
attr(WELL_KNOWN, 3, &[192, 0, 2, 1])
|
|
}
|
|
|
|
/// A whole UPDATE message announcing `nlri` with `attributes`, as bytes, so
|
|
/// that it can be wrong in ways the encoder would not write.
|
|
fn update(attributes: &[Vec<u8>], nlri: &[u8]) -> Vec<u8> {
|
|
let attributes = attributes.concat();
|
|
let mut body = vec![0, 0];
|
|
body.extend_from_slice(&(attributes.len() as u16).to_be_bytes());
|
|
body.extend_from_slice(&attributes);
|
|
body.extend_from_slice(nlri);
|
|
|
|
let mut wire = vec![0xff; 16];
|
|
wire.extend_from_slice(&((19 + body.len()) as u16).to_be_bytes());
|
|
wire.push(2);
|
|
wire.extend_from_slice(&body);
|
|
wire
|
|
}
|
|
|
|
/// 10.1.0.0/16 and 10.2.0.0/16.
|
|
const ROUTE_A: [u8; 3] = [16, 10, 1];
|
|
const ROUTE_B: [u8; 3] = [16, 10, 2];
|
|
|
|
async fn established(server: &TestServer, asn: u32) -> Result<TestPeer> {
|
|
server.established(asn, Ipv4Addr::new(10, 0, 0, 1)).await
|
|
}
|
|
|
|
/// Waits until the server's Adj-RIB-In holds exactly `prefixes`.
|
|
async fn wait_for_routes(server: &TestServer, prefixes: &[&str]) -> Result<Vec<ReceivedRoute>> {
|
|
let mut expected: Vec<IpPrefix> = prefixes.iter().map(|p| prefix(p)).collect();
|
|
expected.sort_by_key(|p| p.to_string());
|
|
wait_for(&format!("the table holds {prefixes:?}"), || {
|
|
let mut routes = server.received_routes();
|
|
routes.sort_by_key(|r| r.prefix.to_string());
|
|
routes
|
|
.iter()
|
|
.map(|r| &r.prefix)
|
|
.eq(expected.iter())
|
|
.then_some(routes)
|
|
})
|
|
.await
|
|
}
|
|
|
|
/// The situation RFC 7606 exists for: a route is in the table, and then an
|
|
/// UPDATE for it arrives with a broken attribute. The route goes; the
|
|
/// session, and the other routes learned over it, stay.
|
|
#[tokio::test]
|
|
async fn test_a_malformed_attribute_withdraws_the_route_and_keeps_the_session() -> Result<()> {
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
let mut peer = established(&server, PEER_ASN).await?;
|
|
let handled_before = UPDATE_ERRORS
|
|
.with_label_values(&["treat-as-withdraw"])
|
|
.get();
|
|
|
|
let good = [origin(), as_path(), next_hop()];
|
|
peer.send_raw(&update(&good, &[ROUTE_A, ROUTE_B].concat()))
|
|
.await?;
|
|
wait_for_routes(&server, &["10.1.0.0/16", "10.2.0.0/16"]).await?;
|
|
|
|
// ORIGIN with a value that is not defined.
|
|
let bad = [attr(WELL_KNOWN, 1, &[7]), as_path(), next_hop()];
|
|
peer.send_raw(&update(&bad, &ROUTE_A)).await?;
|
|
wait_for_routes(&server, &["10.2.0.0/16"]).await?;
|
|
|
|
// The session goes on: the route can be announced again.
|
|
peer.send_raw(&update(&good, &ROUTE_A)).await?;
|
|
wait_for_routes(&server, &["10.1.0.0/16", "10.2.0.0/16"]).await?;
|
|
assert_eq!(server.registry.list().len(), 1);
|
|
assert!(
|
|
UPDATE_ERRORS
|
|
.with_label_values(&["treat-as-withdraw"])
|
|
.get()
|
|
> handled_before
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// An announcement without a mandatory attribute is not a route we can use.
|
|
#[tokio::test]
|
|
async fn test_a_route_without_as_path_is_not_accepted() -> Result<()> {
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
let mut peer = established(&server, PEER_ASN).await?;
|
|
|
|
peer.send_raw(&update(&[origin(), next_hop()], &ROUTE_A))
|
|
.await?;
|
|
// A well-formed one after it shows that the first was dealt with.
|
|
peer.send_raw(&update(&[origin(), as_path(), next_hop()], &ROUTE_B))
|
|
.await?;
|
|
|
|
wait_for_routes(&server, &["10.2.0.0/16"]).await?;
|
|
assert_eq!(server.registry.list().len(), 1);
|
|
Ok(())
|
|
}
|
|
|
|
/// A broken attribute that plays no part in choosing a route is dropped,
|
|
/// and the route is kept.
|
|
#[tokio::test]
|
|
async fn test_a_malformed_aggregator_is_dropped_and_the_route_kept() -> Result<()> {
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
let mut peer = established(&server, PEER_ASN).await?;
|
|
|
|
let attributes = [
|
|
origin(),
|
|
as_path(),
|
|
next_hop(),
|
|
// AGGREGATOR is eight octets on this session.
|
|
attr(OPTIONAL_TRANSITIVE, 7, &[0, 0, 0xfd, 0xe9, 192]),
|
|
];
|
|
peer.send_raw(&update(&attributes, &ROUTE_A)).await?;
|
|
|
|
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
|
|
assert_eq!(routes[0].attributes.len(), 3);
|
|
assert!(
|
|
!routes[0]
|
|
.attributes
|
|
.iter()
|
|
.any(|a| matches!(a, PathAttribute::Aggregator(_)))
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// LOCAL_PREF is a matter between routers of one AS. From an external peer
|
|
/// it is dropped; from an internal one it is kept.
|
|
#[tokio::test]
|
|
async fn test_local_pref_is_kept_only_from_an_internal_peer() -> Result<()> {
|
|
let local_pref = attr(WELL_KNOWN, 5, &[0, 0, 0, 200]);
|
|
let has_local_pref = |routes: &[ReceivedRoute]| {
|
|
routes[0]
|
|
.attributes
|
|
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(200)))
|
|
};
|
|
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
let mut external = established(&server, PEER_ASN).await?;
|
|
let attributes = [origin(), as_path(), next_hop(), local_pref.clone()];
|
|
external.send_raw(&update(&attributes, &ROUTE_A)).await?;
|
|
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
|
|
assert!(!has_local_pref(&routes));
|
|
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
let mut internal = established(&server, LOCAL_ASN).await?;
|
|
// An internal peer has not added its AS to the path.
|
|
let attributes = [origin(), attr(WELL_KNOWN, 2, &[]), next_hop(), local_pref];
|
|
internal.send_raw(&update(&attributes, &ROUTE_A)).await?;
|
|
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
|
|
assert!(has_local_pref(&routes));
|
|
Ok(())
|
|
}
|
|
|
|
/// When the prefixes themselves cannot be read there is nothing to
|
|
/// withdraw, and the session is reset as before.
|
|
#[tokio::test]
|
|
async fn test_an_unreadable_prefix_still_resets_the_session() -> Result<()> {
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
let mut peer = established(&server, PEER_ASN).await?;
|
|
|
|
// An IPv4 prefix of length 33.
|
|
peer.send_raw(&update(
|
|
&[origin(), as_path(), next_hop()],
|
|
&[33, 10, 1, 2, 3, 4],
|
|
))
|
|
.await?;
|
|
|
|
let notification = peer.expect_notification().await?;
|
|
assert_eq!(
|
|
notification.error,
|
|
BgpError::UpdateMsg(UpdateMsgSubcode::InvalidNetworkField)
|
|
);
|
|
server.wait_for_sessions(0).await?;
|
|
Ok(())
|
|
}
|