Files
ferrix/crates/integration-tests/tests/dialer.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 22d0099a46 Negotiate address families and AS number width instead of assuming them
The OPEN used to offer IPv4 and IPv6 unicast unconditionally, and what the
peer offered was ignored apart from its four-octet AS number.

Address families (RFC 4760):
- A peer's `address_families` in the configuration says which to offer it;
  all of them by default.
- A session carries the families both OPENs offered. A peer that sends no
  Multiprotocol capability is a plain RFC 4271 speaker: IPv4 unicast.
- Routes of a family that was not negotiated are not sent, and are ignored
  when received; the rest of the UPDATE is applied and the session stays up.
- A session with no family in common is refused with Unsupported
  Capability, naming the capabilities we needed (RFC 5492).
- The negotiated families are shown on /statusz and in /api/peers.

Bugs found on the way:
- A capability naming an address family we have no constant for (EVPN,
  VPNv4, ...) failed the whole OPEN, so such a peer could never establish.
  It is now kept as an unknown capability and ignored.
- An UPDATE could not be encoded for a peer without four-octet AS support,
  which ended the session on the first route. AS_PATH is now written two
  octets wide with AS_TRANS where needed, and AS4_PATH carries the real
  numbers (RFC 6793).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 22:22:54 +02:00

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::ALL,
}];
})
.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::ALL,
}];
})
.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::ALL,
}];
})
.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::ALL,
}];
})
.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(())
}