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>
280 lines
9.5 KiB
Rust
280 lines
9.5 KiB
Rust
//! End-to-end tests for capability negotiation: what the two OPENs agree on
|
|
//! decides which routes are sent and accepted, and how they are encoded.
|
|
|
|
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::announcement::announcement;
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::{ReceivedRoute, TestServer};
|
|
use bgp_integration_tests::{peer_config, prefix, wait_for};
|
|
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
|
use bgp_packet::message::{
|
|
AsPathAttribute, Message, MpReachNlriPathAttribute, NlriNextHop, PathAttribute, UpdateMessage,
|
|
};
|
|
use bgp_packet::open::Capability;
|
|
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
|
use bgp_server::config::PeerConfig;
|
|
use bgp_server::family::{FamilySet, SUPPORTED};
|
|
use bgp_server::policy::ImportPolicy;
|
|
use eyre::{Result, bail};
|
|
|
|
const LOCAL_ASN: u32 = 65000;
|
|
const PEER_ASN: u32 = 65001;
|
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
|
|
|
|
fn only(family: AfiSafi) -> FamilySet {
|
|
[family].into_iter().collect()
|
|
}
|
|
|
|
fn four_octet() -> Capability {
|
|
Capability::FourByteAsn { asn: PEER_ASN }
|
|
}
|
|
|
|
/// Waits until the server has a route, and returns what it has.
|
|
async fn some_routes(server: &TestServer) -> Result<Vec<ReceivedRoute>> {
|
|
wait_for("a route is in the table", || {
|
|
let routes = server.received_routes();
|
|
(!routes.is_empty()).then_some(routes)
|
|
})
|
|
.await
|
|
}
|
|
|
|
/// An UPDATE announcing one IPv4 route in the body and one IPv6 route in
|
|
/// MP_REACH_NLRI.
|
|
fn dual_stack_update() -> UpdateMessage {
|
|
let ipv6 = PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
|
family: AfiSafi::IPV6_UNICAST,
|
|
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
|
|
prefixes: vec![prefix("2001:db8:1::/48").into()],
|
|
});
|
|
announcement(&["192.0.2.0/24"], &[PEER_ASN])
|
|
.with_attributes([ipv6])
|
|
.into()
|
|
}
|
|
|
|
/// The session carries the families both OPENs offered, and the server's
|
|
/// OPEN offers every family by default.
|
|
#[tokio::test]
|
|
async fn test_families_are_the_ones_both_sides_offered() -> Result<()> {
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
let open = peer
|
|
.handshake_with(
|
|
PEER_ASN,
|
|
PEER_ID,
|
|
90,
|
|
vec![
|
|
four_octet(),
|
|
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
|
],
|
|
)
|
|
.await?;
|
|
|
|
for family in SUPPORTED {
|
|
assert!(
|
|
open.capabilities()
|
|
.any(|c| *c == Capability::multiprotocol(family)),
|
|
"the server's OPEN offers {family}"
|
|
);
|
|
}
|
|
let session = server.wait_for_session().await?;
|
|
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
|
Ok(())
|
|
}
|
|
|
|
/// A route of a family the peer did not offer is not accepted from it. The
|
|
/// rest of the UPDATE is, and the session stays up.
|
|
#[tokio::test]
|
|
async fn test_routes_of_a_family_not_negotiated_are_not_accepted() -> Result<()> {
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake_with(
|
|
PEER_ASN,
|
|
PEER_ID,
|
|
90,
|
|
vec![
|
|
four_octet(),
|
|
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
|
],
|
|
)
|
|
.await?;
|
|
server.wait_for_session().await?;
|
|
|
|
peer.announce(dual_stack_update()).await?;
|
|
|
|
let routes = some_routes(&server).await?;
|
|
// One UPDATE is applied as a whole, so the IPv6 route is not on its way.
|
|
assert_eq!(routes.len(), 1);
|
|
assert_eq!(routes[0].prefix, prefix("192.0.2.0/24"));
|
|
assert_eq!(server.registry.list().len(), 1, "the session survives");
|
|
Ok(())
|
|
}
|
|
|
|
/// A route of a family the peer did not offer is not sent to it.
|
|
#[tokio::test]
|
|
async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
|
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
c.originate = vec![LocalRoute::new(
|
|
prefix("10.100.0.0/16"),
|
|
LocalRouteAttrs::default(),
|
|
)];
|
|
})
|
|
.await?;
|
|
|
|
// This peer wants IPv6 only; the server has an IPv4 route.
|
|
let mut v6_only = TestPeer::connect(server.addr).await?;
|
|
v6_only
|
|
.handshake_with(
|
|
PEER_ASN,
|
|
PEER_ID,
|
|
90,
|
|
vec![
|
|
four_octet(),
|
|
Capability::multiprotocol(AfiSafi::IPV6_UNICAST),
|
|
],
|
|
)
|
|
.await?;
|
|
let session = server.wait_for_session().await?;
|
|
assert_eq!(session.families, only(AfiSafi::IPV6_UNICAST));
|
|
|
|
// A second peer that takes IPv4 is sent the route. The advertiser
|
|
// handles sessions in the order they came up, so by the time this one
|
|
// has its route the first has been dealt with.
|
|
let mut dual = TestPeer::connect(server.addr).await?;
|
|
dual.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
.await?;
|
|
let update = dual.recv_update().await?;
|
|
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
|
|
|
let nothing = tokio::time::timeout(Duration::from_millis(200), v6_only.recv_update()).await;
|
|
assert!(nothing.is_err(), "got {nothing:?}");
|
|
Ok(())
|
|
}
|
|
|
|
/// A configured peer is offered only the families configured for it, and a
|
|
/// peer that shares none of them is refused with the capability we needed.
|
|
#[tokio::test]
|
|
async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
|
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
// Inbound connections from this address inherit the peer's settings.
|
|
let at = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1);
|
|
c.peers = vec![PeerConfig {
|
|
families: only(AfiSafi::IPV6_UNICAST),
|
|
import: ImportPolicy::permissive(),
|
|
..peer_config("v6-only", at, PEER_ASN)
|
|
}];
|
|
// Keep the dialer out of the way.
|
|
c.connect_retry = Duration::from_secs(3600);
|
|
})
|
|
.await?;
|
|
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.send_open_with(
|
|
PEER_ASN,
|
|
PEER_ID,
|
|
90,
|
|
vec![
|
|
four_octet(),
|
|
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
|
],
|
|
)
|
|
.await?;
|
|
let open = peer.recv_open().await?;
|
|
let offered: Vec<_> = open
|
|
.capabilities()
|
|
.filter(|c| matches!(c, Capability::MultiProtocol { .. }))
|
|
.collect();
|
|
assert_eq!(offered, [&Capability::multiprotocol(AfiSafi::IPV6_UNICAST)]);
|
|
|
|
match peer.recv().await? {
|
|
Message::Notification(n) => {
|
|
assert_eq!(
|
|
n.error,
|
|
BgpError::OpenMsg(OpenMsgSubcode::UnsupportedCapability)
|
|
);
|
|
// The Multiprotocol capability for IPv6 unicast.
|
|
assert_eq!(n.data, [1, 4, 0, 2, 0, 1]);
|
|
}
|
|
other => bail!("expected NOTIFICATION, got {other:?}"),
|
|
}
|
|
assert!(server.registry.list().is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
/// Real routers offer families we do not support. That must not stop
|
|
/// the families we do share from working.
|
|
#[tokio::test]
|
|
async fn test_a_peer_offering_an_unsupported_family_is_still_accepted() -> Result<()> {
|
|
let server = TestServer::start(LOCAL_ASN).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake_with(
|
|
PEER_ASN,
|
|
PEER_ID,
|
|
90,
|
|
vec![
|
|
four_octet(),
|
|
Capability::multiprotocol(AfiSafi::new(Afi::L2VPN, Safi::EVPN)),
|
|
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
|
],
|
|
)
|
|
.await?;
|
|
|
|
let session = server.wait_for_session().await?;
|
|
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
|
assert!(
|
|
session.four_octet_asn,
|
|
"capabilities after it are still read"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// A peer that offers no capabilities at all is a plain RFC 4271 speaker:
|
|
/// IPv4 unicast, two-octet AS numbers. Our four-octet AS number reaches it
|
|
/// as AS_TRANS in AS_PATH and in full in AS4_PATH (RFC 6793 §4.2.2).
|
|
#[tokio::test]
|
|
async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()> {
|
|
const WIDE_ASN: u32 = 4_200_000_001;
|
|
let server = TestServer::start_with(WIDE_ASN, |c| {
|
|
c.originate = vec![LocalRoute::new(
|
|
prefix("10.100.0.0/16"),
|
|
LocalRouteAttrs::default(),
|
|
)];
|
|
})
|
|
.await?;
|
|
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake_with(PEER_ASN, PEER_ID, 90, vec![]).await?;
|
|
let session = server.wait_for_session().await?;
|
|
assert!(!session.four_octet_asn);
|
|
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
|
|
|
// The test peer now decodes AS_PATH two octets wide, as such a speaker
|
|
// would; an UPDATE encoded four octets wide would not parse.
|
|
let update = peer.recv_update().await?;
|
|
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
|
assert!(
|
|
update
|
|
.path_attributes
|
|
.contains(&AsPathAttribute::from_asns(vec![23456])),
|
|
"{update:?}"
|
|
);
|
|
let as4_path = update.path_attributes.iter().find_map(|a| match a {
|
|
PathAttribute::As4Path(path) => Some(path.segments[0].path.clone()),
|
|
_ => None,
|
|
});
|
|
assert_eq!(as4_path, Some(vec![WIDE_ASN]));
|
|
|
|
// And its own two-octet paths are read correctly.
|
|
peer.announce(announcement(&["192.0.2.0/24"], &[PEER_ASN, 64512]))
|
|
.await?;
|
|
let routes = some_routes(&server).await?;
|
|
assert!(
|
|
routes[0]
|
|
.attributes
|
|
.contains(&AsPathAttribute::from_asns(vec![PEER_ASN, 64512]))
|
|
);
|
|
Ok(())
|
|
}
|