A configured peer could announce any prefix with any path, in any number, and all of it was stored and could be chosen as best. Each peer now has an import policy, and every route goes through it before it reaches the routing table. Checks that hold for any peer unless turned off for it: - no prefixes that are not routed on the Internet (private, loopback, link-local, documentation, multicast, reserved; for IPv6, anything outside global unicast); - nothing longer than /24 or /48, no default route, nothing shorter than /8 or /16; - no reserved, private or documentation AS number in the path; - a path that begins with the peer's own AS (turned off for a route server), and of at most 64 AS numbers. Always: a route with our own AS number in its path is refused. RFC 4271 requires this loop check and it was missing. Per peer, if configured: the prefixes it may announce, each with an optional longest more-specific, and the AS numbers its routes may originate from. This is the list generated from the routing registries. max_prefixes limits how many routes a peer may have. One more ends the session with Cease / Maximum Number of Prefixes Reached, and the peer is kept down for hold_down_seconds (ten minutes unless set): its connections are not taken and it is not dialled. Refused routes do not count towards the limit. A refused route is withdrawn rather than ignored, so that it replaces whatever the peer had announced for it before. On a session with two-octet AS numbers the path is judged with its AS4_PATH merged in, so that the real AS numbers are what is checked. Peers that are not configured (accept_unconfigured_peers, for test networks) get a policy that takes everything but loops. Refusals are counted in bgp_routes_rejected_total by reason. The status page and /api/peers show, for each session, how many routes the peer has and how many were refused. The server warns at startup about peers with no limit. The prefix check costs about 4 ns a route with the default policy; the end-to-end benchmark is unchanged. Also: the benchmark's server AS is one that is in none of its generated paths, and a table test that depended on the hasher's seed is fixed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
325 lines
11 KiB
Rust
325 lines
11 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::policy::ImportPolicy;
|
|
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(),
|
|
add_path: None,
|
|
import: ImportPolicy::permissive(),
|
|
}];
|
|
})
|
|
.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(),
|
|
add_path: None,
|
|
import: ImportPolicy::permissive(),
|
|
}];
|
|
})
|
|
.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(),
|
|
add_path: None,
|
|
import: ImportPolicy::permissive(),
|
|
}];
|
|
})
|
|
.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(),
|
|
add_path: None,
|
|
import: ImportPolicy::permissive(),
|
|
}];
|
|
})
|
|
.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(())
|
|
}
|
|
|
|
/// A peer that takes the connection and then refuses the session, here
|
|
/// because it is not the AS we are configured to expect, will refuse it
|
|
/// again. It is dialled ever more slowly, like a peer that cannot be
|
|
/// reached: connecting used to count as success and reset the wait, so
|
|
/// such a peer was dialled every few seconds for ever.
|
|
#[tokio::test]
|
|
async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> Result<()> {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
let server = server_dialing(&listener, PEER_ASN).await?;
|
|
|
|
let mut waits = Vec::new();
|
|
for _ in 0..3 {
|
|
let mut peer = accept(&listener).await?;
|
|
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
.await?;
|
|
let _ = peer.recv_open().await?;
|
|
let refusal = tokio::time::timeout(TIMEOUT, peer.recv()).await??;
|
|
assert!(
|
|
matches!(&refusal, Message::Notification(n) if n.error == BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
|
"{refusal:?}"
|
|
);
|
|
drop(peer);
|
|
|
|
// How long the dialer says it will wait before trying again.
|
|
let wait = tokio::time::timeout(TIMEOUT, async {
|
|
loop {
|
|
if let Some((_, DialState::Backoff { until, last_error })) =
|
|
server.registry.configured_peers().into_iter().next()
|
|
{
|
|
assert!(last_error.contains("AS65099"), "{last_error}");
|
|
return until
|
|
.duration_since(std::time::SystemTime::now())
|
|
.unwrap_or_default();
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
waits.push(wait);
|
|
}
|
|
// 200 ms, then 400, then 800, less what passed before each was read.
|
|
assert!(
|
|
waits[2] > Duration::from_millis(500),
|
|
"the waits were {waits:?}"
|
|
);
|
|
Ok(())
|
|
}
|