A review of the wire codec and the routing table, with the fixes. Bugs in the codec: - A NOTIFICATION with a code or subcode we have no name for (subcode 0, "unspecific", is common) failed to decode and was answered as a bad message. Every pair now reads, as BgpError::Other. - Optional parameters in the extended form of RFC 9072 were read and written with one-octet parameter lengths; they are two. - An ORF capability listing more than one address family made the whole OPEN malformed. - Bytes after an OPEN's optional parameters were accepted. - Extended communities were split into fields by rules that were wrong for the common types. They are kept as their eight octets. - Several length fields were computed in u16 and could wrap on encode. Simplifications in the codec: - Lengths are filled in after the value is written, for messages, attributes, capabilities and the UPDATE's two fields. The parallel wire_len functions, which had to be kept in step by hand, are gone. Message::to_wire no longer encodes into a second buffer. - Type codes for messages, attributes and capabilities are each defined once. The strum discriminant enums and their hand-written conversions are removed. - One function splits an attribute off a buffer; the UPDATE decoder, the plain attribute parser and the table's key reader all use it. - The UPDATE decoder's 185-line loop is an AttributeReader with one method that decides what becomes of an attribute. Duplicate detection is a bitset, not a HashSet allocated per UPDATE. - ParserContext::four_octet_asn is a bool. The "context not set" error path, which treated a programming error as a malformed route, is gone. - Codec functions take a context only if they read it, and return a Result only if they can fail. - MP_REACH_NLRI and MP_UNREACH_NLRI hold an AfiSafi. - AddressFamilyId is IpVersion, which is what it was. - Deserialize is no longer derived on codec types: nothing used it, and it bypassed IpPrefix's invariants. IpPrefix::to_wire, which was broken and unused, is removed. Bugs in the table: - The index of a row's best route was sixteen bits. A peer using ADD-PATH could send more routes for a prefix than that counts, and the index wrapped. It is 32 bits, at no cost in row size. - Shards and attribute-store stripes were picked from the top bits of the hash, which are the bits hashbrown keeps as the tag it compares first. With 64 shards each shard's table had 2 of 128 tag values. They are now picked from bits the table does not use. - remove_peer held the peer list's lock for the whole scan of the table, so no session could establish while a peer was removed. - When more prefixes went at once than were left, the forced trim dropped a fixed few entries, reset every reader, and re-ran the tidy on every later write. It drops the older half. - Readers were woken by every change, including a repeated announcement and changes their journal does not record. - Reader::poll with a budget of zero never made progress. Simplifications in the table: - The two journals of a shard are one Journals value written through three calls; the five places that kept them in step by hand are gone. Whether an entry is still live is decided in one place. - The shortcuts of route selection moved to select.rs, next to the invariant they depend on, which is now written down. - announce and withdraw take routes with or without path identifiers; the _paths variants are removed. - The selection key is read off the wire form only; the second implementation over parsed attributes is removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
548 lines
19 KiB
Rust
548 lines
19 KiB
Rust
use std::net::Ipv4Addr;
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::RouteChange;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use bgp_packet::errors::{BgpError, CeaseSubcode, FsmSubcode, MsgHeaderSubcode, UpdateMsgSubcode};
|
|
use bgp_packet::message::{
|
|
AsPathAttribute, Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute,
|
|
NextHopPathAttribute, NlriNextHop, NotificationMessage, OriginPathAttribute, PathAttribute,
|
|
UpdateMessage,
|
|
};
|
|
use bgp_primitives::constants::{AfiSafi, IpVersion};
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use eyre::Result;
|
|
|
|
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
/// The attributes every announcement of IPv4 routes must carry. An UPDATE
|
|
/// without them has its routes withdrawn instead (RFC 7606).
|
|
fn mandatory_attributes() -> Vec<PathAttribute> {
|
|
let mut attributes = with_origin_and_as_path(PathAttribute::NextHop(NextHopPathAttribute(
|
|
Ipv4Addr::new(192, 0, 2, 1),
|
|
)));
|
|
// In order of type code: ORIGIN, AS_PATH, NEXT_HOP.
|
|
attributes.rotate_left(1);
|
|
attributes
|
|
}
|
|
|
|
/// `first` followed by the attributes every announcement must carry.
|
|
/// Routes in MP_REACH_NLRI have their next hop there.
|
|
fn with_origin_and_as_path(first: PathAttribute) -> Vec<PathAttribute> {
|
|
vec![
|
|
first,
|
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
AsPathAttribute::from_asns(vec![65001]),
|
|
]
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Connectivity
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn test_server_starts() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
assert_ne!(server.addr.port(), 0);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_server_accepts_tcp_connection() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let _peer = TestPeer::connect(server.addr).await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_server_accepts_multiple_connections() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut peers = Vec::new();
|
|
for _ in 0..5 {
|
|
peers.push(TestPeer::connect(server.addr).await?);
|
|
}
|
|
assert_eq!(peers.len(), 5);
|
|
Ok(())
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// OpenSent state
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Server sends an OPEN immediately after TCP is established.
|
|
#[tokio::test]
|
|
async fn test_server_sends_open() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
|
|
let open = peer.recv_open().await?;
|
|
assert_eq!(open.version, 4);
|
|
assert_eq!(open.asn, 65000);
|
|
Ok(())
|
|
}
|
|
|
|
/// Server sends a NOTIFICATION and closes if we send garbage instead of OPEN.
|
|
#[tokio::test]
|
|
async fn test_bad_marker_does_not_crash_server() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
|
|
let mut bad = TestPeer::connect(server.addr).await?;
|
|
// Discard the server's OPEN, then send garbage.
|
|
let _ = bad.recv_open().await?;
|
|
bad.send_raw(&[0x00; 19]).await?;
|
|
drop(bad);
|
|
|
|
// New connections are still accepted.
|
|
let _good = TestPeer::connect(server.addr).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Reads until a NOTIFICATION arrives, skipping KEEPALIVEs.
|
|
async fn expect_notification(peer: &mut TestPeer) -> Result<NotificationMessage> {
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
loop {
|
|
match peer.recv().await? {
|
|
Message::Notification(n) => return Ok(n),
|
|
Message::KeepAlive => {}
|
|
other => eyre::bail!("expected NOTIFICATION, got {other:?}"),
|
|
}
|
|
}
|
|
})
|
|
.await?
|
|
}
|
|
|
|
/// A header with the given length and type, followed by `body`.
|
|
fn framed(length: u16, msg_type: u8, body: &[u8]) -> Vec<u8> {
|
|
let mut buf = vec![0xFF; 16];
|
|
buf.extend_from_slice(&length.to_be_bytes());
|
|
buf.push(msg_type);
|
|
buf.extend_from_slice(body);
|
|
buf
|
|
}
|
|
|
|
/// What the peer sent decides the NOTIFICATION it gets back (RFC 4271 §6),
|
|
/// in every state, and the session ends.
|
|
#[tokio::test]
|
|
async fn test_malformed_messages_are_answered_with_the_matching_notification() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let header = |subcode| BgpError::MsgHeader(subcode);
|
|
let mut bad_marker = framed(19, 4, &[]);
|
|
bad_marker[0] = 0;
|
|
|
|
let cases: Vec<(&str, Vec<u8>, BgpError, Vec<u8>)> = vec![
|
|
(
|
|
"length beyond the maximum",
|
|
framed(5000, 2, &[]),
|
|
header(MsgHeaderSubcode::BadMessageLength),
|
|
5000u16.to_be_bytes().to_vec(),
|
|
),
|
|
(
|
|
"length below a header",
|
|
framed(5, 4, &[]),
|
|
header(MsgHeaderSubcode::BadMessageLength),
|
|
vec![0, 5],
|
|
),
|
|
(
|
|
"unknown message type",
|
|
framed(19, 9, &[]),
|
|
header(MsgHeaderSubcode::BadMessageType),
|
|
vec![9],
|
|
),
|
|
(
|
|
"marker not all ones",
|
|
bad_marker,
|
|
header(MsgHeaderSubcode::ConnNotSynchronized),
|
|
vec![],
|
|
),
|
|
(
|
|
"UPDATE with attributes running past its end",
|
|
framed(23, 2, &[0, 0, 0, 40]),
|
|
BgpError::UpdateMsg(UpdateMsgSubcode::MalformedAttrs),
|
|
vec![],
|
|
),
|
|
];
|
|
|
|
for (i, (what, bytes, error, data)) in cases.into_iter().enumerate() {
|
|
// Established: a distinct router ID each time, so that a session
|
|
// still closing does not collide with the next one.
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 1, i as u8 + 1), 90)
|
|
.await?;
|
|
peer.send_raw(&bytes).await?;
|
|
let notification = expect_notification(&mut peer).await?;
|
|
assert_eq!(
|
|
(notification.error, notification.data),
|
|
(error.clone(), data.clone()),
|
|
"{what}, Established"
|
|
);
|
|
|
|
// OpenSent: the same bytes instead of an OPEN.
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
let _ = peer.recv_open().await?;
|
|
peer.send_raw(&bytes).await?;
|
|
let notification = expect_notification(&mut peer).await?;
|
|
assert_eq!(
|
|
(notification.error, notification.data),
|
|
(error, data),
|
|
"{what}, OpenSent"
|
|
);
|
|
}
|
|
|
|
// A well-formed message at the wrong time is an FSM error, not a
|
|
// message error.
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
let _ = peer.recv_open().await?;
|
|
peer.send(&Message::KeepAlive).await?;
|
|
let notification = expect_notification(&mut peer).await?;
|
|
assert_eq!(
|
|
notification.error,
|
|
BgpError::FsmError(FsmSubcode::UnexpectedOpenSent)
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// A message may arrive in pieces, with the server's own timers firing in
|
|
/// between. The server once threw away the piece it had already read when
|
|
/// that happened, so the message was silently lost (and the stream was only
|
|
/// back in step if the remainder happened to be thrown away as well).
|
|
#[tokio::test]
|
|
async fn test_message_split_across_a_keepalive_tick_is_still_one_message() -> Result<()> {
|
|
// Hold time 3 s: the server sends a KEEPALIVE every second.
|
|
let server = TestServer::start_with_hold_time(65000, 3).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
|
|
|
|
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
|
|
let mut update = bytes::BytesMut::new();
|
|
Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: mandatory_attributes(),
|
|
announced_routes: vec![prefix.into()],
|
|
})
|
|
.to_wire(&peer.ctx, &mut update)?;
|
|
|
|
// The first half, a pause long enough for the server's keepalive timer
|
|
// to fire, then the rest.
|
|
peer.send_raw(&update[..10]).await?;
|
|
tokio::time::sleep(Duration::from_millis(1300)).await;
|
|
peer.send_raw(&update[10..]).await?;
|
|
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while !server.received_routes().iter().any(|e| e.prefix == prefix) {
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
}
|
|
})
|
|
.await
|
|
.map_err(|_| eyre::eyre!("the split UPDATE never reached the RIB"))?;
|
|
assert_eq!(server.registry.list().len(), 1, "session was dropped");
|
|
Ok(())
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// OpenConfirm → Established
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Full OPEN/KEEPALIVE handshake reaches Established.
|
|
#[tokio::test]
|
|
async fn test_session_established() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
|
|
let server_open = peer
|
|
.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
|
.await?;
|
|
|
|
assert_eq!(server_open.version, 4);
|
|
Ok(())
|
|
}
|
|
|
|
/// After Established, the server sends periodic KEEPALIVEs.
|
|
#[tokio::test]
|
|
async fn test_server_sends_keepalives() -> Result<()> {
|
|
// Use a short hold time so the keepalive interval (hold/3 = 1 s) fires quickly.
|
|
let server = TestServer::start_with_hold_time(65000, 3).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
|
|
|
|
// We should receive a KEEPALIVE within ~1 second (hold/3).
|
|
let msg = tokio::time::timeout(Duration::from_secs(3), peer.recv()).await??;
|
|
assert!(
|
|
matches!(msg, Message::KeepAlive),
|
|
"Expected KEEPALIVE, got {:?}",
|
|
msg
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Server closes with a NOTIFICATION when the hold timer expires.
|
|
#[tokio::test]
|
|
async fn test_hold_timer_expires() -> Result<()> {
|
|
// hold_time = 3 s → server expects a KEEPALIVE within 3 s.
|
|
let server = TestServer::start_with_hold_time(65000, 3).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
|
|
|
|
// Don't send any keepalives. The server will send keepalives until its own
|
|
// hold timer fires and it sends a NOTIFICATION. Drain messages until we
|
|
// see it (or the outer timeout fires).
|
|
let notif = tokio::time::timeout(Duration::from_secs(6), async {
|
|
loop {
|
|
match peer.recv().await? {
|
|
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
|
|
Message::KeepAlive => {} // server keepalive — keep waiting
|
|
other => eyre::bail!("Unexpected message: {:?}", other),
|
|
}
|
|
}
|
|
})
|
|
.await??;
|
|
assert!(
|
|
matches!(notif.error, BgpError::HoldTimer),
|
|
"Expected HoldTimer NOTIFICATION, got {:?}",
|
|
notif
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// A peer-sent NOTIFICATION is handled gracefully (session closes cleanly).
|
|
#[tokio::test]
|
|
async fn test_peer_sends_notification() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
|
.await?;
|
|
|
|
peer.send(&Message::Notification(NotificationMessage {
|
|
error: BgpError::Cease(CeaseSubcode::AdminShutdown),
|
|
data: vec![],
|
|
}))
|
|
.await?;
|
|
|
|
// Server should close the connection cleanly.
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
assert!(peer.is_closed().await);
|
|
Ok(())
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UPDATE
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// An UPDATE message is accepted without crashing the server.
|
|
#[tokio::test]
|
|
async fn test_server_accepts_update() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
|
.await?;
|
|
|
|
let update = Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: mandatory_attributes(),
|
|
// 10.0.0.0/8 — one byte of prefix for a /8.
|
|
announced_routes: vec![IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?.into()],
|
|
});
|
|
peer.send(&update).await?;
|
|
|
|
// Server should stay up — we can still exchange a keepalive.
|
|
peer.send(&Message::KeepAlive).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// An IPv6 UPDATE via MP_REACH_NLRI is accepted without crashing the server.
|
|
#[tokio::test]
|
|
async fn test_server_accepts_ipv6_update() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
|
.await?;
|
|
|
|
// 2001:db8::/32
|
|
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
|
|
let update = Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
|
|
MpReachNlriPathAttribute {
|
|
family: AfiSafi::IPV6_UNICAST,
|
|
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
|
|
prefixes: vec![prefix.into()],
|
|
},
|
|
)),
|
|
announced_routes: vec![],
|
|
});
|
|
peer.send(&update).await?;
|
|
|
|
// Server should stay up — we can still exchange a keepalive.
|
|
peer.send(&Message::KeepAlive).await?;
|
|
Ok(())
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Adj-RIB-In introspection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// IPv4 prefix announced via the legacy NLRI field appears in the RIB.
|
|
#[tokio::test]
|
|
async fn test_rib_records_ipv4_announce() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut events = server.route_changes();
|
|
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
|
.await?;
|
|
|
|
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: mandatory_attributes(),
|
|
announced_routes: vec![prefix.into()],
|
|
}))
|
|
.await?;
|
|
|
|
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
|
assert!(
|
|
matches!(&event, RouteChange::Announced { prefix: p, .. } if p == &prefix),
|
|
"Expected Announced for 10.0.0.0/8, got {event:?}",
|
|
);
|
|
|
|
let snapshot = server.received_routes();
|
|
assert!(
|
|
snapshot.iter().any(|e| e.prefix == prefix),
|
|
"10.0.0.0/8 missing from RIB snapshot",
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Withdrawn IPv4 prefix is removed from the RIB.
|
|
#[tokio::test]
|
|
async fn test_rib_records_ipv4_withdraw() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut events = server.route_changes();
|
|
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
|
.await?;
|
|
|
|
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
|
|
|
|
// Announce first.
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: mandatory_attributes(),
|
|
announced_routes: vec![prefix.into()],
|
|
}))
|
|
.await?;
|
|
tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
|
|
|
// Now withdraw.
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![prefix.into()],
|
|
path_attributes: vec![],
|
|
announced_routes: vec![],
|
|
}))
|
|
.await?;
|
|
|
|
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
|
assert!(
|
|
matches!(&event, RouteChange::Withdrawn { prefix: p, .. } if p == &prefix),
|
|
"Expected Withdrawn for 10.0.0.0/8, got {event:?}",
|
|
);
|
|
|
|
let snapshot = server.received_routes();
|
|
assert!(
|
|
!snapshot.iter().any(|e| e.prefix == prefix),
|
|
"10.0.0.0/8 should have been removed from RIB snapshot",
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// IPv6 prefix announced via MP_REACH_NLRI appears in the RIB.
|
|
#[tokio::test]
|
|
async fn test_rib_records_ipv6_announce() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut events = server.route_changes();
|
|
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
|
.await?;
|
|
|
|
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
|
|
MpReachNlriPathAttribute {
|
|
family: AfiSafi::IPV6_UNICAST,
|
|
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
|
|
prefixes: vec![prefix.into()],
|
|
},
|
|
)),
|
|
announced_routes: vec![],
|
|
}))
|
|
.await?;
|
|
|
|
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
|
assert!(
|
|
matches!(&event, RouteChange::Announced { prefix: p, .. } if p == &prefix),
|
|
"Expected Announced for 2001:db8::/32, got {event:?}",
|
|
);
|
|
|
|
let snapshot = server.received_routes();
|
|
assert!(
|
|
snapshot.iter().any(|e| e.prefix == prefix),
|
|
"2001:db8::/32 missing from RIB snapshot",
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// IPv6 prefix withdrawn via MP_UNREACH_NLRI is removed from the RIB.
|
|
#[tokio::test]
|
|
async fn test_rib_records_ipv6_withdraw() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut events = server.route_changes();
|
|
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
|
.await?;
|
|
|
|
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
|
|
|
|
// Announce first.
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
|
|
MpReachNlriPathAttribute {
|
|
family: AfiSafi::IPV6_UNICAST,
|
|
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
|
|
prefixes: vec![prefix.into()],
|
|
},
|
|
)),
|
|
announced_routes: vec![],
|
|
}))
|
|
.await?;
|
|
tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
|
|
|
// Now withdraw via MP_UNREACH_NLRI.
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: vec![PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
|
|
family: AfiSafi::IPV6_UNICAST,
|
|
prefixes: vec![prefix.into()],
|
|
})],
|
|
announced_routes: vec![],
|
|
}))
|
|
.await?;
|
|
|
|
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
|
assert!(
|
|
matches!(&event, RouteChange::Withdrawn { prefix: p, .. } if p == &prefix),
|
|
"Expected Withdrawn for 2001:db8::/32, got {event:?}",
|
|
);
|
|
|
|
let snapshot = server.received_routes();
|
|
assert!(
|
|
!snapshot.iter().any(|e| e.prefix == prefix),
|
|
"2001:db8::/32 should have been removed from RIB snapshot",
|
|
);
|
|
Ok(())
|
|
}
|