AFI and SAFI were closed enums, so a family without a variant could not be represented at all, and the set of negotiated families was a struct with one bool per family. Supporting another family meant touching both. - `Afi`, `Safi` and `AfiSafi` in bgp-primitives are newtypes over the wire numbers, with constants and names for the common families. Capabilities and MP_REACH/MP_UNREACH use them, so any family a peer names is read as what it is. The special case that turned a capability for an unlisted family into an unknown capability is gone. - `FamilySet` is a set of `AfiSafi`. What we can exchange routes for is the explicit `family::SUPPORTED` list; only those are offered in our OPEN. - `AddressFamilyId` stays as the IP version of a prefix. - NLRI is still a list of IP prefixes. MP_REACH/MP_UNREACH for a family whose NLRI is something else (labelled, VPN, EVPN, ...) is refused rather than misread as prefixes, which is what happened before for the SAFIs the old enum listed. Bug found on the way: the reserved octet in MP_REACH_NLRI was skipped only for IPv6, so an IPv4 MP_REACH_NLRI was parsed with an extra 0.0.0.0/0 route that the peer never sent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
268 lines
8.9 KiB
Rust
268 lines
8.9 KiB
Rust
//! The active side: the server dials its configured peers and keeps
|
|
//! retrying.
|
|
|
|
use std::net::{Ipv4Addr, SocketAddr};
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
|
use bgp_packet::message::Message;
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
|
use bgp_server::config::PeerConfig;
|
|
use bgp_server::family::FamilySet;
|
|
use bgp_server::session::DialState;
|
|
use eyre::Result;
|
|
use tokio::net::TcpListener;
|
|
|
|
const LOCAL_ASN: u32 = 65000;
|
|
const PEER_ASN: u32 = 65001;
|
|
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
/// Starts a server configured to dial `listener`'s address as peer "r1".
|
|
async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestServer> {
|
|
let endpoint: SocketAddr = listener.local_addr()?;
|
|
TestServer::start_with(LOCAL_ASN, |c| {
|
|
c.connect_retry = Duration::from_millis(200);
|
|
c.peers = vec![PeerConfig {
|
|
name: "r1".into(),
|
|
endpoint,
|
|
remote_asn,
|
|
hold_time_secs: 90,
|
|
local_address: None,
|
|
families: FamilySet::supported(),
|
|
}];
|
|
})
|
|
.await
|
|
}
|
|
|
|
async fn accept(listener: &TcpListener) -> Result<TestPeer> {
|
|
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
|
Ok(TestPeer::from_stream(stream))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
let server = server_dialing(&listener, PEER_ASN).await?;
|
|
|
|
let mut peer = accept(&listener).await?;
|
|
// Both sides send OPEN on connect; the test peer's handshake order works
|
|
// whichever side opened TCP.
|
|
let open = peer
|
|
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
.await?;
|
|
assert_eq!(open.asn, LOCAL_ASN as u16);
|
|
|
|
let mut events = server.registry.subscribe();
|
|
// Either the Up event is still queued or the session is already listed.
|
|
let listed = tokio::time::timeout(TIMEOUT, async {
|
|
loop {
|
|
if let Some(s) = server.registry.list().first().cloned() {
|
|
return s;
|
|
}
|
|
let _ = events.recv().await;
|
|
}
|
|
})
|
|
.await?;
|
|
assert_eq!(listed.name.as_deref(), Some("r1"));
|
|
assert_eq!(listed.peer.asn, PEER_ASN);
|
|
assert_eq!(listed.peer.addr, listener.local_addr()?);
|
|
|
|
// Configured view reflects it; the dialer is not in backoff.
|
|
let (cfg, state) = server
|
|
.registry
|
|
.configured_peers()
|
|
.into_iter()
|
|
.next()
|
|
.unwrap();
|
|
assert_eq!(cfg.name, "r1");
|
|
assert!(
|
|
matches!(state, DialState::Connecting | DialState::Idle),
|
|
"{state:?}"
|
|
);
|
|
|
|
// Local routes flow over the dialled session too.
|
|
server
|
|
.local_rib
|
|
.announce(LocalRoute::new(
|
|
IpPrefix::try_from("10.100.1.0/24")?,
|
|
LocalRouteAttrs::default(),
|
|
))
|
|
.await?;
|
|
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
|
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_server_reconnects_after_peer_closes() -> Result<()> {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
let server = server_dialing(&listener, PEER_ASN).await?;
|
|
|
|
let mut first = accept(&listener).await?;
|
|
first
|
|
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
.await?;
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while server.registry.list().is_empty() {
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
drop(first);
|
|
|
|
// Session goes away, then the dialer comes back.
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while !server.registry.list().is_empty() {
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
let (_, state) = server
|
|
.registry
|
|
.configured_peers()
|
|
.into_iter()
|
|
.next()
|
|
.unwrap();
|
|
assert!(matches!(state, DialState::Backoff { .. }), "{state:?}");
|
|
|
|
let mut second = accept(&listener).await?;
|
|
second
|
|
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
.await?;
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while server.registry.list().is_empty() {
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_wrong_asn_is_refused_with_bad_peer_as() -> Result<()> {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
let _server = server_dialing(&listener, PEER_ASN).await?;
|
|
|
|
let mut peer = accept(&listener).await?;
|
|
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
.await?;
|
|
let _ = peer.recv_open().await?;
|
|
let notif = tokio::time::timeout(TIMEOUT, async {
|
|
loop {
|
|
match peer.recv().await? {
|
|
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
|
|
Message::KeepAlive => {}
|
|
other => eyre::bail!("unexpected {other:?}"),
|
|
}
|
|
}
|
|
})
|
|
.await??;
|
|
assert!(
|
|
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
|
"{notif:?}"
|
|
);
|
|
|
|
// The dialer retries rather than giving up.
|
|
let _again = accept(&listener).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// An inbound connection from a configured peer's address is also checked
|
|
/// against the configured ASN and gets the configured name.
|
|
#[tokio::test]
|
|
async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
|
|
// Configure a peer at the loopback address on a port nobody listens on,
|
|
// so the dialer never succeeds; then connect inbound from loopback.
|
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
c.connect_retry = Duration::from_secs(60);
|
|
c.peers = vec![PeerConfig {
|
|
name: "r1".into(),
|
|
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
|
|
remote_asn: PEER_ASN,
|
|
hold_time_secs: 90,
|
|
local_address: None,
|
|
families: FamilySet::supported(),
|
|
}];
|
|
})
|
|
.await?;
|
|
|
|
// Wrong ASN inbound → refused.
|
|
let mut bad = TestPeer::connect(server.addr).await?;
|
|
bad.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
|
|
let _ = bad.recv_open().await?;
|
|
let msg = tokio::time::timeout(TIMEOUT, bad.recv()).await??;
|
|
assert!(
|
|
matches!(&msg, Message::Notification(n) if matches!(n.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs))),
|
|
"{msg:?}"
|
|
);
|
|
|
|
// Right ASN inbound → established, named.
|
|
let mut good = TestPeer::connect(server.addr).await?;
|
|
good.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
.await?;
|
|
let listed = tokio::time::timeout(TIMEOUT, async {
|
|
loop {
|
|
if let Some(s) = server.registry.list().first().cloned() {
|
|
return s;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
assert_eq!(listed.name.as_deref(), Some("r1"));
|
|
Ok(())
|
|
}
|
|
|
|
/// With `local_address` set the connection must come from that address (the
|
|
/// peer may identify us by it), and an address the host does not have is
|
|
/// reported as the reason for backing off rather than silently ignored.
|
|
#[tokio::test]
|
|
async fn test_local_address_is_used_as_the_source() -> Result<()> {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
let endpoint: SocketAddr = listener.local_addr()?;
|
|
let _server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
c.connect_retry = Duration::from_millis(200);
|
|
c.peers = vec![PeerConfig {
|
|
name: "r1".into(),
|
|
endpoint,
|
|
remote_asn: PEER_ASN,
|
|
hold_time_secs: 90,
|
|
local_address: Some(Ipv4Addr::LOCALHOST.into()),
|
|
families: FamilySet::supported(),
|
|
}];
|
|
})
|
|
.await?;
|
|
let (_stream, from) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
|
assert_eq!(from.ip(), std::net::IpAddr::from(Ipv4Addr::LOCALHOST));
|
|
|
|
// 192.0.2.1 is documentation space; no host has it.
|
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
c.connect_retry = Duration::from_millis(200);
|
|
c.peers = vec![PeerConfig {
|
|
name: "r1".into(),
|
|
endpoint,
|
|
remote_asn: PEER_ASN,
|
|
hold_time_secs: 90,
|
|
local_address: Some(Ipv4Addr::new(192, 0, 2, 1).into()),
|
|
families: FamilySet::supported(),
|
|
}];
|
|
})
|
|
.await?;
|
|
let reason = tokio::time::timeout(TIMEOUT, async {
|
|
loop {
|
|
if let Some((_, DialState::Backoff { last_error, .. })) =
|
|
server.registry.configured_peers().into_iter().next()
|
|
{
|
|
return last_error;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
assert!(reason.contains("local_address 192.0.2.1"), "{reason}");
|
|
Ok(())
|
|
}
|