Preparing to peer with a production BIRD router turned up two problems. - packet: most Capability::from_wire arms returned the per-capability buffer (always empty by then) instead of the bytes following the capability. The list parser then saw an empty remainder and silently dropped every later capability. Any OPEN listing extended message, role, graceful restart, add-path, enhanced route refresh, LLGR, extended next hop or an unknown capability before the 4-octet ASN capability lost the ASN, so the peer appeared as AS_TRANS (23456) and a configured remote_asn was refused with Bad Peer AS. BIRD and FRR both do this. Single-capability round-trip tests could not catch it; new tests parse every capability with another following it, and a full BIRD capability list. - server: advertise MultiProtocol IPv6 unicast alongside IPv4. Without it a peer will not accept the IPv6 routes we originate via MP_REACH_NLRI. - Interop test: byte-for-byte BIRD 2 OPEN is accepted with the right ASN, and an IPv6 End-of-RIB neither drops the session nor creates a route. - docs/lab/prod: BIRD config for an announce-only ferrix neighbour (strict /48 import filter, community scrubbing, unreachable next hop, import limit, export none, passive) and the matching ferrix config. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
120 lines
5.2 KiB
Rust
120 lines
5.2 KiB
Rust
//! Interoperability with what real routers put on the wire.
|
|
|
|
use std::net::Ipv4Addr;
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use bgp_packet::message::{Message, PathAttribute};
|
|
use bgp_packet::open::{Capability, OpenOption};
|
|
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
|
use eyre::Result;
|
|
|
|
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
/// The OPEN a BIRD 2 router sends for an IPv6-only session from AS210036:
|
|
/// one optional parameter carrying MP(IPv6 unicast), route refresh,
|
|
/// extended message, graceful restart (aware only), 4-octet AS, enhanced
|
|
/// route refresh and long-lived graceful restart (aware only).
|
|
#[rustfmt::skip]
|
|
const BIRD_OPEN: &[u8] = &[
|
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
0x00, 0x37, 0x01, // length 55, type OPEN
|
|
0x04, // version
|
|
0x5b, 0xa0, // AS_TRANS
|
|
0x00, 0xf0, // hold time 240
|
|
0x0a, 0xff, 0x00, 0x01, // router id 10.255.0.1
|
|
0x1a, // optional parameters length 26
|
|
0x02, 0x18, // capabilities, 24 bytes
|
|
0x01, 0x04, 0x00, 0x02, 0x00, 0x01, // MP: IPv6 unicast
|
|
0x02, 0x00, // route refresh
|
|
0x06, 0x00, // extended message
|
|
0x40, 0x02, 0x00, 0x00, // graceful restart, no address families
|
|
0x41, 0x04, 0x00, 0x03, 0x34, 0x74, // 4-octet AS 210036
|
|
0x46, 0x00, // enhanced route refresh
|
|
0x47, 0x00, // long-lived graceful restart, none
|
|
];
|
|
|
|
/// IPv6 End-of-RIB: an UPDATE whose only attribute is an empty
|
|
/// MP_UNREACH_NLRI for IPv6 unicast (RFC 4724 §2).
|
|
#[rustfmt::skip]
|
|
const IPV6_END_OF_RIB: &[u8] = &[
|
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
0x00, 0x1e, 0x02, // length 30, type UPDATE
|
|
0x00, 0x00, // no withdrawn routes
|
|
0x00, 0x07, // 7 bytes of path attributes
|
|
0x90, 0x0f, 0x00, 0x03, // optional + extended length, MP_UNREACH_NLRI, len 3
|
|
0x00, 0x02, 0x01, // IPv6 unicast, no prefixes
|
|
];
|
|
|
|
#[tokio::test]
|
|
async fn test_server_advertises_ipv4_and_ipv6_unicast() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
let open = peer.recv_open().await?;
|
|
let caps: Vec<&Capability> = open
|
|
.options
|
|
.iter()
|
|
.flat_map(|o| match o {
|
|
OpenOption::Capabilities(c) => c.iter(),
|
|
})
|
|
.collect();
|
|
for afi in [AddressFamilyId::Ipv4, AddressFamilyId::Ipv6] {
|
|
assert!(
|
|
caps.iter().any(|c| matches!(c, Capability::MultiProtocol { afi: a, safi: SubsequentAfi::Unicast } if *a == afi)),
|
|
"missing MP capability for {afi:?}: {caps:?}"
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bird_open_and_end_of_rib_are_accepted() -> Result<()> {
|
|
let server = TestServer::start(4200000001).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
|
|
peer.send_raw(BIRD_OPEN).await?;
|
|
peer.recv_open().await?;
|
|
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
|
Message::KeepAlive => {}
|
|
other => eyre::bail!("server did not accept BIRD's OPEN: {other:?}"),
|
|
}
|
|
peer.send(&Message::KeepAlive).await?;
|
|
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while server.registry.list().is_empty() {
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
let session = server.registry.list().remove(0);
|
|
assert_eq!(session.peer.asn, 210036, "4-octet ASN must come from the capability, not AS_TRANS");
|
|
assert!(session.four_octet_asn);
|
|
|
|
// End-of-RIB must neither break the session nor create a route.
|
|
peer.send_raw(IPV6_END_OF_RIB).await?;
|
|
peer.send(&Message::KeepAlive).await?;
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
assert_eq!(server.registry.list().len(), 1, "session dropped after End-of-RIB");
|
|
assert!(server.adj_rib_in.snapshot().await.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
/// Over an IPv4 test transport an IPv6 route cannot be sent (no IPv6 next
|
|
/// hop), so this only checks the builder the advertiser uses: the UPDATE for
|
|
/// a lab /48 carries MP_REACH_NLRI with an IPv6 next hop and no NEXT_HOP.
|
|
#[tokio::test]
|
|
async fn test_ipv6_lab_prefix_update_shape() -> Result<()> {
|
|
let route = LocalRoute::new(IpPrefix::try_from("2a0d:d741:1::/48")?, "student1", LocalRouteAttrs::default());
|
|
let nh: std::net::Ipv6Addr = "fd00:179::2".parse()?;
|
|
let update = bgp_server::advertise::build_announce(&route, 4200000001, nh.into()).expect("ipv6 over ipv6");
|
|
assert!(update.announced_routes.is_empty());
|
|
assert!(!update.path_attributes.iter().any(|a| matches!(a, PathAttribute::NextHop(_))));
|
|
assert!(update.path_attributes.iter().any(|a| matches!(a, PathAttribute::MpReachNlri(m)
|
|
if m.afi == AddressFamilyId::Ipv6 && m.prefixes[0].to_string() == "2a0d:d741:1::/48")));
|
|
let _ = Ipv4Addr::LOCALHOST;
|
|
Ok(())
|
|
}
|