Integration tests: one copy of each helper
The test files had grown their own copies of the same helpers: prefix() twelve times, established() seven, an UPDATE builder six, a seventeen field PeerConfig literal in fifteen places, a timeout constant in twenty one. Each is in crates/integration-tests/src once now: peer_config() and TestServer::with_peer for a configured peer, Announcement for building UPDATEs, established(), the waits (wait_until, wait_for, wait_for_routes, wait_for_sessions), get_json, and TIMEOUT. A new field on PeerConfig is one line to add, not fifteen. TestPeer::recv has a deadline, so a test that gets nothing fails instead of hanging; is_closed, which read one byte and took a KEEPALIVE for an open connection, is closed_within and closed_unanswered. Net 788 lines fewer; every test checks what it did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
673920d48b
commit
c218ea4d85
27 files changed
+989
-1667
No files matched your search
@@ -18,13 +18,13 @@ bgp-server.workspace = true
|
|||||||
bgp-web.workspace = true
|
bgp-web.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
eyre.workspace = true
|
eyre.workspace = true
|
||||||
tokio.workspace = true
|
|
||||||
|
|
||||||
# Used by the examples and the tests only.
|
|
||||||
[dev-dependencies]
|
|
||||||
clap.workspace = true
|
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
|
# Used by the examples only.
|
||||||
|
[dev-dependencies]
|
||||||
|
clap.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
@@ -16,11 +16,9 @@
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::{Announcement, withdrawal};
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{Message, PathAttribute, UpdateMessage};
|
||||||
AsPathAttribute, CommunitiesPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute,
|
|
||||||
PathAttribute, UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
use bgp_primitives::ip_prefix::IpPrefix;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
@@ -88,8 +86,8 @@ async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Res
|
|||||||
|
|
||||||
for p in &args.announce {
|
for p in &args.announce {
|
||||||
let prefix = IpPrefix::try_from(p.as_str())?;
|
let prefix = IpPrefix::try_from(p.as_str())?;
|
||||||
let update = announce(prefix, vec![args.asn], vec![(args.asn as u16, 1)]);
|
peer.announce(announce(prefix, vec![args.asn], vec![(args.asn as u16, 1)]))
|
||||||
peer.send(&Message::Update(update)).await?;
|
.await?;
|
||||||
println!("announced {prefix}");
|
println!("announced {prefix}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +96,8 @@ async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Res
|
|||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = ticker.tick() => peer.send(&Message::KeepAlive).await?,
|
_ = ticker.tick() => peer.send(&Message::KeepAlive).await?,
|
||||||
msg = peer.recv() => match msg? {
|
// However long the server is quiet: this peer stays on the line.
|
||||||
|
msg = peer.recv_untimed() => match msg? {
|
||||||
Message::KeepAlive => {}
|
Message::KeepAlive => {}
|
||||||
Message::Update(u) => {
|
Message::Update(u) => {
|
||||||
for w in &u.withdrawn_routes {
|
for w in &u.withdrawn_routes {
|
||||||
@@ -121,22 +120,10 @@ async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Res
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn announce(prefix: IpPrefix, as_path: Vec<u32>, communities: Vec<(u16, u16)>) -> UpdateMessage {
|
fn announce(prefix: IpPrefix, as_path: Vec<u32>, communities: Vec<(u16, u16)>) -> Announcement {
|
||||||
let mut path_attributes = vec![
|
Announcement::of([prefix.into()], &as_path)
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
.with_next_hop(Ipv4Addr::new(10, 0, 0, 2))
|
||||||
AsPathAttribute::from_asns(as_path),
|
.with_communities(&communities)
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 2))),
|
|
||||||
];
|
|
||||||
if !communities.is_empty() {
|
|
||||||
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
|
||||||
communities,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
UpdateMessage {
|
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes,
|
|
||||||
announced_routes: vec![prefix.into()],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<()> {
|
async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<()> {
|
||||||
@@ -157,21 +144,13 @@ async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<(
|
|||||||
}
|
}
|
||||||
for nlri in &u.announced_routes {
|
for nlri in &u.announced_routes {
|
||||||
let prefix = nlri.prefix;
|
let prefix = nlri.prefix;
|
||||||
peer.send(&Message::Update(announce(
|
peer.announce(announce(prefix, as_path.clone(), communities.clone()))
|
||||||
prefix,
|
.await?;
|
||||||
as_path.clone(),
|
|
||||||
communities.clone(),
|
|
||||||
)))
|
|
||||||
.await?;
|
|
||||||
println!(" echoed {prefix} back with path {as_path:?}");
|
println!(" echoed {prefix} back with path {as_path:?}");
|
||||||
}
|
}
|
||||||
if !u.withdrawn_routes.is_empty() {
|
if !u.withdrawn_routes.is_empty() {
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(withdrawal(u.withdrawn_routes.iter().cloned()))
|
||||||
withdrawn_routes: u.withdrawn_routes.clone(),
|
.await?;
|
||||||
path_attributes: vec![],
|
|
||||||
announced_routes: vec![],
|
|
||||||
}))
|
|
||||||
.await?;
|
|
||||||
println!(
|
println!(
|
||||||
" echoed withdraw of {:?}",
|
" echoed withdraw of {:?}",
|
||||||
u.withdrawn_routes
|
u.withdrawn_routes
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
//! The UPDATEs a test peer sends, built from what a test has to say about
|
||||||
|
//! them and nothing more.
|
||||||
|
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
use bgp_packet::message::{
|
||||||
|
AsPathAttribute, CommunitiesPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute,
|
||||||
|
PathAttribute, UpdateMessage,
|
||||||
|
};
|
||||||
|
use bgp_primitives::ip_prefix::Nlri;
|
||||||
|
|
||||||
|
use crate::prefix;
|
||||||
|
|
||||||
|
/// An UPDATE announcing IPv4 routes as the peers of these tests do: with
|
||||||
|
/// ORIGIN IGP, the AS path given, and 192.0.2.1 as the next hop unless
|
||||||
|
/// another is set. Attributes are written in order of type code.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Announcement {
|
||||||
|
routes: Vec<Nlri>,
|
||||||
|
path: Vec<u32>,
|
||||||
|
next_hop: Ipv4Addr,
|
||||||
|
only_to_customer: Option<u32>,
|
||||||
|
communities: Vec<(u16, u16)>,
|
||||||
|
more: Vec<PathAttribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Announces `prefixes`, written as text, with the AS path `path`.
|
||||||
|
pub fn announcement(prefixes: &[&str], path: &[u32]) -> Announcement {
|
||||||
|
Announcement::of(prefixes.iter().map(|p| prefix(p).into()), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Withdraws `routes`.
|
||||||
|
pub fn withdrawal(routes: impl IntoIterator<Item = Nlri>) -> UpdateMessage {
|
||||||
|
UpdateMessage {
|
||||||
|
withdrawn_routes: routes.into_iter().collect(),
|
||||||
|
path_attributes: vec![],
|
||||||
|
announced_routes: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Announcement {
|
||||||
|
/// Announces `routes`, prefixes with or without path identifiers, with
|
||||||
|
/// the AS path `path`.
|
||||||
|
pub fn of(routes: impl IntoIterator<Item = Nlri>, path: &[u32]) -> Self {
|
||||||
|
Self {
|
||||||
|
routes: routes.into_iter().collect(),
|
||||||
|
path: path.to_vec(),
|
||||||
|
next_hop: Ipv4Addr::new(192, 0, 2, 1),
|
||||||
|
only_to_customer: None,
|
||||||
|
communities: Vec::new(),
|
||||||
|
more: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_next_hop(mut self, next_hop: Ipv4Addr) -> Self {
|
||||||
|
self.next_hop = next_hop;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With a COMMUNITIES attribute carrying `communities`, if there are
|
||||||
|
/// any.
|
||||||
|
pub fn with_communities(mut self, communities: &[(u16, u16)]) -> Self {
|
||||||
|
self.communities = communities.to_vec();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marked as only to go to customers, by `asn` (RFC 9234).
|
||||||
|
pub fn with_only_to_customer(mut self, asn: u32) -> Self {
|
||||||
|
self.only_to_customer = Some(asn);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With `attributes` after the mandatory ones, as given.
|
||||||
|
pub fn with_attributes(mut self, attributes: impl IntoIterator<Item = PathAttribute>) -> Self {
|
||||||
|
self.more.extend(attributes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Announcement> for UpdateMessage {
|
||||||
|
fn from(announcement: Announcement) -> Self {
|
||||||
|
let mut path_attributes = vec![
|
||||||
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||||
|
AsPathAttribute::from_asns(announcement.path),
|
||||||
|
PathAttribute::NextHop(NextHopPathAttribute(announcement.next_hop)),
|
||||||
|
];
|
||||||
|
if !announcement.communities.is_empty() {
|
||||||
|
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
||||||
|
announcement.communities,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
path_attributes.extend(
|
||||||
|
announcement
|
||||||
|
.only_to_customer
|
||||||
|
.map(PathAttribute::OnlyToCustomer),
|
||||||
|
);
|
||||||
|
path_attributes.extend(announcement.more);
|
||||||
|
UpdateMessage {
|
||||||
|
withdrawn_routes: vec![],
|
||||||
|
path_attributes,
|
||||||
|
announced_routes: announcement.routes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Announcement> for Message {
|
||||||
|
fn from(announcement: Announcement) -> Self {
|
||||||
|
Message::Update(announcement.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,22 @@
|
|||||||
|
pub mod announcement;
|
||||||
pub mod test_cache;
|
pub mod test_cache;
|
||||||
pub mod test_peer;
|
pub mod test_peer;
|
||||||
pub mod test_server;
|
pub mod test_server;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_primitives::ip_prefix::IpPrefix;
|
||||||
|
use bgp_server::address::Endpoint;
|
||||||
|
use bgp_server::config::PeerConfig;
|
||||||
|
use bgp_server::family::FamilySet;
|
||||||
|
use eyre::{Result, bail};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// How long a test waits for something the server does on its own: a
|
||||||
|
/// route reaching the table, a session coming up, a message arriving.
|
||||||
|
/// Everything happens on the loopback, so it is generous.
|
||||||
|
pub const TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
/// The policy a peer has with nothing configured, but for one thing: the
|
/// The policy a peer has with nothing configured, but for one thing: the
|
||||||
/// peers of these tests are all at 127.0.0.1, which no route can have as
|
/// peers of these tests are all at 127.0.0.1, which no route can have as
|
||||||
/// its next hop, so their routes name another.
|
/// its next hop, so their routes name another.
|
||||||
@@ -11,3 +26,74 @@ pub fn default_policy() -> bgp_server::policy::ImportPolicy {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A prefix written as text. The tests' prefixes are literals, so a bad
|
||||||
|
/// one is a mistake in the test.
|
||||||
|
pub fn prefix(s: &str) -> IpPrefix {
|
||||||
|
IpPrefix::try_from(s).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A configured peer with nothing said about it but where it is and what
|
||||||
|
/// AS it is in: the default hold time, every family, the default policy of
|
||||||
|
/// these tests, and no role, limits, groups or ADD-PATH. A test says what
|
||||||
|
/// else with struct update syntax: `PeerConfig { role: Some(..),
|
||||||
|
/// ..peer_config(..) }`.
|
||||||
|
pub fn peer_config(name: &str, endpoint: impl Into<Endpoint>, remote_asn: u32) -> PeerConfig {
|
||||||
|
PeerConfig {
|
||||||
|
name: name.into(),
|
||||||
|
endpoint: endpoint.into(),
|
||||||
|
remote_asn,
|
||||||
|
hold_time_secs: 90,
|
||||||
|
local_address: None,
|
||||||
|
families: FamilySet::supported(),
|
||||||
|
add_path: None,
|
||||||
|
import: default_policy(),
|
||||||
|
role: None,
|
||||||
|
require_role: false,
|
||||||
|
export: Default::default(),
|
||||||
|
export_limits: None,
|
||||||
|
graceful_restart: Default::default(),
|
||||||
|
groups: Vec::new(),
|
||||||
|
reflector_client: false,
|
||||||
|
passive: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until `done` holds, looking every few milliseconds, or fails
|
||||||
|
/// with `what` once [`TIMEOUT`] has passed.
|
||||||
|
pub async fn wait_until(what: &str, mut done: impl FnMut() -> bool) -> Result<()> {
|
||||||
|
let waited = tokio::time::timeout(TIMEOUT, async {
|
||||||
|
while !done() {
|
||||||
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
if waited.is_err() {
|
||||||
|
bail!("timed out waiting until {what}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until `get` has something, and returns it; fails with `what`
|
||||||
|
/// once [`TIMEOUT`] has passed.
|
||||||
|
pub async fn wait_for<T>(what: &str, mut get: impl FnMut() -> Option<T>) -> Result<T> {
|
||||||
|
let mut got = None;
|
||||||
|
wait_until(what, || {
|
||||||
|
got = get();
|
||||||
|
got.is_some()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(got.expect("the wait ended with something"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `GET`s `url` and reads the JSON it answers with. An error status is an
|
||||||
|
/// error.
|
||||||
|
pub async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
|
||||||
|
Ok(client
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json()
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use bgp_packet::message::{BGP_HEADER_LEN, Message, UpdateMessage};
|
use bgp_packet::message::{BGP_HEADER_LEN, Message, NotificationMessage, UpdateMessage};
|
||||||
use bgp_packet::open::{Capability, OpenMessage, OpenOption};
|
use bgp_packet::open::{Capability, OpenMessage, OpenOption};
|
||||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||||
use bgp_primitives::parser::ParserContext;
|
use bgp_primitives::parser::ParserContext;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use eyre::{Result, bail, eyre};
|
use eyre::{Result, bail, eyre};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
|
||||||
|
use crate::TIMEOUT;
|
||||||
|
|
||||||
/// A minimal BGP peer for use in tests. Connects over TCP and can exchange
|
/// A minimal BGP peer for use in tests. Connects over TCP and can exchange
|
||||||
/// framed BGP messages using the `bgp-packet` wire format.
|
/// framed BGP messages using the `bgp-packet` wire format.
|
||||||
@@ -30,6 +33,13 @@ impl TestPeer {
|
|||||||
Ok(Self::from_stream(stream))
|
Ok(Self::from_stream(stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The next connection to `listener`, as the server dials it. It is
|
||||||
|
/// waited for at most [`TIMEOUT`].
|
||||||
|
pub async fn accept(listener: &TcpListener) -> Result<Self> {
|
||||||
|
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
||||||
|
Ok(Self::from_stream(stream))
|
||||||
|
}
|
||||||
|
|
||||||
/// Wraps an already-connected stream, e.g. one accepted from a server
|
/// Wraps an already-connected stream, e.g. one accepted from a server
|
||||||
/// that dialled us.
|
/// that dialled us.
|
||||||
pub fn from_stream(stream: TcpStream) -> Self {
|
pub fn from_stream(stream: TcpStream) -> Self {
|
||||||
@@ -41,6 +51,11 @@ impl TestPeer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Our end of the connection: what the server sees the peer as.
|
||||||
|
pub fn local_addr(&self) -> Result<SocketAddr> {
|
||||||
|
Ok(self.stream.local_addr()?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Send a BGP message.
|
/// Send a BGP message.
|
||||||
pub async fn send(&mut self, msg: &Message) -> Result<()> {
|
pub async fn send(&mut self, msg: &Message) -> Result<()> {
|
||||||
let send_ctx = ParserContext {
|
let send_ctx = ParserContext {
|
||||||
@@ -53,8 +68,32 @@ impl TestPeer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read exactly one BGP message.
|
/// Send an UPDATE: an [`Announcement`](crate::announcement::Announcement),
|
||||||
|
/// a [`withdrawal`](crate::announcement::withdrawal), or one built by
|
||||||
|
/// hand.
|
||||||
|
pub async fn announce(&mut self, update: impl Into<UpdateMessage>) -> Result<()> {
|
||||||
|
self.send(&Message::Update(update.into())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read exactly one BGP message, waiting at most [`TIMEOUT`] for it. A
|
||||||
|
/// test that expects nothing to arrive says for how long with
|
||||||
|
/// [`TestPeer::recv_within`].
|
||||||
pub async fn recv(&mut self) -> Result<Message> {
|
pub async fn recv(&mut self) -> Result<Message> {
|
||||||
|
self.recv_within(TIMEOUT).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read exactly one BGP message, waiting at most `wait` for it.
|
||||||
|
pub async fn recv_within(&mut self, wait: Duration) -> Result<Message> {
|
||||||
|
match tokio::time::timeout(wait, self.recv_untimed()).await {
|
||||||
|
Ok(message) => message,
|
||||||
|
Err(_) => bail!("no message from the server within {wait:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read exactly one BGP message, however long it takes to come. For a
|
||||||
|
/// peer that is to stay on the line, like the `fake_peer` example;
|
||||||
|
/// tests want [`TestPeer::recv`].
|
||||||
|
pub async fn recv_untimed(&mut self) -> Result<Message> {
|
||||||
let mut header = [0u8; BGP_HEADER_LEN as usize];
|
let mut header = [0u8; BGP_HEADER_LEN as usize];
|
||||||
self.stream.read_exact(&mut header).await?;
|
self.stream.read_exact(&mut header).await?;
|
||||||
|
|
||||||
@@ -84,6 +123,18 @@ impl TestPeer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read messages until a NOTIFICATION arrives, skipping KEEPALIVEs. Any
|
||||||
|
/// other message type is an error.
|
||||||
|
pub async fn expect_notification(&mut self) -> Result<NotificationMessage> {
|
||||||
|
loop {
|
||||||
|
match self.recv().await? {
|
||||||
|
Message::Notification(n) => return Ok(n),
|
||||||
|
Message::KeepAlive => {}
|
||||||
|
other => bail!("expected NOTIFICATION, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Receive and unwrap as `OpenMessage`, failing if a different type arrives.
|
/// Receive and unwrap as `OpenMessage`, failing if a different type arrives.
|
||||||
pub async fn recv_open(&mut self) -> Result<OpenMessage> {
|
pub async fn recv_open(&mut self) -> Result<OpenMessage> {
|
||||||
match self.recv().await? {
|
match self.recv().await? {
|
||||||
@@ -207,12 +258,23 @@ impl TestPeer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the remote end has closed the connection.
|
/// Whether the server closes the connection within `wait`. Whatever it
|
||||||
pub async fn is_closed(&mut self) -> bool {
|
/// still sends first, KEEPALIVEs as a rule, is read past: the end of
|
||||||
let mut buf = [0u8; 1];
|
/// the stream, or anything unreadable, is what counts.
|
||||||
match self.stream.read(&mut buf).await {
|
pub async fn closed_within(&mut self, wait: Duration) -> bool {
|
||||||
Ok(0) | Err(_) => true,
|
tokio::time::timeout(wait, async { while self.recv_untimed().await.is_ok() {} })
|
||||||
Ok(_) => false,
|
.await
|
||||||
}
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the server closes the connection within [`TIMEOUT`] without
|
||||||
|
/// having sent anything: what a peer that is not let in sees. A server
|
||||||
|
/// that sends its OPEN first, or that leaves the connection open, is
|
||||||
|
/// answering.
|
||||||
|
pub async fn closed_unanswered(&mut self) -> bool {
|
||||||
|
matches!(
|
||||||
|
tokio::time::timeout(TIMEOUT, self.recv_untimed()).await,
|
||||||
|
Ok(Err(_))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,28 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bgp_fib::Fib;
|
use bgp_fib::Fib;
|
||||||
use bgp_packet::message::{NlriNextHop, PathAttribute};
|
use bgp_packet::message::{NlriNextHop, PathAttribute};
|
||||||
|
use bgp_packet::open::Capability;
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
use bgp_primitives::ip_prefix::IpPrefix;
|
||||||
use bgp_rib::LocalRib;
|
use bgp_rib::LocalRib;
|
||||||
use bgp_rib::table::{Event, PeerIndex, Reader, RibTable};
|
use bgp_rib::table::{Event, PeerIndex, Reader, RibTable};
|
||||||
use bgp_rpki::Rpki;
|
use bgp_rpki::Rpki;
|
||||||
use bgp_server::config::Config;
|
use bgp_server::config::{Config, PeerConfig};
|
||||||
use bgp_server::error::ServerError;
|
use bgp_server::error::ServerError;
|
||||||
use bgp_server::refusals::RefusalLog;
|
use bgp_server::refusals::RefusalLog;
|
||||||
use bgp_server::restart::Restarts;
|
use bgp_server::restart::Restarts;
|
||||||
use bgp_server::server::BgpServer;
|
use bgp_server::server::BgpServer;
|
||||||
use bgp_server::session::SessionRegistry;
|
use bgp_server::session::{SessionHandle, SessionRegistry};
|
||||||
use bgp_web::{AppState, RouterInfo};
|
use bgp_web::{AppState, RouterInfo};
|
||||||
use eyre::Result;
|
use eyre::{Result, eyre};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
|
use crate::test_peer::TestPeer;
|
||||||
|
use crate::{peer_config, wait_for, wait_until};
|
||||||
|
|
||||||
/// A BGP server started on an ephemeral port for testing.
|
/// A BGP server started on an ephemeral port for testing.
|
||||||
/// Aborts the server task when dropped.
|
/// Aborts the server task when dropped.
|
||||||
pub struct TestServer {
|
pub struct TestServer {
|
||||||
@@ -106,6 +111,129 @@ impl TestServer {
|
|||||||
local_asn,
|
local_asn,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A server in `local_asn` that takes connections from one configured
|
||||||
|
/// peer only: "r1", in `remote_asn`, at the loopback address the test
|
||||||
|
/// connects from. `customize` edits the configuration and the peer's
|
||||||
|
/// before the server starts. The server dials the peer too; where that
|
||||||
|
/// goes is returned with it, for the test to answer or to leave alone.
|
||||||
|
pub async fn with_peer(
|
||||||
|
local_asn: u32,
|
||||||
|
remote_asn: u32,
|
||||||
|
customize: impl FnOnce(&mut Config, &mut PeerConfig),
|
||||||
|
) -> Result<ServerWithPeer> {
|
||||||
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
||||||
|
let endpoint = listener.local_addr()?;
|
||||||
|
let server = Self::start_with(local_asn, |config| {
|
||||||
|
config.accept_unconfigured_peers = false;
|
||||||
|
let mut peer = peer_config("r1", endpoint, remote_asn);
|
||||||
|
customize(config, &mut peer);
|
||||||
|
config.peers = vec![peer];
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(ServerWithPeer { server, listener })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A server with one configured peer, and the listener at the peer's
|
||||||
|
/// address, which the server's dialling goes to.
|
||||||
|
pub struct ServerWithPeer {
|
||||||
|
pub server: TestServer,
|
||||||
|
pub listener: TcpListener,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestServer {
|
||||||
|
/// A session with the server from a peer in `asn` with `router_id`,
|
||||||
|
/// offering the capabilities of a typical dual-stack speaker. Returns
|
||||||
|
/// once the server lists the session as established. Distinct peers
|
||||||
|
/// need distinct identifiers, or the second connection is (correctly)
|
||||||
|
/// treated as a collision with the first.
|
||||||
|
pub async fn established(&self, asn: u32, router_id: Ipv4Addr) -> Result<TestPeer> {
|
||||||
|
let mut peer = TestPeer::connect(self.addr).await?;
|
||||||
|
peer.handshake(asn, router_id, 90).await?;
|
||||||
|
self.listed(&peer).await?;
|
||||||
|
Ok(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// As [`TestServer::established`], with an OPEN carrying exactly
|
||||||
|
/// `capabilities`.
|
||||||
|
pub async fn established_with(
|
||||||
|
&self,
|
||||||
|
asn: u32,
|
||||||
|
router_id: Ipv4Addr,
|
||||||
|
capabilities: Vec<Capability>,
|
||||||
|
) -> Result<TestPeer> {
|
||||||
|
let mut peer = TestPeer::connect(self.addr).await?;
|
||||||
|
peer.handshake_with(asn, router_id, 90, capabilities)
|
||||||
|
.await?;
|
||||||
|
self.listed(&peer).await?;
|
||||||
|
Ok(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until the registry lists `peer`'s session.
|
||||||
|
async fn listed(&self, peer: &TestPeer) -> Result<()> {
|
||||||
|
let from = peer.local_addr()?;
|
||||||
|
wait_until("the session is established", || {
|
||||||
|
self.registry
|
||||||
|
.list()
|
||||||
|
.iter()
|
||||||
|
.any(|session| session.peer.addr == from)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until the registry lists `n` established sessions.
|
||||||
|
pub async fn wait_for_sessions(&self, n: usize) -> Result<()> {
|
||||||
|
wait_until(&format!("{n} sessions are established"), || {
|
||||||
|
self.registry.list().len() == n
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until a session is established, and returns it.
|
||||||
|
pub async fn wait_for_session(&self) -> Result<SessionHandle> {
|
||||||
|
wait_for("a session is established", || {
|
||||||
|
self.registry.list().into_iter().next()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until the table holds `routes` routes.
|
||||||
|
pub async fn wait_for_routes(&self, routes: usize) -> Result<()> {
|
||||||
|
wait_until(&format!("the table holds {routes} routes"), || {
|
||||||
|
self.rib.len() == routes
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until the table holds exactly the prefixes `expected` and the
|
||||||
|
/// peer has `held` routes held aside.
|
||||||
|
pub async fn table_becomes(&self, expected: &[&str], held: u64) -> Result<()> {
|
||||||
|
let expected: BTreeSet<String> = expected.iter().map(|p| p.to_string()).collect();
|
||||||
|
let table = || -> BTreeSet<String> {
|
||||||
|
self.received_routes()
|
||||||
|
.iter()
|
||||||
|
.map(|route| route.prefix.to_string())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let held_now = || {
|
||||||
|
self.registry
|
||||||
|
.list()
|
||||||
|
.first()
|
||||||
|
.map_or(0, |session| session.counters.held())
|
||||||
|
};
|
||||||
|
wait_until("the table is as expected", || {
|
||||||
|
table() == expected && held_now() == held
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
eyre!(
|
||||||
|
"the table has {:?} with {} held; expected {expected:?} with {held} held",
|
||||||
|
table(),
|
||||||
|
held_now()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A route in the server's table, as a test wants to look at it.
|
/// A route in the server's table, as a test wants to look at it.
|
||||||
|
|||||||
@@ -4,29 +4,23 @@
|
|||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::{Announcement, withdrawal};
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::TestServer;
|
||||||
use bgp_packet::message::{
|
use bgp_integration_tests::{peer_config, prefix, wait_until};
|
||||||
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
use bgp_packet::message::PathAttribute;
|
||||||
UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_packet::open::{AddPathEntry, AddPathSendRecv, Capability, OpenMessage};
|
use bgp_packet::open::{AddPathEntry, AddPathSendRecv, Capability, OpenMessage};
|
||||||
use bgp_primitives::constants::AfiSafi;
|
use bgp_primitives::constants::AfiSafi;
|
||||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
use bgp_primitives::ip_prefix::Nlri;
|
||||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||||
use bgp_server::config::PeerConfig;
|
use bgp_server::config::PeerConfig;
|
||||||
use bgp_server::family::{FamilySet, SUPPORTED};
|
use bgp_server::family::SUPPORTED;
|
||||||
use bgp_server::policy::ImportPolicy;
|
use bgp_server::policy::ImportPolicy;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A server on which connections from this machine are those of a
|
/// A server on which connections from this machine are those of a
|
||||||
/// configured peer, with ADD-PATH offered to it as `add_path` says.
|
/// configured peer, with ADD-PATH offered to it as `add_path` says.
|
||||||
@@ -39,23 +33,11 @@ async fn server_offering(
|
|||||||
.map(|p| LocalRoute::new(prefix(p), LocalRouteAttrs::default()))
|
.map(|p| LocalRoute::new(prefix(p), LocalRouteAttrs::default()))
|
||||||
.collect();
|
.collect();
|
||||||
TestServer::start_with(LOCAL_ASN, |c| {
|
TestServer::start_with(LOCAL_ASN, |c| {
|
||||||
|
let at = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1);
|
||||||
c.peers = vec![PeerConfig {
|
c.peers = vec![PeerConfig {
|
||||||
graceful_restart: Default::default(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "route-server-client".into(),
|
|
||||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path,
|
add_path,
|
||||||
import: ImportPolicy::permissive(),
|
import: ImportPolicy::permissive(),
|
||||||
role: None,
|
..peer_config("route-server-client", at, PEER_ASN)
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
}];
|
||||||
c.originate = originate;
|
c.originate = originate;
|
||||||
// Keep the dialer out of the way.
|
// Keep the dialer out of the way.
|
||||||
@@ -85,12 +67,7 @@ async fn peer_offering(
|
|||||||
let open = peer
|
let open = peer
|
||||||
.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90, capabilities)
|
.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90, capabilities)
|
||||||
.await?;
|
.await?;
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
server.wait_for_session().await?;
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok((peer, open))
|
Ok((peer, open))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,26 +82,8 @@ fn add_path_offered(open: &OpenMessage) -> Vec<(AfiSafi, AddPathSendRecv)> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn announcement(nlri: Nlri, as_path: &[u32]) -> Message {
|
fn announcement(nlri: Nlri, as_path: &[u32]) -> Announcement {
|
||||||
Message::Update(UpdateMessage {
|
Announcement::of([nlri], as_path)
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes: vec![
|
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(as_path.to_vec()),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
],
|
|
||||||
announced_routes: vec![nlri],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wait_for_routes(server: &TestServer, routes: usize) -> Result<()> {
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.rib.len() != routes {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The routes the server holds, as (path identifier, AS path, best).
|
/// The routes the server holds, as (path identifier, AS path, best).
|
||||||
@@ -172,11 +131,11 @@ async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
|
|||||||
|
|
||||||
let p = prefix("203.0.113.0/24");
|
let p = prefix("203.0.113.0/24");
|
||||||
let path = |path_id| Nlri { prefix: p, path_id };
|
let path = |path_id| Nlri { prefix: p, path_id };
|
||||||
peer.send(&announcement(path(1), &[PEER_ASN, 64999, 64998]))
|
peer.announce(announcement(path(1), &[PEER_ASN, 64999, 64998]))
|
||||||
.await?;
|
.await?;
|
||||||
peer.send(&announcement(path(2), &[PEER_ASN, 64998]))
|
peer.announce(announcement(path(2), &[PEER_ASN, 64998]))
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_routes(&server, 2).await?;
|
server.wait_for_routes(2).await?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
held(&server),
|
held(&server),
|
||||||
[
|
[
|
||||||
@@ -209,34 +168,24 @@ async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Announcing a path again replaces that path only.
|
// Announcing a path again replaces that path only.
|
||||||
peer.send(&announcement(path(1), &[PEER_ASN])).await?;
|
peer.announce(announcement(path(1), &[PEER_ASN])).await?;
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("the path is replaced", || held(&server)[0].1.len() == 1).await?;
|
||||||
while held(&server)[0].1.len() != 1 {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
held(&server),
|
held(&server),
|
||||||
[(1, vec![PEER_ASN], true), (2, vec![PEER_ASN, 64998], false),]
|
[(1, vec![PEER_ASN], true), (2, vec![PEER_ASN, 64998], false),]
|
||||||
);
|
);
|
||||||
|
|
||||||
// And withdrawing one leaves the other.
|
// And withdrawing one leaves the other.
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(withdrawal([path(1)])).await?;
|
||||||
withdrawn_routes: vec![path(1)],
|
server.wait_for_routes(1).await?;
|
||||||
path_attributes: vec![],
|
|
||||||
announced_routes: vec![],
|
|
||||||
}))
|
|
||||||
.await?;
|
|
||||||
wait_for_routes(&server, 1).await?;
|
|
||||||
assert_eq!(held(&server), [(2, vec![PEER_ASN, 64998], true)]);
|
assert_eq!(held(&server), [(2, vec![PEER_ASN, 64998], true)]);
|
||||||
|
|
||||||
// When the session ends, all of its routes go.
|
// When the session ends, all of its routes go.
|
||||||
peer.send(&announcement(path(3), &[PEER_ASN, 1, 2, 3]))
|
peer.announce(announcement(path(3), &[PEER_ASN, 1, 2, 3]))
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_routes(&server, 2).await?;
|
server.wait_for_routes(2).await?;
|
||||||
drop(peer);
|
drop(peer);
|
||||||
wait_for_routes(&server, 0).await?;
|
server.wait_for_routes(0).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,18 +201,18 @@ async fn test_add_path_is_not_used_unless_both_sides_offer_it() -> Result<()> {
|
|||||||
assert!(session.add_path_receive.is_empty() && session.add_path_send.is_empty());
|
assert!(session.add_path_receive.is_empty() && session.add_path_send.is_empty());
|
||||||
|
|
||||||
// Plain prefixes both ways.
|
// Plain prefixes both ways.
|
||||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
let update = peer.recv_update().await?;
|
||||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||||
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
peer.announce(announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_routes(&server, 1).await?;
|
server.wait_for_routes(1).await?;
|
||||||
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
|
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
|
||||||
|
|
||||||
// And the other way round: offered by the server, not by the peer.
|
// And the other way round: offered by the server, not by the peer.
|
||||||
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
|
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
|
||||||
let (mut peer, open) = peer_offering(&server, None).await?;
|
let (mut peer, open) = peer_offering(&server, None).await?;
|
||||||
assert_eq!(add_path_offered(&open).len(), SUPPORTED.len());
|
assert_eq!(add_path_offered(&open).len(), SUPPORTED.len());
|
||||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
let update = peer.recv_update().await?;
|
||||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -284,15 +233,15 @@ async fn test_prefixes_are_sent_with_path_identifiers_when_agreed() -> Result<()
|
|||||||
|
|
||||||
// Read as agreed: with identifiers.
|
// Read as agreed: with identifiers.
|
||||||
peer.ctx.add_path = vec![AfiSafi::IPV4_UNICAST];
|
peer.ctx.add_path = vec![AfiSafi::IPV4_UNICAST];
|
||||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
let update = peer.recv_update().await?;
|
||||||
assert_eq!(update.announced_routes.len(), 1);
|
assert_eq!(update.announced_routes.len(), 1);
|
||||||
assert_eq!(update.announced_routes[0].prefix, prefix("10.100.0.0/16"));
|
assert_eq!(update.announced_routes[0].prefix, prefix("10.100.0.0/16"));
|
||||||
|
|
||||||
// The peer did not offer to send, so the server reads its prefixes
|
// The peer did not offer to send, so the server reads its prefixes
|
||||||
// plain.
|
// plain.
|
||||||
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
peer.announce(announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_routes(&server, 1).await?;
|
server.wait_for_routes(1).await?;
|
||||||
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
|
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -5,42 +5,14 @@ use std::net::{Ipv4Addr, SocketAddr};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
|
use bgp_integration_tests::{TIMEOUT, peer_config};
|
||||||
use bgp_server::config::PeerConfig;
|
use bgp_server::config::PeerConfig;
|
||||||
use bgp_server::family::FamilySet;
|
|
||||||
use bgp_server::policy::ImportPolicy;
|
use bgp_server::policy::ImportPolicy;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
fn peer_at(endpoint: SocketAddr) -> PeerConfig {
|
|
||||||
PeerConfig {
|
|
||||||
graceful_restart: Default::default(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import: ImportPolicy::permissive(),
|
|
||||||
role: None,
|
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the server closed the connection without sending anything.
|
|
||||||
async fn closed_unanswered(peer: &mut TestPeer) -> bool {
|
|
||||||
matches!(tokio::time::timeout(TIMEOUT, peer.recv()).await, Ok(Err(_)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A host that is not a configured peer gets nothing: no OPEN to answer,
|
/// A host that is not a configured peer gets nothing: no OPEN to answer,
|
||||||
/// no session, no routes taken from it. Any host used to be able to open a
|
/// no session, no routes taken from it. Any host used to be able to open a
|
||||||
@@ -50,12 +22,16 @@ async fn test_a_host_that_is_not_a_configured_peer_is_refused() -> Result<()> {
|
|||||||
// The only peer configured is somewhere else (documentation space).
|
// The only peer configured is somewhere else (documentation space).
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||||
c.accept_unconfigured_peers = false;
|
c.accept_unconfigured_peers = false;
|
||||||
c.peers = vec![peer_at("192.0.2.9:179".parse().unwrap())];
|
let elsewhere: SocketAddr = "192.0.2.9:179".parse().unwrap();
|
||||||
|
c.peers = vec![PeerConfig {
|
||||||
|
import: ImportPolicy::permissive(),
|
||||||
|
..peer_config("r1", elsewhere, PEER_ASN)
|
||||||
|
}];
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut stranger = TestPeer::connect(server.addr).await?;
|
let mut stranger = TestPeer::connect(server.addr).await?;
|
||||||
assert!(closed_unanswered(&mut stranger).await);
|
assert!(stranger.closed_unanswered().await);
|
||||||
assert!(server.registry.list().is_empty());
|
assert!(server.registry.list().is_empty());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -64,24 +40,18 @@ async fn test_a_host_that_is_not_a_configured_peer_is_refused() -> Result<()> {
|
|||||||
/// configured for that peer.
|
/// configured for that peer.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_configured_peer_is_let_in() -> Result<()> {
|
async fn test_a_configured_peer_is_let_in() -> Result<()> {
|
||||||
// Somewhere for the server's own dialling to go; never answered.
|
// The server's own dialling goes to the listener; it is never answered.
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer {
|
||||||
let endpoint = listener.local_addr()?;
|
server,
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
listener: _listener,
|
||||||
c.accept_unconfigured_peers = false;
|
} = TestServer::with_peer(LOCAL_ASN, PEER_ASN, |_, peer| {
|
||||||
c.peers = vec![peer_at(endpoint)];
|
peer.import = ImportPolicy::permissive();
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let _peer = server
|
||||||
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
.established(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2))
|
||||||
.await?;
|
.await?;
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
assert_eq!(server.registry.list()[0].name.as_deref(), Some("r1"));
|
assert_eq!(server.registry.list()[0].name.as_deref(), Some("r1"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -97,11 +67,11 @@ async fn test_one_address_may_open_only_a_few_connections() -> Result<()> {
|
|||||||
let mut held = Vec::new();
|
let mut held = Vec::new();
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
tokio::time::timeout(TIMEOUT, peer.recv_open()).await??;
|
peer.recv_open().await?;
|
||||||
held.push(peer);
|
held.push(peer);
|
||||||
}
|
}
|
||||||
let mut one_too_many = TestPeer::connect(server.addr).await?;
|
let mut one_too_many = TestPeer::connect(server.addr).await?;
|
||||||
assert!(closed_unanswered(&mut one_too_many).await);
|
assert!(one_too_many.closed_unanswered().await);
|
||||||
|
|
||||||
// When one of them goes, there is room again.
|
// When one of them goes, there is room again.
|
||||||
held.pop();
|
held.pop();
|
||||||
|
|||||||
@@ -4,25 +4,21 @@
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::announcement;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::{RouteChange, TestServer};
|
use bgp_integration_tests::test_server::{RouteChange, TestServer};
|
||||||
|
use bgp_integration_tests::{TIMEOUT, prefix};
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{
|
||||||
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
|
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
|
||||||
LocalPrefPathAttribute, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
LocalPrefPathAttribute, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
||||||
UpdateMessage,
|
UpdateMessage,
|
||||||
};
|
};
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||||
use bgp_server::session::SessionEvent;
|
use bgp_server::session::SessionEvent;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn route(p: &str, attrs: LocalRouteAttrs) -> LocalRoute {
|
fn route(p: &str, attrs: LocalRouteAttrs) -> LocalRoute {
|
||||||
LocalRoute::new(prefix(p), attrs)
|
LocalRoute::new(prefix(p), attrs)
|
||||||
@@ -32,27 +28,12 @@ async fn established_peer(server: &TestServer) -> Result<TestPeer> {
|
|||||||
established_peer_with_id(server, Ipv4Addr::new(10, 0, 0, 1)).await
|
established_peer_with_id(server, Ipv4Addr::new(10, 0, 0, 1)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Distinct routers need distinct BGP Identifiers, or the second connection
|
|
||||||
/// is (correctly) treated as a collision with the first.
|
|
||||||
async fn established_peer_with_id(server: &TestServer, router_id: Ipv4Addr) -> Result<TestPeer> {
|
async fn established_peer_with_id(server: &TestServer, router_id: Ipv4Addr) -> Result<TestPeer> {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
server.established(PEER_ASN, router_id).await
|
||||||
peer.handshake(PEER_ASN, router_id, 90).await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn next_update(peer: &mut TestPeer) -> Result<UpdateMessage> {
|
async fn next_update(peer: &mut TestPeer) -> Result<UpdateMessage> {
|
||||||
tokio::time::timeout(TIMEOUT, peer.recv_update()).await?
|
peer.recv_update().await
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits until the registry reports `n` established sessions.
|
|
||||||
async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> {
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().len() != n {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -97,7 +78,7 @@ async fn test_registry_tracks_session_lifecycle() -> Result<()> {
|
|||||||
async fn test_announce_after_session_up() -> Result<()> {
|
async fn test_announce_after_session_up() -> Result<()> {
|
||||||
let server = TestServer::start(LOCAL_ASN).await?;
|
let server = TestServer::start(LOCAL_ASN).await?;
|
||||||
let mut peer = established_peer(&server).await?;
|
let mut peer = established_peer(&server).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
|
|
||||||
server
|
server
|
||||||
.local_rib
|
.local_rib
|
||||||
@@ -174,7 +155,7 @@ async fn test_announce_reaches_all_sessions() -> Result<()> {
|
|||||||
let server = TestServer::start(LOCAL_ASN).await?;
|
let server = TestServer::start(LOCAL_ASN).await?;
|
||||||
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
|
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
|
||||||
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
|
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
|
||||||
wait_for_sessions(&server, 2).await?;
|
server.wait_for_sessions(2).await?;
|
||||||
|
|
||||||
server
|
server
|
||||||
.local_rib
|
.local_rib
|
||||||
@@ -192,7 +173,7 @@ async fn test_announce_reaches_all_sessions() -> Result<()> {
|
|||||||
async fn test_withdraw_sends_withdrawn_route() -> Result<()> {
|
async fn test_withdraw_sends_withdrawn_route() -> Result<()> {
|
||||||
let server = TestServer::start(LOCAL_ASN).await?;
|
let server = TestServer::start(LOCAL_ASN).await?;
|
||||||
let mut peer = established_peer(&server).await?;
|
let mut peer = established_peer(&server).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
|
|
||||||
server
|
server
|
||||||
.local_rib
|
.local_rib
|
||||||
@@ -213,7 +194,7 @@ async fn test_withdraw_sends_withdrawn_route() -> Result<()> {
|
|||||||
async fn test_reannounce_with_prepend_and_med() -> Result<()> {
|
async fn test_reannounce_with_prepend_and_med() -> Result<()> {
|
||||||
let server = TestServer::start(LOCAL_ASN).await?;
|
let server = TestServer::start(LOCAL_ASN).await?;
|
||||||
let mut peer = established_peer(&server).await?;
|
let mut peer = established_peer(&server).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
|
|
||||||
server
|
server
|
||||||
.local_rib
|
.local_rib
|
||||||
@@ -258,7 +239,7 @@ async fn test_keepalives_continue_alongside_updates() -> Result<()> {
|
|||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 3)
|
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 3)
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
|
|
||||||
server
|
server
|
||||||
.local_rib
|
.local_rib
|
||||||
@@ -289,22 +270,12 @@ async fn test_received_routes_removed_when_session_closes() -> Result<()> {
|
|||||||
let mut rib_events = server.route_changes();
|
let mut rib_events = server.route_changes();
|
||||||
let mut session_events = server.registry.subscribe();
|
let mut session_events = server.registry.subscribe();
|
||||||
let mut peer = established_peer(&server).await?;
|
let mut peer = established_peer(&server).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
|
|
||||||
peer.send(&bgp_packet::message::Message::Update(
|
peer.announce(
|
||||||
bgp_packet::message::UpdateMessage {
|
announcement(&["203.0.113.0/24", "198.51.100.0/24"], &[PEER_ASN])
|
||||||
withdrawn_routes: vec![],
|
.with_next_hop(Ipv4Addr::new(10, 0, 0, 1)),
|
||||||
path_attributes: vec![
|
)
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))),
|
|
||||||
],
|
|
||||||
announced_routes: vec![
|
|
||||||
prefix("203.0.113.0/24").into(),
|
|
||||||
prefix("198.51.100.0/24").into(),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.await?;
|
.await?;
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
let ev = tokio::time::timeout(TIMEOUT, rib_events.recv()).await??;
|
let ev = tokio::time::timeout(TIMEOUT, rib_events.recv()).await??;
|
||||||
@@ -351,7 +322,7 @@ const ROUTE_REFRESH_IPV4: &[u8] = &[
|
|||||||
async fn test_route_refresh_resends_local_routes() -> Result<()> {
|
async fn test_route_refresh_resends_local_routes() -> Result<()> {
|
||||||
let server = TestServer::start(LOCAL_ASN).await?;
|
let server = TestServer::start(LOCAL_ASN).await?;
|
||||||
let mut peer = established_peer(&server).await?;
|
let mut peer = established_peer(&server).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
|
|
||||||
server
|
server
|
||||||
.local_rib
|
.local_rib
|
||||||
@@ -386,12 +357,11 @@ async fn test_route_refresh_resends_local_routes() -> Result<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_shutdown_sends_cease_to_every_peer() -> Result<()> {
|
async fn test_shutdown_sends_cease_to_every_peer() -> Result<()> {
|
||||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||||
use bgp_packet::message::Message;
|
|
||||||
|
|
||||||
let server = TestServer::start(LOCAL_ASN).await?;
|
let server = TestServer::start(LOCAL_ASN).await?;
|
||||||
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
|
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
|
||||||
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
|
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
|
||||||
wait_for_sessions(&server, 2).await?;
|
server.wait_for_sessions(2).await?;
|
||||||
|
|
||||||
let left = server
|
let left = server
|
||||||
.registry
|
.registry
|
||||||
@@ -401,21 +371,12 @@ async fn test_shutdown_sends_cease_to_every_peer() -> Result<()> {
|
|||||||
assert!(server.registry.is_shutting_down());
|
assert!(server.registry.is_shutting_down());
|
||||||
|
|
||||||
for peer in [&mut a, &mut b] {
|
for peer in [&mut a, &mut b] {
|
||||||
let notif = tokio::time::timeout(TIMEOUT, async {
|
let notif = peer.expect_notification().await?;
|
||||||
loop {
|
|
||||||
match peer.recv().await? {
|
|
||||||
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
|
|
||||||
Message::KeepAlive => {}
|
|
||||||
other => eyre::bail!("unexpected {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(notif.error, BgpError::Cease(CeaseSubcode::AdminShutdown)),
|
matches!(notif.error, BgpError::Cease(CeaseSubcode::AdminShutdown)),
|
||||||
"{notif:?}"
|
"{notif:?}"
|
||||||
);
|
);
|
||||||
assert!(peer.is_closed().await);
|
assert!(peer.closed_within(TIMEOUT).await);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -470,7 +431,7 @@ async fn test_a_peer_in_our_own_as_gets_an_empty_path_and_a_local_pref() -> Resu
|
|||||||
// The peer reads as a router in the server's AS does: one outside it
|
// The peer reads as a router in the server's AS does: one outside it
|
||||||
// would drop the LOCAL_PREF.
|
// would drop the LOCAL_PREF.
|
||||||
peer.ctx.ibgp = true;
|
peer.ctx.ibgp = true;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
assert!(server.registry.list()[0].ibgp);
|
assert!(server.registry.list()[0].ibgp);
|
||||||
|
|
||||||
server
|
server
|
||||||
|
|||||||
@@ -6,37 +6,20 @@ use std::collections::BTreeSet;
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bgp_integration_tests::default_policy;
|
use bgp_integration_tests::announcement::announcement;
|
||||||
use bgp_integration_tests::test_cache::TestCache;
|
use bgp_integration_tests::test_cache::TestCache;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
use bgp_packet::message::{
|
use bgp_integration_tests::{default_policy, prefix, wait_until};
|
||||||
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
use bgp_packet::message::PathAttribute;
|
||||||
UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rpki::{CacheState, RpkiConfig};
|
use bgp_rpki::{CacheState, RpkiConfig};
|
||||||
use bgp_server::config::PeerConfig;
|
|
||||||
use bgp_server::family::FamilySet;
|
|
||||||
use bgp_server::policy::{ImportPolicy, PeerRole};
|
use bgp_server::policy::{ImportPolicy, PeerRole};
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 1299;
|
const LOCAL_ASN: u32 = 1299;
|
||||||
const PEER_ASN: u32 = 3333;
|
const PEER_ASN: u32 = 3333;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What a test holds on to: the server, and the listener its dialling
|
|
||||||
/// goes to.
|
|
||||||
struct Setup {
|
|
||||||
server: TestServer,
|
|
||||||
_listener: TcpListener,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A server that validates against `cache`, with one configured peer at
|
/// A server that validates against `cache`, with one configured peer at
|
||||||
/// the address the test connects from, which is `role` to it. Returns once
|
/// the address the test connects from, which is `role` to it. Returns once
|
||||||
@@ -45,108 +28,33 @@ async fn server_with(
|
|||||||
cache: &TestCache,
|
cache: &TestCache,
|
||||||
role: Option<PeerRole>,
|
role: Option<PeerRole>,
|
||||||
import: ImportPolicy,
|
import: ImportPolicy,
|
||||||
) -> Result<Setup> {
|
) -> Result<ServerWithPeer> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
||||||
let endpoint = listener.local_addr()?;
|
|
||||||
let cache = cache.address();
|
let cache = cache.address();
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
let setup = TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||||
c.accept_unconfigured_peers = false;
|
|
||||||
c.rpki = Some(RpkiConfig {
|
c.rpki = Some(RpkiConfig {
|
||||||
caches: vec![cache],
|
caches: vec![cache],
|
||||||
retry: Some(Duration::from_millis(100)),
|
retry: Some(Duration::from_millis(100)),
|
||||||
..RpkiConfig::default()
|
..RpkiConfig::default()
|
||||||
});
|
});
|
||||||
c.peers = vec![PeerConfig {
|
peer.import = import;
|
||||||
graceful_restart: Default::default(),
|
peer.role = role;
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import,
|
|
||||||
role,
|
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let rpki = server.rpki.clone().expect("validation is configured");
|
let rpki = setup.server.rpki.clone().expect("validation is configured");
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("the server has the cache's data", || {
|
||||||
while rpki.status().state != CacheState::Synchronised {
|
rpki.status().state == CacheState::Synchronised
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Setup {
|
Ok(setup)
|
||||||
server,
|
|
||||||
_listener: listener,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
async fn established(server: &TestServer) -> Result<TestPeer> {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
server.established(PEER_ASN, PEER_ID).await
|
||||||
peer.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(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Announces `prefixes` with the AS path `path`.
|
/// Announces `prefixes` with the AS path `path`.
|
||||||
async fn announce(peer: &mut TestPeer, prefixes: &[&str], path: &[u32]) -> Result<()> {
|
async fn announce(peer: &mut TestPeer, prefixes: &[&str], path: &[u32]) -> Result<()> {
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(announcement(prefixes, path)).await
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes: vec![
|
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(path.to_vec()),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
],
|
|
||||||
announced_routes: prefixes.iter().map(|p| prefix(p).into()).collect(),
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits until the table holds exactly `expected` and the peer has `held`
|
|
||||||
/// routes held aside.
|
|
||||||
async fn table_becomes(server: &TestServer, expected: &[&str], held: u64) -> Result<()> {
|
|
||||||
let expected: BTreeSet<String> = expected.iter().map(|p| p.to_string()).collect();
|
|
||||||
let table = || -> BTreeSet<String> {
|
|
||||||
server
|
|
||||||
.received_routes()
|
|
||||||
.iter()
|
|
||||||
.map(|route| route.prefix.to_string())
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
let held_now = || {
|
|
||||||
server
|
|
||||||
.registry
|
|
||||||
.list()
|
|
||||||
.first()
|
|
||||||
.map_or(0, |session| session.counters.held())
|
|
||||||
};
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while table() != expected || held_now() != held {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| {
|
|
||||||
eyre::eyre!(
|
|
||||||
"the table has {:?} with {} held; expected {expected:?} with {held} held",
|
|
||||||
table(),
|
|
||||||
held_now()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AS 15169 buys from 3356 and says so; 3356 and the peer, 3333, have no
|
/// AS 15169 buys from 3356 and says so; 3356 and the peer, 3333, have no
|
||||||
@@ -161,7 +69,8 @@ fn aspas() -> Vec<(u32, Vec<u32>)> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_leaked_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
async fn test_a_leaked_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
||||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
let ServerWithPeer { server, .. } =
|
||||||
|
server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
||||||
let (base, _http) = server.serve_http().await?;
|
let (base, _http) = server.serve_http().await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
@@ -171,7 +80,9 @@ async fn test_a_leaked_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
|||||||
announce(&mut peer, &["193.0.0.0/21"], &[PEER_ASN]).await?;
|
announce(&mut peer, &["193.0.0.0/21"], &[PEER_ASN]).await?;
|
||||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121]).await?;
|
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121]).await?;
|
||||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||||
table_becomes(&server, &["193.0.0.0/21", "194.0.0.0/24"], 1).await?;
|
server
|
||||||
|
.table_becomes(&["193.0.0.0/21", "194.0.0.0/24"], 1)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let get = |path: &str| {
|
let get = |path: &str| {
|
||||||
@@ -217,13 +128,13 @@ async fn test_a_route_that_has_crossed_a_peering_is_taken_from_a_provider_only()
|
|||||||
(None, true),
|
(None, true),
|
||||||
] {
|
] {
|
||||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||||
let Setup { server, .. } = server_with(&cache, role, default_policy()).await?;
|
let ServerWithPeer { server, .. } = server_with(&cache, role, default_policy()).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||||
let expected = if taken {
|
let expected = if taken {
|
||||||
table_becomes(&server, &["8.8.8.0/24"], 0).await
|
server.table_becomes(&["8.8.8.0/24"], 0).await
|
||||||
} else {
|
} else {
|
||||||
table_becomes(&server, &[], 1).await
|
server.table_becomes(&[], 1).await
|
||||||
};
|
};
|
||||||
expected.map_err(|e| eyre::eyre!("from a peer that is {role:?}: {e}"))?;
|
expected.map_err(|e| eyre::eyre!("from a peer that is {role:?}: {e}"))?;
|
||||||
}
|
}
|
||||||
@@ -240,14 +151,15 @@ async fn test_paths_from_a_route_server_do_not_begin_with_its_as() -> Result<()>
|
|||||||
accept_any_first_as: true,
|
accept_any_first_as: true,
|
||||||
..default_policy()
|
..default_policy()
|
||||||
};
|
};
|
||||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::RouteServer), import).await?;
|
let ServerWithPeer { server, .. } =
|
||||||
|
server_with(&cache, Some(PeerRole::RouteServer), import).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
// 3356's customer route, as 3356 gave it to the route server.
|
// 3356's customer route, as 3356 gave it to the route server.
|
||||||
announce(&mut peer, &["8.8.8.0/24"], &[3356, 15169]).await?;
|
announce(&mut peer, &["8.8.8.0/24"], &[3356, 15169]).await?;
|
||||||
// The same route leaked to the route server by another member.
|
// The same route leaked to the route server by another member.
|
||||||
announce(&mut peer, &["8.8.4.0/24"], &[2121, 3356, 15169]).await?;
|
announce(&mut peer, &["8.8.4.0/24"], &[2121, 3356, 15169]).await?;
|
||||||
table_becomes(&server, &["8.8.8.0/24"], 1).await
|
server.table_becomes(&["8.8.8.0/24"], 1).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validity changes with the ASPAs, and the routes follow: one that was
|
/// Validity changes with the ASPAs, and the routes follow: one that was
|
||||||
@@ -256,19 +168,20 @@ async fn test_paths_from_a_route_server_do_not_begin_with_its_as() -> Result<()>
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_routes_follow_the_aspas() -> Result<()> {
|
async fn test_routes_follow_the_aspas() -> Result<()> {
|
||||||
let cache = TestCache::start_with(vec![], aspas()).await;
|
let cache = TestCache::start_with(vec![], aspas()).await;
|
||||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
let ServerWithPeer { server, .. } =
|
||||||
|
server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121]).await?;
|
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121]).await?;
|
||||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||||
table_becomes(&server, &["194.0.0.0/24"], 1).await?;
|
server.table_becomes(&["194.0.0.0/24"], 1).await?;
|
||||||
|
|
||||||
// 2121 says that its only provider is 1101: its route by way of 3333
|
// 2121 says that its only provider is 1101: its route by way of 3333
|
||||||
// is now invalid, and goes.
|
// is now invalid, and goes.
|
||||||
let mut changed = aspas();
|
let mut changed = aspas();
|
||||||
changed.push((2121, vec![1101]));
|
changed.push((2121, vec![1101]));
|
||||||
cache.set_aspas(changed);
|
cache.set_aspas(changed);
|
||||||
table_becomes(&server, &[], 2).await?;
|
server.table_becomes(&[], 2).await?;
|
||||||
let refused = &server.refusals.peers()[0].1;
|
let refused = &server.refusals.peers()[0].1;
|
||||||
assert_eq!(refused.total(), 2);
|
assert_eq!(refused.total(), 2);
|
||||||
assert_eq!(refused.recent()[0].prefix, prefix("194.0.0.0/24"));
|
assert_eq!(refused.recent()[0].prefix, prefix("194.0.0.0/24"));
|
||||||
@@ -282,7 +195,9 @@ async fn test_routes_follow_the_aspas() -> Result<()> {
|
|||||||
(PEER_ASN, vec![0]),
|
(PEER_ASN, vec![0]),
|
||||||
(2121, vec![1101, PEER_ASN]),
|
(2121, vec![1101, PEER_ASN]),
|
||||||
]);
|
]);
|
||||||
table_becomes(&server, &["194.0.0.0/24", "8.8.8.0/24"], 0).await?;
|
server
|
||||||
|
.table_becomes(&["194.0.0.0/24", "8.8.8.0/24"], 0)
|
||||||
|
.await?;
|
||||||
let paths: BTreeSet<Vec<u32>> = server
|
let paths: BTreeSet<Vec<u32>> = server
|
||||||
.received_routes()
|
.received_routes()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -304,9 +219,11 @@ async fn test_routes_follow_the_aspas() -> Result<()> {
|
|||||||
(3356, vec![0]),
|
(3356, vec![0]),
|
||||||
(PEER_ASN, vec![0]),
|
(PEER_ASN, vec![0]),
|
||||||
]);
|
]);
|
||||||
table_becomes(&server, &["194.0.0.0/24"], 1).await?;
|
server.table_becomes(&["194.0.0.0/24"], 1).await?;
|
||||||
cache.set_aspas(vec![]);
|
cache.set_aspas(vec![]);
|
||||||
table_becomes(&server, &["194.0.0.0/24", "8.8.8.0/24"], 0).await
|
server
|
||||||
|
.table_becomes(&["194.0.0.0/24", "8.8.8.0/24"], 0)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A peer can be exempt: its routes with invalid paths are taken, and
|
/// A peer can be exempt: its routes with invalid paths are taken, and
|
||||||
@@ -318,12 +235,12 @@ async fn test_a_peer_can_be_exempt_from_path_verification() -> Result<()> {
|
|||||||
accept_aspa_invalid: true,
|
accept_aspa_invalid: true,
|
||||||
..default_policy()
|
..default_policy()
|
||||||
};
|
};
|
||||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), import).await?;
|
let ServerWithPeer { server, .. } = server_with(&cache, Some(PeerRole::Peer), import).await?;
|
||||||
let (base, _http) = server.serve_http().await?;
|
let (base, _http) = server.serve_http().await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||||
table_becomes(&server, &["8.8.8.0/24"], 0).await?;
|
server.table_becomes(&["8.8.8.0/24"], 0).await?;
|
||||||
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
|
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
|
||||||
.await?
|
.await?
|
||||||
.json()
|
.json()
|
||||||
@@ -342,16 +259,17 @@ async fn test_a_route_is_held_until_origin_and_path_are_both_in_order() -> Resul
|
|||||||
asn,
|
asn,
|
||||||
};
|
};
|
||||||
let cache = TestCache::start_with(vec![google(13335)], aspas()).await;
|
let cache = TestCache::start_with(vec![google(13335)], aspas()).await;
|
||||||
let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
let ServerWithPeer { server, .. } =
|
||||||
|
server_with(&cache, Some(PeerRole::Peer), default_policy()).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?;
|
||||||
table_becomes(&server, &[], 1).await?;
|
server.table_becomes(&[], 1).await?;
|
||||||
|
|
||||||
// The origin is put right; the path is still a leak.
|
// The origin is put right; the path is still a leak.
|
||||||
cache.set(vec![google(15169)]);
|
cache.set(vec![google(15169)]);
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
table_becomes(&server, &[], 1).await?;
|
server.table_becomes(&[], 1).await?;
|
||||||
|
|
||||||
cache.set_aspas(vec![(15169, vec![3356]), (3356, vec![PEER_ASN])]);
|
cache.set_aspas(vec![(15169, vec![3356]), (3356, vec![PEER_ASN])]);
|
||||||
table_becomes(&server, &["8.8.8.0/24"], 0).await
|
server.table_becomes(&["8.8.8.0/24"], 0).await
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ use std::time::Duration;
|
|||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::RouteChange;
|
use bgp_integration_tests::test_server::RouteChange;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::TestServer;
|
||||||
|
use bgp_integration_tests::{TIMEOUT, wait_until};
|
||||||
use bgp_packet::errors::{BgpError, CeaseSubcode, FsmSubcode, MsgHeaderSubcode, UpdateMsgSubcode};
|
use bgp_packet::errors::{BgpError, CeaseSubcode, FsmSubcode, MsgHeaderSubcode, UpdateMsgSubcode};
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{
|
||||||
AsPathAttribute, Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute,
|
AsPathAttribute, Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute,
|
||||||
@@ -14,8 +15,6 @@ use bgp_primitives::constants::{AfiSafi, IpVersion};
|
|||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
use bgp_primitives::ip_prefix::IpPrefix;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
|
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
/// The attributes every announcement of IPv4 routes must carry. An UPDATE
|
/// The attributes every announcement of IPv4 routes must carry. An UPDATE
|
||||||
/// without them has its routes withdrawn instead (RFC 7606).
|
/// without them has its routes withdrawn instead (RFC 7606).
|
||||||
fn mandatory_attributes() -> Vec<PathAttribute> {
|
fn mandatory_attributes() -> Vec<PathAttribute> {
|
||||||
@@ -98,20 +97,6 @@ async fn test_bad_marker_does_not_crash_server() -> Result<()> {
|
|||||||
Ok(())
|
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`.
|
/// A header with the given length and type, followed by `body`.
|
||||||
fn framed(length: u16, msg_type: u8, body: &[u8]) -> Vec<u8> {
|
fn framed(length: u16, msg_type: u8, body: &[u8]) -> Vec<u8> {
|
||||||
let mut buf = vec![0xFF; 16];
|
let mut buf = vec![0xFF; 16];
|
||||||
@@ -170,7 +155,7 @@ async fn test_malformed_messages_are_answered_with_the_matching_notification() -
|
|||||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 1, i as u8 + 1), 90)
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 1, i as u8 + 1), 90)
|
||||||
.await?;
|
.await?;
|
||||||
peer.send_raw(&bytes).await?;
|
peer.send_raw(&bytes).await?;
|
||||||
let notification = expect_notification(&mut peer).await?;
|
let notification = peer.expect_notification().await?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(notification.error, notification.data),
|
(notification.error, notification.data),
|
||||||
(error.clone(), data.clone()),
|
(error.clone(), data.clone()),
|
||||||
@@ -181,7 +166,7 @@ async fn test_malformed_messages_are_answered_with_the_matching_notification() -
|
|||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
let _ = peer.recv_open().await?;
|
let _ = peer.recv_open().await?;
|
||||||
peer.send_raw(&bytes).await?;
|
peer.send_raw(&bytes).await?;
|
||||||
let notification = expect_notification(&mut peer).await?;
|
let notification = peer.expect_notification().await?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(notification.error, notification.data),
|
(notification.error, notification.data),
|
||||||
(error, data),
|
(error, data),
|
||||||
@@ -194,7 +179,7 @@ async fn test_malformed_messages_are_answered_with_the_matching_notification() -
|
|||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
let _ = peer.recv_open().await?;
|
let _ = peer.recv_open().await?;
|
||||||
peer.send(&Message::KeepAlive).await?;
|
peer.send(&Message::KeepAlive).await?;
|
||||||
let notification = expect_notification(&mut peer).await?;
|
let notification = peer.expect_notification().await?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
notification.error,
|
notification.error,
|
||||||
BgpError::FsmError(FsmSubcode::UnexpectedOpenSent)
|
BgpError::FsmError(FsmSubcode::UnexpectedOpenSent)
|
||||||
@@ -228,13 +213,10 @@ async fn test_message_split_across_a_keepalive_tick_is_still_one_message() -> Re
|
|||||||
tokio::time::sleep(Duration::from_millis(1300)).await;
|
tokio::time::sleep(Duration::from_millis(1300)).await;
|
||||||
peer.send_raw(&update[10..]).await?;
|
peer.send_raw(&update[10..]).await?;
|
||||||
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("the split UPDATE reaches the RIB", || {
|
||||||
while !server.received_routes().iter().any(|e| e.prefix == prefix) {
|
server.received_routes().iter().any(|e| e.prefix == prefix)
|
||||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| eyre::eyre!("the split UPDATE never reached the RIB"))?;
|
|
||||||
assert_eq!(server.registry.list().len(), 1, "session was dropped");
|
assert_eq!(server.registry.list().len(), 1, "session was dropped");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -266,7 +248,7 @@ async fn test_server_sends_keepalives() -> Result<()> {
|
|||||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
|
||||||
|
|
||||||
// We should receive a KEEPALIVE within ~1 second (hold/3).
|
// We should receive a KEEPALIVE within ~1 second (hold/3).
|
||||||
let msg = tokio::time::timeout(Duration::from_secs(3), peer.recv()).await??;
|
let msg = peer.recv_within(Duration::from_secs(3)).await?;
|
||||||
assert!(
|
assert!(
|
||||||
matches!(msg, Message::KeepAlive),
|
matches!(msg, Message::KeepAlive),
|
||||||
"Expected KEEPALIVE, got {:?}",
|
"Expected KEEPALIVE, got {:?}",
|
||||||
@@ -286,16 +268,7 @@ async fn test_hold_timer_expires() -> Result<()> {
|
|||||||
// Don't send any keepalives. The server will send keepalives until its own
|
// 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
|
// hold timer fires and it sends a NOTIFICATION. Drain messages until we
|
||||||
// see it (or the outer timeout fires).
|
// see it (or the outer timeout fires).
|
||||||
let notif = tokio::time::timeout(Duration::from_secs(6), async {
|
let notif = tokio::time::timeout(Duration::from_secs(6), peer.expect_notification()).await??;
|
||||||
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!(
|
assert!(
|
||||||
matches!(notif.error, BgpError::HoldTimer),
|
matches!(notif.error, BgpError::HoldTimer),
|
||||||
"Expected HoldTimer NOTIFICATION, got {:?}",
|
"Expected HoldTimer NOTIFICATION, got {:?}",
|
||||||
@@ -319,8 +292,7 @@ async fn test_peer_sends_notification() -> Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Server should close the connection cleanly.
|
// Server should close the connection cleanly.
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
assert!(peer.closed_within(TIMEOUT).await);
|
||||||
assert!(peer.is_closed().await);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,66 +2,34 @@
|
|||||||
//! peers, which of them the server chooses, and how that is shown.
|
//! peers, which of them the server chooses, and how that is shown.
|
||||||
|
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::{Announcement, withdrawal as withdrawal_of};
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::{RouteChange, TestServer};
|
use bgp_integration_tests::test_server::{RouteChange, TestServer};
|
||||||
use bgp_packet::message::{
|
use bgp_integration_tests::{TIMEOUT, get_json, prefix};
|
||||||
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
use bgp_packet::message::{PathAttribute, UpdateMessage};
|
||||||
UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A session from the peer numbered `number`, in AS 65000 + `number`.
|
/// A session from the peer numbered `number`, in AS 65000 + `number`.
|
||||||
async fn peer(server: &TestServer, number: u8) -> Result<TestPeer> {
|
async fn peer(server: &TestServer, number: u8) -> Result<TestPeer> {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
server
|
||||||
peer.handshake(
|
.established(
|
||||||
LOCAL_ASN + u32::from(number),
|
LOCAL_ASN + u32::from(number),
|
||||||
Ipv4Addr::new(10, 0, 0, number),
|
Ipv4Addr::new(10, 0, 0, number),
|
||||||
90,
|
)
|
||||||
)
|
.await
|
||||||
.await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn announcement(prefixes: &[IpPrefix], as_path: &[u32]) -> Message {
|
fn announcement(prefixes: &[IpPrefix], as_path: &[u32]) -> Announcement {
|
||||||
Message::Update(UpdateMessage {
|
Announcement::of(Nlri::plain(prefixes.iter().copied()), as_path)
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes: vec![
|
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(as_path.to_vec()),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
],
|
|
||||||
announced_routes: Nlri::plain(prefixes.iter().copied()),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn withdrawal(prefixes: &[IpPrefix]) -> Message {
|
fn withdrawal(prefixes: &[IpPrefix]) -> UpdateMessage {
|
||||||
Message::Update(UpdateMessage {
|
withdrawal_of(Nlri::plain(prefixes.iter().copied()))
|
||||||
withdrawn_routes: Nlri::plain(prefixes.iter().copied()),
|
|
||||||
path_attributes: vec![],
|
|
||||||
announced_routes: vec![],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits until the table holds `routes` routes.
|
|
||||||
async fn wait_for_routes(server: &TestServer, routes: usize) -> Result<()> {
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.rib.len() != routes {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The AS path of the route the server holds as best for `prefix`.
|
/// The AS path of the route the server holds as best for `prefix`.
|
||||||
@@ -78,16 +46,6 @@ fn best_path(server: &TestServer, prefix: &IpPrefix) -> Option<Vec<u32>> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
|
|
||||||
Ok(client
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.json()
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two peers offer the same prefix. The route with the shorter AS path is
|
/// Two peers offer the same prefix. The route with the shorter AS path is
|
||||||
/// chosen; when it goes, the other is.
|
/// chosen; when it goes, the other is.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -99,14 +57,14 @@ async fn test_the_best_route_is_chosen_and_replaced_when_it_goes() -> Result<()>
|
|||||||
let p = prefix("203.0.113.0/24");
|
let p = prefix("203.0.113.0/24");
|
||||||
|
|
||||||
let mut far = peer(&server, 1).await?;
|
let mut far = peer(&server, 1).await?;
|
||||||
far.send(&announcement(&[p], &[65001, 64999, 64998]))
|
far.announce(announcement(&[p], &[65001, 64999, 64998]))
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_routes(&server, 1).await?;
|
server.wait_for_routes(1).await?;
|
||||||
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
|
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
|
||||||
|
|
||||||
let mut near = peer(&server, 2).await?;
|
let mut near = peer(&server, 2).await?;
|
||||||
near.send(&announcement(&[p], &[65002, 64998])).await?;
|
near.announce(announcement(&[p], &[65002, 64998])).await?;
|
||||||
wait_for_routes(&server, 2).await?;
|
server.wait_for_routes(2).await?;
|
||||||
assert_eq!(best_path(&server, &p), Some(vec![65002, 64998]));
|
assert_eq!(best_path(&server, &p), Some(vec![65002, 64998]));
|
||||||
|
|
||||||
// Both routes are kept and shown; one is marked.
|
// Both routes are kept and shown; one is marked.
|
||||||
@@ -126,11 +84,11 @@ async fn test_the_best_route_is_chosen_and_replaced_when_it_goes() -> Result<()>
|
|||||||
|
|
||||||
// The session with the best route ends: the other route takes over.
|
// The session with the best route ends: the other route takes over.
|
||||||
drop(near);
|
drop(near);
|
||||||
wait_for_routes(&server, 1).await?;
|
server.wait_for_routes(1).await?;
|
||||||
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
|
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
|
||||||
|
|
||||||
far.send(&withdrawal(&[p])).await?;
|
far.announce(withdrawal(&[p])).await?;
|
||||||
wait_for_routes(&server, 0).await?;
|
server.wait_for_routes(0).await?;
|
||||||
assert_eq!(best_path(&server, &p), None);
|
assert_eq!(best_path(&server, &p), None);
|
||||||
|
|
||||||
// A follower of the best routes is told of each change: the first
|
// A follower of the best routes is told of each change: the first
|
||||||
@@ -157,9 +115,9 @@ async fn test_changes_of_best_route_are_on_the_event_stream() -> Result<()> {
|
|||||||
// A route that is there before the client connects is not an event.
|
// A route that is there before the client connects is not an event.
|
||||||
let mut first = peer(&server, 1).await?;
|
let mut first = peer(&server, 1).await?;
|
||||||
first
|
first
|
||||||
.send(&announcement(&[prefix("198.51.100.0/24")], &[65001]))
|
.announce(announcement(&[prefix("198.51.100.0/24")], &[65001]))
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_routes(&server, 1).await?;
|
server.wait_for_routes(1).await?;
|
||||||
|
|
||||||
let mut stream = reqwest::Client::new()
|
let mut stream = reqwest::Client::new()
|
||||||
.get(format!("{base}/api/events"))
|
.get(format!("{base}/api/events"))
|
||||||
@@ -167,10 +125,10 @@ async fn test_changes_of_best_route_are_on_the_event_stream() -> Result<()> {
|
|||||||
.await?
|
.await?
|
||||||
.error_for_status()?;
|
.error_for_status()?;
|
||||||
|
|
||||||
first.send(&announcement(&[p], &[65001, 64998])).await?;
|
first.announce(announcement(&[p], &[65001, 64998])).await?;
|
||||||
wait_for_routes(&server, 2).await?;
|
server.wait_for_routes(2).await?;
|
||||||
first.send(&withdrawal(&[p])).await?;
|
first.announce(withdrawal(&[p])).await?;
|
||||||
wait_for_routes(&server, 1).await?;
|
server.wait_for_routes(1).await?;
|
||||||
|
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let mut events: Vec<Value> = Vec::new();
|
let mut events: Vec<Value> = Vec::new();
|
||||||
@@ -223,8 +181,8 @@ async fn test_received_routes_are_returned_up_to_a_limit() -> Result<()> {
|
|||||||
|
|
||||||
let prefixes: Vec<IpPrefix> = (0..40).map(|n| prefix(&format!("10.{n}.0.0/16"))).collect();
|
let prefixes: Vec<IpPrefix> = (0..40).map(|n| prefix(&format!("10.{n}.0.0/16"))).collect();
|
||||||
let mut peer = peer(&server, 1).await?;
|
let mut peer = peer(&server, 1).await?;
|
||||||
peer.send(&announcement(&prefixes, &[65001])).await?;
|
peer.announce(announcement(&prefixes, &[65001])).await?;
|
||||||
wait_for_routes(&server, 40).await?;
|
server.wait_for_routes(40).await?;
|
||||||
|
|
||||||
let count = |page: Value| page["routes"].as_array().unwrap().len();
|
let count = |page: Value| page["routes"].as_array().unwrap().len();
|
||||||
let url = format!("{base}/api/routes/received");
|
let url = format!("{base}/api/routes/received");
|
||||||
|
|||||||
@@ -3,56 +3,33 @@
|
|||||||
//! The test server's BGP Identifier is 127.0.0.1. A test peer picks an
|
//! The test server's BGP Identifier is 127.0.0.1. A test peer picks an
|
||||||
//! identifier above or below that to steer which connection must survive.
|
//! identifier above or below that to steer which connection must survive.
|
||||||
|
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::announcement;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
|
use bgp_integration_tests::{TIMEOUT, prefix, wait_until};
|
||||||
use bgp_packet::errors::{BgpError, CeaseSubcode, OpenMsgSubcode};
|
use bgp_packet::errors::{BgpError, CeaseSubcode, OpenMsgSubcode};
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{Message, NotificationMessage};
|
||||||
AsPathAttribute, Message, NextHopPathAttribute, NotificationMessage, OriginPathAttribute,
|
|
||||||
PathAttribute, UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||||
use bgp_server::collision::Direction;
|
use bgp_server::collision::Direction;
|
||||||
use bgp_server::config::PeerConfig;
|
|
||||||
use bgp_server::family::FamilySet;
|
|
||||||
use bgp_server::policy::ImportPolicy;
|
use bgp_server::policy::ImportPolicy;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
/// Above the server's 127.0.0.1.
|
/// Above the server's 127.0.0.1.
|
||||||
const HIGH_ID: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1);
|
const HIGH_ID: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1);
|
||||||
/// Below the server's 127.0.0.1.
|
/// Below the server's 127.0.0.1.
|
||||||
const LOW_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
|
const LOW_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
|
||||||
|
|
||||||
async fn server_dialing(listener: &TcpListener) -> Result<TestServer> {
|
/// A server that dials its peer "r1" every 200 ms until it has a session.
|
||||||
let endpoint: SocketAddr = listener.local_addr()?;
|
async fn server_dialing() -> Result<ServerWithPeer> {
|
||||||
TestServer::start_with(LOCAL_ASN, |c| {
|
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||||
c.connect_retry = Duration::from_millis(200);
|
c.connect_retry = Duration::from_millis(200);
|
||||||
c.peers = vec![PeerConfig {
|
peer.import = ImportPolicy::permissive();
|
||||||
graceful_restart: Default::default(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import: ImportPolicy::permissive(),
|
|
||||||
role: None,
|
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -67,27 +44,13 @@ async fn open_only(peer: &mut TestPeer, router_id: Ipv4Addr) -> Result<()> {
|
|||||||
|
|
||||||
/// Finishes the handshake begun by [`open_only`].
|
/// Finishes the handshake begun by [`open_only`].
|
||||||
async fn confirm(peer: &mut TestPeer) -> Result<()> {
|
async fn confirm(peer: &mut TestPeer) -> Result<()> {
|
||||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
match peer.recv().await? {
|
||||||
Message::KeepAlive => {}
|
Message::KeepAlive => {}
|
||||||
other => eyre::bail!("expected KEEPALIVE, got {other:?}"),
|
other => eyre::bail!("expected KEEPALIVE, got {other:?}"),
|
||||||
}
|
}
|
||||||
peer.send(&Message::KeepAlive).await
|
peer.send(&Message::KeepAlive).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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?
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_collision_cease(n: &NotificationMessage) -> bool {
|
fn is_collision_cease(n: &NotificationMessage) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
n.error,
|
n.error,
|
||||||
@@ -95,38 +58,26 @@ fn is_collision_cease(n: &NotificationMessage) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> {
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().len() != n {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Peer has the higher identifier, so the connection the *peer* initiated
|
/// Peer has the higher identifier, so the connection the *peer* initiated
|
||||||
/// must survive: the server closes its own outbound connection.
|
/// must survive: the server closes its own outbound connection.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
|
async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer { server, listener } = server_dialing().await?;
|
||||||
let server = server_dialing(&listener).await?;
|
|
||||||
|
|
||||||
// Server's outbound connection reaches OpenConfirm.
|
// Server's outbound connection reaches OpenConfirm.
|
||||||
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
let mut dialled = TestPeer::accept(&listener).await?;
|
||||||
let mut dialled = TestPeer::from_stream(stream);
|
|
||||||
open_only(&mut dialled, HIGH_ID).await?;
|
open_only(&mut dialled, HIGH_ID).await?;
|
||||||
|
|
||||||
// The peer now also dials the server.
|
// The peer now also dials the server.
|
||||||
let mut inbound = TestPeer::connect(server.addr).await?;
|
let mut inbound = TestPeer::connect(server.addr).await?;
|
||||||
open_only(&mut inbound, HIGH_ID).await?;
|
open_only(&mut inbound, HIGH_ID).await?;
|
||||||
|
|
||||||
let notif = expect_notification(&mut dialled).await?;
|
let notif = dialled.expect_notification().await?;
|
||||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||||
assert!(dialled.is_closed().await);
|
assert!(dialled.closed_within(TIMEOUT).await);
|
||||||
|
|
||||||
confirm(&mut inbound).await?;
|
confirm(&mut inbound).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
let session = server.registry.list().remove(0);
|
let session = server.registry.list().remove(0);
|
||||||
assert_eq!(session.direction, Direction::Inbound);
|
assert_eq!(session.direction, Direction::Inbound);
|
||||||
assert_eq!(session.name.as_deref(), Some("r1"));
|
assert_eq!(session.name.as_deref(), Some("r1"));
|
||||||
@@ -135,11 +86,11 @@ async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
|
|||||||
server
|
server
|
||||||
.local_rib
|
.local_rib
|
||||||
.announce(LocalRoute::new(
|
.announce(LocalRoute::new(
|
||||||
IpPrefix::try_from("10.100.1.0/24")?,
|
prefix("10.100.1.0/24"),
|
||||||
LocalRouteAttrs::default(),
|
LocalRouteAttrs::default(),
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, inbound.recv_update()).await??;
|
let update = inbound.recv_update().await?;
|
||||||
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
||||||
|
|
||||||
// The dialer notices the surviving session and does not open another.
|
// The dialer notices the surviving session and does not open another.
|
||||||
@@ -155,22 +106,20 @@ async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
|
|||||||
/// must survive: the peer's inbound connection is refused.
|
/// must survive: the peer's inbound connection is refused.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_simultaneous_open_peer_lower_keeps_outbound() -> Result<()> {
|
async fn test_simultaneous_open_peer_lower_keeps_outbound() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer { server, listener } = server_dialing().await?;
|
||||||
let server = server_dialing(&listener).await?;
|
|
||||||
|
|
||||||
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
let mut dialled = TestPeer::accept(&listener).await?;
|
||||||
let mut dialled = TestPeer::from_stream(stream);
|
|
||||||
open_only(&mut dialled, LOW_ID).await?;
|
open_only(&mut dialled, LOW_ID).await?;
|
||||||
|
|
||||||
let mut inbound = TestPeer::connect(server.addr).await?;
|
let mut inbound = TestPeer::connect(server.addr).await?;
|
||||||
open_only(&mut inbound, LOW_ID).await?;
|
open_only(&mut inbound, LOW_ID).await?;
|
||||||
|
|
||||||
let notif = expect_notification(&mut inbound).await?;
|
let notif = inbound.expect_notification().await?;
|
||||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||||
assert!(inbound.is_closed().await);
|
assert!(inbound.closed_within(TIMEOUT).await);
|
||||||
|
|
||||||
confirm(&mut dialled).await?;
|
confirm(&mut dialled).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
|
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -179,22 +128,20 @@ async fn test_simultaneous_open_peer_lower_keeps_outbound() -> Result<()> {
|
|||||||
/// outcome: here the inbound connection reaches OpenConfirm first.
|
/// outcome: here the inbound connection reaches OpenConfirm first.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_outcome_is_independent_of_arrival_order() -> Result<()> {
|
async fn test_outcome_is_independent_of_arrival_order() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer { server, listener } = server_dialing().await?;
|
||||||
let server = server_dialing(&listener).await?;
|
|
||||||
|
|
||||||
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
let mut dialled = TestPeer::accept(&listener).await?;
|
||||||
let mut dialled = TestPeer::from_stream(stream);
|
|
||||||
|
|
||||||
// Inbound first this time; peer is lower so outbound must still win.
|
// Inbound first this time; peer is lower so outbound must still win.
|
||||||
let mut inbound = TestPeer::connect(server.addr).await?;
|
let mut inbound = TestPeer::connect(server.addr).await?;
|
||||||
open_only(&mut inbound, LOW_ID).await?;
|
open_only(&mut inbound, LOW_ID).await?;
|
||||||
open_only(&mut dialled, LOW_ID).await?;
|
open_only(&mut dialled, LOW_ID).await?;
|
||||||
|
|
||||||
let notif = expect_notification(&mut inbound).await?;
|
let notif = inbound.expect_notification().await?;
|
||||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||||
|
|
||||||
confirm(&mut dialled).await?;
|
confirm(&mut dialled).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
|
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -207,11 +154,11 @@ async fn test_established_session_wins_by_default() -> Result<()> {
|
|||||||
|
|
||||||
let mut first = TestPeer::connect(server.addr).await?;
|
let mut first = TestPeer::connect(server.addr).await?;
|
||||||
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
|
|
||||||
let mut second = TestPeer::connect(server.addr).await?;
|
let mut second = TestPeer::connect(server.addr).await?;
|
||||||
open_only(&mut second, HIGH_ID).await?;
|
open_only(&mut second, HIGH_ID).await?;
|
||||||
let notif = expect_notification(&mut second).await?;
|
let notif = second.expect_notification().await?;
|
||||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||||
|
|
||||||
// The original is untouched.
|
// The original is untouched.
|
||||||
@@ -219,11 +166,11 @@ async fn test_established_session_wins_by_default() -> Result<()> {
|
|||||||
server
|
server
|
||||||
.local_rib
|
.local_rib
|
||||||
.announce(LocalRoute::new(
|
.announce(LocalRoute::new(
|
||||||
IpPrefix::try_from("10.100.1.0/24")?,
|
prefix("10.100.1.0/24"),
|
||||||
LocalRouteAttrs::default(),
|
LocalRouteAttrs::default(),
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, first.recv_update()).await??;
|
let update = first.recv_update().await?;
|
||||||
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -238,41 +185,26 @@ async fn test_restarted_peer_replaces_stale_session_when_enabled() -> Result<()>
|
|||||||
|
|
||||||
let mut stale = TestPeer::connect(server.addr).await?;
|
let mut stale = TestPeer::connect(server.addr).await?;
|
||||||
stale.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
stale.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
stale
|
stale
|
||||||
.send(&Message::Update(UpdateMessage {
|
.announce(
|
||||||
withdrawn_routes: vec![],
|
announcement(&["203.0.113.0/24"], &[PEER_ASN])
|
||||||
path_attributes: vec![
|
.with_next_hop(Ipv4Addr::new(10, 0, 0, 9)),
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
)
|
||||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 9))),
|
|
||||||
],
|
|
||||||
announced_routes: vec![IpPrefix::try_from("203.0.113.0/24")?.into()],
|
|
||||||
}))
|
|
||||||
.await?;
|
.await?;
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
server.wait_for_routes(1).await?;
|
||||||
while server.received_routes().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let stale_addr = server.registry.list()[0].peer.addr;
|
let stale_addr = server.registry.list()[0].peer.addr;
|
||||||
|
|
||||||
// "Restart": a fresh connection from the same router.
|
// "Restart": a fresh connection from the same router.
|
||||||
let mut fresh = TestPeer::connect(server.addr).await?;
|
let mut fresh = TestPeer::connect(server.addr).await?;
|
||||||
fresh.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
fresh.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||||
|
|
||||||
let notif = expect_notification(&mut stale).await?;
|
let notif = stale.expect_notification().await?;
|
||||||
assert!(is_collision_cease(¬if), "{notif:?}");
|
assert!(is_collision_cease(¬if), "{notif:?}");
|
||||||
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("the fresh session has replaced the stale one", || {
|
||||||
loop {
|
let sessions = server.registry.list();
|
||||||
let sessions = server.registry.list();
|
sessions.len() == 1 && sessions[0].peer.addr != stale_addr
|
||||||
if sessions.len() == 1 && sessions[0].peer.addr != stale_addr {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
assert!(
|
assert!(
|
||||||
@@ -290,7 +222,7 @@ async fn test_distinct_identifiers_do_not_collide() -> Result<()> {
|
|||||||
a.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
a.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||||
let mut b = TestPeer::connect(server.addr).await?;
|
let mut b = TestPeer::connect(server.addr).await?;
|
||||||
b.handshake(PEER_ASN, LOW_ID, 90).await?;
|
b.handshake(PEER_ASN, LOW_ID, 90).await?;
|
||||||
wait_for_sessions(&server, 2).await?;
|
server.wait_for_sessions(2).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,13 +232,13 @@ async fn test_identifier_is_released_when_connection_ends() -> Result<()> {
|
|||||||
let server = TestServer::start(LOCAL_ASN).await?;
|
let server = TestServer::start(LOCAL_ASN).await?;
|
||||||
let mut first = TestPeer::connect(server.addr).await?;
|
let mut first = TestPeer::connect(server.addr).await?;
|
||||||
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
drop(first);
|
drop(first);
|
||||||
wait_for_sessions(&server, 0).await?;
|
server.wait_for_sessions(0).await?;
|
||||||
|
|
||||||
let mut second = TestPeer::connect(server.addr).await?;
|
let mut second = TestPeer::connect(server.addr).await?;
|
||||||
second.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
second.handshake(PEER_ASN, HIGH_ID, 90).await?;
|
||||||
wait_for_sessions(&server, 1).await?;
|
server.wait_for_sessions(1).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +248,7 @@ async fn test_peer_with_our_identifier_is_refused() -> Result<()> {
|
|||||||
let server = TestServer::start(LOCAL_ASN).await?;
|
let server = TestServer::start(LOCAL_ASN).await?;
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
open_only(&mut peer, Ipv4Addr::new(127, 0, 0, 1)).await?;
|
open_only(&mut peer, Ipv4Addr::new(127, 0, 0, 1)).await?;
|
||||||
let notif = expect_notification(&mut peer).await?;
|
let notif = peer.expect_notification().await?;
|
||||||
assert!(
|
assert!(
|
||||||
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadBgpId)),
|
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadBgpId)),
|
||||||
"{notif:?}"
|
"{notif:?}"
|
||||||
|
|||||||
@@ -2,39 +2,13 @@
|
|||||||
//! what was refused.
|
//! what was refused.
|
||||||
|
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use bgp_integration_tests::default_policy;
|
use bgp_integration_tests::announcement::announcement;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::{default_policy, wait_until};
|
||||||
use bgp_packet::message::{
|
|
||||||
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
|
||||||
UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_server::config::PeerConfig;
|
|
||||||
use bgp_server::family::FamilySet;
|
|
||||||
use bgp_server::policy::ImportPolicy;
|
use bgp_server::policy::ImportPolicy;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
fn announcement(prefixes: &[&str], as_path: &[u32]) -> Message {
|
|
||||||
Message::Update(UpdateMessage {
|
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes: vec![
|
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(as_path.to_vec()),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
],
|
|
||||||
announced_routes: prefixes
|
|
||||||
.iter()
|
|
||||||
.map(|p| IpPrefix::try_from(*p).unwrap().into())
|
|
||||||
.collect(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The routes a query returns, as "prefix from AS, best or not".
|
/// The routes a query returns, as "prefix from AS, best or not".
|
||||||
async fn routes(client: &reqwest::Client, base: &str, query: &str) -> Result<Vec<String>> {
|
async fn routes(client: &reqwest::Client, base: &str, query: &str) -> Result<Vec<String>> {
|
||||||
@@ -67,24 +41,21 @@ async fn test_routes_can_be_looked_up() -> Result<()> {
|
|||||||
let (base, _http) = server.serve_http().await?;
|
let (base, _http) = server.serve_http().await?;
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
let mut one = TestPeer::connect(server.addr).await?;
|
let mut one = server
|
||||||
one.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
.established(65001, Ipv4Addr::new(10, 0, 0, 1))
|
||||||
let mut two = TestPeer::connect(server.addr).await?;
|
.await?;
|
||||||
two.handshake(65002, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
|
let mut two = server
|
||||||
one.send(&announcement(
|
.established(65002, Ipv4Addr::new(10, 0, 0, 2))
|
||||||
|
.await?;
|
||||||
|
one.announce(announcement(
|
||||||
&["10.0.0.0/8", "10.1.0.0/16", "10.1.2.0/24", "192.0.2.0/24"],
|
&["10.0.0.0/8", "10.1.0.0/16", "10.1.2.0/24", "192.0.2.0/24"],
|
||||||
&[65001],
|
&[65001],
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
// A longer path for one of the same prefixes: it loses.
|
// A longer path for one of the same prefixes: it loses.
|
||||||
two.send(&announcement(&["10.1.0.0/16"], &[65002, 65003]))
|
two.announce(announcement(&["10.1.0.0/16"], &[65002, 65003]))
|
||||||
.await?;
|
.await?;
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
server.wait_for_routes(5).await?;
|
||||||
while server.rib.len() != 5 {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// One prefix: every route for it, the best first.
|
// One prefix: every route for it, the best first.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -162,31 +133,15 @@ async fn test_routes_can_be_looked_up() -> Result<()> {
|
|||||||
/// something to alert on.
|
/// something to alert on.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
|
async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer {
|
||||||
let endpoint = listener.local_addr()?;
|
server,
|
||||||
let server = TestServer::start_with(1299, |c| {
|
listener: _listener,
|
||||||
c.accept_unconfigured_peers = false;
|
} = TestServer::with_peer(1299, 3333, |_, peer| {
|
||||||
c.peers = vec![PeerConfig {
|
peer.name = "refused-demo".into();
|
||||||
graceful_restart: Default::default(),
|
peer.import = ImportPolicy {
|
||||||
groups: Vec::new(),
|
max_prefixes: Some(100),
|
||||||
export_limits: None,
|
..default_policy()
|
||||||
passive: false,
|
};
|
||||||
reflector_client: false,
|
|
||||||
name: "refused-demo".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: 3333,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import: ImportPolicy {
|
|
||||||
max_prefixes: Some(100),
|
|
||||||
..default_policy()
|
|
||||||
},
|
|
||||||
role: None,
|
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let (base, _http) = server.serve_http().await?;
|
let (base, _http) = server.serve_http().await?;
|
||||||
@@ -199,27 +154,23 @@ async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
|
|||||||
let refused: Value = get("/api/routes/rejected").await?.json().await?;
|
let refused: Value = get("/api/routes/rejected").await?.json().await?;
|
||||||
assert_eq!(refused, serde_json::json!([]), "nothing refused yet");
|
assert_eq!(refused, serde_json::json!([]), "nothing refused yet");
|
||||||
|
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = server.established(3333, Ipv4Addr::new(10, 0, 0, 2)).await?;
|
||||||
peer.handshake(3333, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
|
peer.announce(announcement(&["193.0.0.0/21", "10.0.0.0/8"], &[3333]))
|
||||||
peer.send(&announcement(&["193.0.0.0/21", "10.0.0.0/8"], &[3333]))
|
|
||||||
.await?;
|
.await?;
|
||||||
peer.send(&announcement(
|
peer.announce(announcement(
|
||||||
&["194.0.1.0/24", "194.0.2.0/24"],
|
&["194.0.1.0/24", "194.0.2.0/24"],
|
||||||
&[3333, 64512],
|
&[3333, 64512],
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
peer.send(&announcement(&["193.0.9.0/25"], &[3333, 2121]))
|
peer.announce(announcement(&["193.0.9.0/25"], &[3333, 2121]))
|
||||||
.await?;
|
.await?;
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("four routes are refused", || {
|
||||||
while server
|
server
|
||||||
.registry
|
.registry
|
||||||
.list()
|
.list()
|
||||||
.first()
|
.first()
|
||||||
.map(|s| s.counters.rejected())
|
.map(|s| s.counters.rejected())
|
||||||
!= Some(4)
|
== Some(4)
|
||||||
{
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -301,12 +252,7 @@ async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
|
|||||||
|
|
||||||
// The record outlives the session; the count of routes does not.
|
// The record outlives the session; the count of routes does not.
|
||||||
drop(peer);
|
drop(peer);
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
server.wait_for_sessions(0).await?;
|
||||||
while !server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let refused: Value = get("/api/routes/rejected").await?.json().await?;
|
let refused: Value = get("/api/routes/rejected").await?.json().await?;
|
||||||
assert_eq!(refused[0]["total"], 4);
|
assert_eq!(refused[0]["total"], 4);
|
||||||
let metrics = get("/metricz").await?.text().await?;
|
let metrics = get("/metricz").await?.text().await?;
|
||||||
|
|||||||
@@ -5,89 +5,47 @@ use std::net::{Ipv4Addr, SocketAddr};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
|
use bgp_integration_tests::{TIMEOUT, peer_config, prefix, wait_for, wait_until};
|
||||||
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
||||||
use bgp_packet::message::Message;
|
use bgp_packet::message::Message;
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||||
use bgp_server::config::PeerConfig;
|
use bgp_server::config::PeerConfig;
|
||||||
use bgp_server::family::FamilySet;
|
|
||||||
use bgp_server::policy::ImportPolicy;
|
use bgp_server::policy::ImportPolicy;
|
||||||
use bgp_server::session::DialState;
|
use bgp_server::session::DialState;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||||
|
|
||||||
/// Starts a server configured to dial `listener`'s address as peer "r1".
|
/// A server that dials its peer "r1", in `remote_asn`, every 200 ms.
|
||||||
async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestServer> {
|
async fn server_dialing(remote_asn: u32) -> Result<ServerWithPeer> {
|
||||||
server_with_peer(listener, remote_asn, false).await
|
server_with_peer(remote_asn, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts a server with peer "r1" at `listener`'s address, which it dials
|
/// A server with peer "r1" in `remote_asn`, which it dials every 200 ms
|
||||||
/// unless the peer is `passive`.
|
/// unless the peer is `passive`.
|
||||||
async fn server_with_peer(
|
async fn server_with_peer(remote_asn: u32, passive: bool) -> Result<ServerWithPeer> {
|
||||||
listener: &TcpListener,
|
TestServer::with_peer(LOCAL_ASN, remote_asn, |c, peer| {
|
||||||
remote_asn: u32,
|
|
||||||
passive: bool,
|
|
||||||
) -> Result<TestServer> {
|
|
||||||
let endpoint: SocketAddr = listener.local_addr()?;
|
|
||||||
TestServer::start_with(LOCAL_ASN, |c| {
|
|
||||||
c.connect_retry = Duration::from_millis(200);
|
c.connect_retry = Duration::from_millis(200);
|
||||||
c.accept_unconfigured_peers = !passive;
|
peer.import = ImportPolicy::permissive();
|
||||||
c.peers = vec![PeerConfig {
|
peer.passive = passive;
|
||||||
graceful_restart: Default::default(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import: ImportPolicy::permissive(),
|
|
||||||
role: None,
|
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn accept(listener: &TcpListener) -> Result<TestPeer> {
|
|
||||||
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
|
||||||
Ok(TestPeer::from_stream(stream))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
|
async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer { server, listener } = server_dialing(PEER_ASN).await?;
|
||||||
let server = server_dialing(&listener, PEER_ASN).await?;
|
|
||||||
|
|
||||||
let mut peer = accept(&listener).await?;
|
let mut peer = TestPeer::accept(&listener).await?;
|
||||||
// Both sides send OPEN on connect; the test peer's handshake order works
|
// Both sides send OPEN on connect; the test peer's handshake order works
|
||||||
// whichever side opened TCP.
|
// whichever side opened TCP.
|
||||||
let open = peer
|
let open = peer.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||||
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
||||||
.await?;
|
|
||||||
assert_eq!(open.asn, LOCAL_ASN as u16);
|
assert_eq!(open.asn, LOCAL_ASN as u16);
|
||||||
|
|
||||||
let mut events = server.registry.subscribe();
|
let listed = server.wait_for_session().await?;
|
||||||
// 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.name.as_deref(), Some("r1"));
|
||||||
assert_eq!(listed.peer.asn, PEER_ASN);
|
assert_eq!(listed.peer.asn, PEER_ASN);
|
||||||
assert_eq!(listed.peer.addr, listener.local_addr()?);
|
assert_eq!(listed.peer.addr, listener.local_addr()?);
|
||||||
@@ -109,39 +67,26 @@ async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
|
|||||||
server
|
server
|
||||||
.local_rib
|
.local_rib
|
||||||
.announce(LocalRoute::new(
|
.announce(LocalRoute::new(
|
||||||
IpPrefix::try_from("10.100.1.0/24")?,
|
prefix("10.100.1.0/24"),
|
||||||
LocalRouteAttrs::default(),
|
LocalRouteAttrs::default(),
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
let update = peer.recv_update().await?;
|
||||||
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_server_reconnects_after_peer_closes() -> Result<()> {
|
async fn test_server_reconnects_after_peer_closes() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer { server, listener } = server_dialing(PEER_ASN).await?;
|
||||||
let server = server_dialing(&listener, PEER_ASN).await?;
|
|
||||||
|
|
||||||
let mut first = accept(&listener).await?;
|
let mut first = TestPeer::accept(&listener).await?;
|
||||||
first
|
first.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||||
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
server.wait_for_session().await?;
|
||||||
.await?;
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
drop(first);
|
drop(first);
|
||||||
|
|
||||||
// Session goes away, then the dialer comes back.
|
// Session goes away, then the dialer comes back.
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
server.wait_for_sessions(0).await?;
|
||||||
while !server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let (_, state) = server
|
let (_, state) = server
|
||||||
.registry
|
.registry
|
||||||
.configured_peers()
|
.configured_peers()
|
||||||
@@ -150,45 +95,30 @@ async fn test_server_reconnects_after_peer_closes() -> Result<()> {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(matches!(state, DialState::Backoff { .. }), "{state:?}");
|
assert!(matches!(state, DialState::Backoff { .. }), "{state:?}");
|
||||||
|
|
||||||
let mut second = accept(&listener).await?;
|
let mut second = TestPeer::accept(&listener).await?;
|
||||||
second
|
second.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||||
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
server.wait_for_session().await?;
|
||||||
.await?;
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_wrong_asn_is_refused_with_bad_peer_as() -> Result<()> {
|
async fn test_wrong_asn_is_refused_with_bad_peer_as() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer {
|
||||||
let _server = server_dialing(&listener, PEER_ASN).await?;
|
server: _server,
|
||||||
|
listener,
|
||||||
|
} = server_dialing(PEER_ASN).await?;
|
||||||
|
|
||||||
let mut peer = accept(&listener).await?;
|
let mut peer = TestPeer::accept(&listener).await?;
|
||||||
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
|
peer.send_open(65099, PEER_ID, 90).await?;
|
||||||
.await?;
|
|
||||||
let _ = peer.recv_open().await?;
|
let _ = peer.recv_open().await?;
|
||||||
let notif = tokio::time::timeout(TIMEOUT, async {
|
let notif = peer.expect_notification().await?;
|
||||||
loop {
|
|
||||||
match peer.recv().await? {
|
|
||||||
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
|
|
||||||
Message::KeepAlive => {}
|
|
||||||
other => eyre::bail!("unexpected {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
||||||
"{notif:?}"
|
"{notif:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
// The dialer retries rather than giving up.
|
// The dialer retries rather than giving up.
|
||||||
let _again = accept(&listener).await?;
|
let _again = TestPeer::accept(&listener).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,32 +130,19 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
|
|||||||
// so the dialer never succeeds; then connect inbound from loopback.
|
// so the dialer never succeeds; then connect inbound from loopback.
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||||
c.connect_retry = Duration::from_secs(60);
|
c.connect_retry = Duration::from_secs(60);
|
||||||
|
let at = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1);
|
||||||
c.peers = vec![PeerConfig {
|
c.peers = vec![PeerConfig {
|
||||||
graceful_restart: Default::default(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import: ImportPolicy::permissive(),
|
import: ImportPolicy::permissive(),
|
||||||
role: None,
|
..peer_config("r1", at, PEER_ASN)
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
}];
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Wrong ASN inbound → refused.
|
// Wrong ASN inbound → refused.
|
||||||
let mut bad = TestPeer::connect(server.addr).await?;
|
let mut bad = TestPeer::connect(server.addr).await?;
|
||||||
bad.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
|
bad.send_open(65099, PEER_ID, 90).await?;
|
||||||
let _ = bad.recv_open().await?;
|
let _ = bad.recv_open().await?;
|
||||||
let msg = tokio::time::timeout(TIMEOUT, bad.recv()).await??;
|
let msg = bad.recv().await?;
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&msg, Message::Notification(n) if matches!(n.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs))),
|
matches!(&msg, Message::Notification(n) if matches!(n.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs))),
|
||||||
"{msg:?}"
|
"{msg:?}"
|
||||||
@@ -233,17 +150,8 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
|
|||||||
|
|
||||||
// Right ASN inbound → established, named.
|
// Right ASN inbound → established, named.
|
||||||
let mut good = TestPeer::connect(server.addr).await?;
|
let mut good = TestPeer::connect(server.addr).await?;
|
||||||
good.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
good.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||||
.await?;
|
let listed = server.wait_for_session().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"));
|
assert_eq!(listed.name.as_deref(), Some("r1"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -253,64 +161,33 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
|
|||||||
/// reported as the reason for backing off rather than silently ignored.
|
/// reported as the reason for backing off rather than silently ignored.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_local_address_is_used_as_the_source() -> Result<()> {
|
async fn test_local_address_is_used_as_the_source() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer {
|
||||||
let endpoint: SocketAddr = listener.local_addr()?;
|
server: _server,
|
||||||
let _server = TestServer::start_with(LOCAL_ASN, |c| {
|
listener,
|
||||||
|
} = TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||||
c.connect_retry = Duration::from_millis(200);
|
c.connect_retry = Duration::from_millis(200);
|
||||||
c.peers = vec![PeerConfig {
|
peer.import = ImportPolicy::permissive();
|
||||||
graceful_restart: Default::default(),
|
peer.local_address = Some(Ipv4Addr::LOCALHOST.into());
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: Some(Ipv4Addr::LOCALHOST.into()),
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import: ImportPolicy::permissive(),
|
|
||||||
role: None,
|
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let (_stream, from) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
let (_stream, from) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
||||||
assert_eq!(from.ip(), std::net::IpAddr::from(Ipv4Addr::LOCALHOST));
|
assert_eq!(from.ip(), std::net::IpAddr::from(Ipv4Addr::LOCALHOST));
|
||||||
|
|
||||||
// 192.0.2.1 is documentation space; no host has it.
|
// 192.0.2.1 is documentation space; no host has it.
|
||||||
|
let endpoint = listener.local_addr()?;
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||||
c.connect_retry = Duration::from_millis(200);
|
c.connect_retry = Duration::from_millis(200);
|
||||||
c.peers = vec![PeerConfig {
|
c.peers = vec![PeerConfig {
|
||||||
graceful_restart: Default::default(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: Some(Ipv4Addr::new(192, 0, 2, 1).into()),
|
local_address: Some(Ipv4Addr::new(192, 0, 2, 1).into()),
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import: ImportPolicy::permissive(),
|
import: ImportPolicy::permissive(),
|
||||||
role: None,
|
..peer_config("r1", endpoint, PEER_ASN)
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
}];
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let reason = tokio::time::timeout(TIMEOUT, async {
|
let reason = wait_for("the dialer is backing off", || {
|
||||||
loop {
|
match server.registry.configured_peers().into_iter().next() {
|
||||||
if let Some((_, DialState::Backoff { last_error, .. })) =
|
Some((_, DialState::Backoff { last_error, .. })) => Some(last_error),
|
||||||
server.registry.configured_peers().into_iter().next()
|
_ => None,
|
||||||
{
|
|
||||||
return last_error;
|
|
||||||
}
|
|
||||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -325,16 +202,14 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
|
|||||||
/// such a peer was dialled every few seconds for ever.
|
/// such a peer was dialled every few seconds for ever.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> Result<()> {
|
async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer { server, listener } = server_dialing(PEER_ASN).await?;
|
||||||
let server = server_dialing(&listener, PEER_ASN).await?;
|
|
||||||
|
|
||||||
let mut waits = Vec::new();
|
let mut waits = Vec::new();
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let mut peer = accept(&listener).await?;
|
let mut peer = TestPeer::accept(&listener).await?;
|
||||||
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
|
peer.send_open(65099, PEER_ID, 90).await?;
|
||||||
.await?;
|
|
||||||
let _ = peer.recv_open().await?;
|
let _ = peer.recv_open().await?;
|
||||||
let refusal = tokio::time::timeout(TIMEOUT, peer.recv()).await??;
|
let refusal = peer.recv().await?;
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&refusal, Message::Notification(n) if n.error == BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
matches!(&refusal, Message::Notification(n) if n.error == BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
|
||||||
"{refusal:?}"
|
"{refusal:?}"
|
||||||
@@ -342,17 +217,17 @@ async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> R
|
|||||||
drop(peer);
|
drop(peer);
|
||||||
|
|
||||||
// How long the dialer says it will wait before trying again.
|
// How long the dialer says it will wait before trying again.
|
||||||
let wait = tokio::time::timeout(TIMEOUT, async {
|
let wait = wait_for("the dialer is backing off", || {
|
||||||
loop {
|
match server.registry.configured_peers().into_iter().next() {
|
||||||
if let Some((_, DialState::Backoff { until, last_error })) =
|
Some((_, DialState::Backoff { until, last_error })) => {
|
||||||
server.registry.configured_peers().into_iter().next()
|
|
||||||
{
|
|
||||||
assert!(last_error.contains("AS65099"), "{last_error}");
|
assert!(last_error.contains("AS65099"), "{last_error}");
|
||||||
return until
|
Some(
|
||||||
.duration_since(std::time::SystemTime::now())
|
until
|
||||||
.unwrap_or_default();
|
.duration_since(std::time::SystemTime::now())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
_ => None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -371,8 +246,7 @@ async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> R
|
|||||||
/// session ended.
|
/// session ended.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_passive_peer_is_not_dialled_and_comes_up_when_it_connects() -> Result<()> {
|
async fn test_a_passive_peer_is_not_dialled_and_comes_up_when_it_connects() -> Result<()> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
let ServerWithPeer { server, listener } = server_with_peer(PEER_ASN, true).await?;
|
||||||
let server = server_with_peer(&listener, PEER_ASN, true).await?;
|
|
||||||
let state = || server.registry.configured_peers()[0].1.clone();
|
let state = || server.registry.configured_peers()[0].1.clone();
|
||||||
|
|
||||||
// Many retry intervals pass, and nothing connects.
|
// Many retry intervals pass, and nothing connects.
|
||||||
@@ -385,29 +259,18 @@ async fn test_a_passive_peer_is_not_dialled_and_comes_up_when_it_connects() -> R
|
|||||||
assert_eq!(state(), DialState::Passive { last_error: None });
|
assert_eq!(state(), DialState::Passive { last_error: None });
|
||||||
|
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
peer.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||||
.await?;
|
let listed = server.wait_for_session().await?;
|
||||||
let listed = tokio::time::timeout(TIMEOUT, async {
|
|
||||||
loop {
|
|
||||||
if let Some(session) = server.registry.list().first().cloned() {
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
assert_eq!(listed.name.as_deref(), Some("r1"));
|
assert_eq!(listed.name.as_deref(), Some("r1"));
|
||||||
|
|
||||||
drop(peer);
|
drop(peer);
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("the peer is shown as waiting, with how it ended", || {
|
||||||
while !matches!(
|
matches!(
|
||||||
state(),
|
state(),
|
||||||
DialState::Passive {
|
DialState::Passive {
|
||||||
last_error: Some(_)
|
last_error: Some(_)
|
||||||
}
|
}
|
||||||
) {
|
)
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
assert!(server.registry.list().is_empty());
|
assert!(server.registry.list().is_empty());
|
||||||
|
|||||||
@@ -7,78 +7,39 @@ use std::net::Ipv4Addr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bgp_integration_tests::default_policy;
|
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
|
use bgp_integration_tests::{TIMEOUT, prefix};
|
||||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||||
use bgp_packet::message::Message;
|
use bgp_packet::message::Message;
|
||||||
use bgp_packet::open::{BgpRole, Capability};
|
use bgp_packet::open::{BgpRole, Capability};
|
||||||
use bgp_primitives::constants::AfiSafi;
|
use bgp_primitives::constants::AfiSafi;
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rib::LocalRoute;
|
use bgp_rib::LocalRoute;
|
||||||
use bgp_server::config::PeerConfig;
|
|
||||||
use bgp_server::family::FamilySet;
|
|
||||||
use bgp_server::policy::{ExportLimits, PeerRole, PrefixSet};
|
use bgp_server::policy::{ExportLimits, PeerRole, PrefixSet};
|
||||||
use eyre::{Result, bail};
|
use eyre::{Result, bail};
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 1299;
|
const LOCAL_ASN: u32 = 1299;
|
||||||
const PEER_ASN: u32 = 3333;
|
const PEER_ASN: u32 = 3333;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What a test holds on to: the server, and the listener its dialling
|
|
||||||
/// goes to.
|
|
||||||
struct Setup {
|
|
||||||
server: TestServer,
|
|
||||||
_listener: TcpListener,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A server that originates `originated`, with one configured peer at the
|
/// A server that originates `originated`, with one configured peer at the
|
||||||
/// address the test connects from, which is a peer of ours held to
|
/// address the test connects from, which is a peer of ours held to
|
||||||
/// `limits`.
|
/// `limits`.
|
||||||
async fn server_with(originated: &[&str], limits: ExportLimits) -> Result<Setup> {
|
async fn server_with(originated: &[&str], limits: ExportLimits) -> Result<ServerWithPeer> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
||||||
let endpoint = listener.local_addr()?;
|
|
||||||
let originate = originated
|
let originate = originated
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| LocalRoute::new(prefix(p), Default::default()))
|
.map(|p| LocalRoute::new(prefix(p), Default::default()))
|
||||||
.collect();
|
.collect();
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||||
c.accept_unconfigured_peers = false;
|
|
||||||
c.originate = originate;
|
c.originate = originate;
|
||||||
c.peers = vec![PeerConfig {
|
peer.export_limits = Some(Arc::new(limits));
|
||||||
graceful_restart: Default::default(),
|
peer.role = Some(PeerRole::Peer);
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: Some(Arc::new(limits)),
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import: default_policy(),
|
|
||||||
role: Some(PeerRole::Peer),
|
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(Setup {
|
|
||||||
server,
|
|
||||||
_listener: listener,
|
|
||||||
})
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A session with the server, as its peer.
|
/// A session with the server, as its peer.
|
||||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
async fn established(server: &TestServer) -> Result<TestPeer> {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
|
||||||
let capabilities = vec![
|
let capabilities = vec![
|
||||||
Capability::FourByteAsn { asn: PEER_ASN },
|
Capability::FourByteAsn { asn: PEER_ASN },
|
||||||
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
|
||||||
@@ -87,9 +48,9 @@ async fn established(server: &TestServer) -> Result<TestPeer> {
|
|||||||
role: BgpRole::Peer,
|
role: BgpRole::Peer,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
peer.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90, capabilities)
|
server
|
||||||
.await?;
|
.established_with(PEER_ASN, PEER_ID, capabilities)
|
||||||
Ok(peer)
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What the peer has been told once it has been quiet for a while: the
|
/// What the peer has been told once it has been quiet for a while: the
|
||||||
@@ -97,8 +58,7 @@ async fn established(server: &TestServer) -> Result<TestPeer> {
|
|||||||
/// did.
|
/// did.
|
||||||
async fn told(peer: &mut TestPeer) -> (BTreeSet<String>, Option<(BgpError, Vec<u8>)>) {
|
async fn told(peer: &mut TestPeer) -> (BTreeSet<String>, Option<(BgpError, Vec<u8>)>) {
|
||||||
let mut routes = BTreeSet::new();
|
let mut routes = BTreeSet::new();
|
||||||
while let Ok(Ok(message)) = tokio::time::timeout(Duration::from_millis(500), peer.recv()).await
|
while let Ok(message) = peer.recv_within(Duration::from_millis(500)).await {
|
||||||
{
|
|
||||||
match message {
|
match message {
|
||||||
Message::Update(update) => {
|
Message::Update(update) => {
|
||||||
for route in &update.withdrawn_routes {
|
for route in &update.withdrawn_routes {
|
||||||
@@ -134,7 +94,8 @@ async fn test_a_route_outside_the_export_limits_is_not_announced() -> Result<()>
|
|||||||
prefixes: allowing(&["193.0.0.0/21"]),
|
prefixes: allowing(&["193.0.0.0/21"]),
|
||||||
max_prefixes: None,
|
max_prefixes: None,
|
||||||
};
|
};
|
||||||
let Setup { server, .. } = server_with(&["193.0.0.0/21", "198.51.100.0/24"], limits).await?;
|
let ServerWithPeer { server, .. } =
|
||||||
|
server_with(&["193.0.0.0/21", "198.51.100.0/24"], limits).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
let (routes, ended) = told(&mut peer).await;
|
let (routes, ended) = told(&mut peer).await;
|
||||||
assert_eq!(routes, BTreeSet::from(["193.0.0.0/21".to_string()]));
|
assert_eq!(routes, BTreeSet::from(["193.0.0.0/21".to_string()]));
|
||||||
@@ -152,7 +113,8 @@ async fn test_going_over_the_export_limit_ends_the_session_and_keeps_the_peer_do
|
|||||||
prefixes: None,
|
prefixes: None,
|
||||||
max_prefixes: Some(2),
|
max_prefixes: Some(2),
|
||||||
};
|
};
|
||||||
let Setup { server, .. } = server_with(&["193.0.0.0/21", "193.0.8.0/21"], limits).await?;
|
let ServerWithPeer { server, .. } =
|
||||||
|
server_with(&["193.0.0.0/21", "193.0.8.0/21"], limits).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
let (routes, ended) = told(&mut peer).await;
|
let (routes, ended) = told(&mut peer).await;
|
||||||
assert_eq!(routes.len(), 2);
|
assert_eq!(routes.len(), 2);
|
||||||
@@ -173,15 +135,12 @@ async fn test_going_over_the_export_limit_ends_the_session_and_keeps_the_peer_do
|
|||||||
text.starts_with("export limit of 2 routes reached"),
|
text.starts_with("export limit of 2 routes reached"),
|
||||||
"{text}"
|
"{text}"
|
||||||
);
|
);
|
||||||
assert!(peer.is_closed().await);
|
assert!(peer.closed_within(TIMEOUT).await);
|
||||||
|
|
||||||
// The peer is kept down: a new connection is closed unanswered.
|
// The peer is kept down: a new connection is closed unanswered.
|
||||||
let mut again = TestPeer::connect(server.addr).await?;
|
let mut again = TestPeer::connect(server.addr).await?;
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
again.closed_unanswered().await,
|
||||||
tokio::time::timeout(TIMEOUT, again.recv()).await,
|
|
||||||
Ok(Err(_))
|
|
||||||
),
|
|
||||||
"a held-down peer gets no session"
|
"a held-down peer gets no session"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -6,52 +6,24 @@
|
|||||||
//! stops with an error, for whatever supervises it to restart it.
|
//! stops with an error, for whatever supervises it to restart it.
|
||||||
|
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::TIMEOUT;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::TestServer;
|
||||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||||
use bgp_packet::message::Message;
|
|
||||||
use bgp_rib::RibStopped;
|
use bgp_rib::RibStopped;
|
||||||
use bgp_server::error::ServerError;
|
use bgp_server::error::ServerError;
|
||||||
use eyre::{Result, bail};
|
use eyre::Result;
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
|
||||||
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
|
|
||||||
.await?;
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads until the server's NOTIFICATION, skipping KEEPALIVEs.
|
|
||||||
async fn farewell(peer: &mut TestPeer) -> Result<BgpError> {
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
loop {
|
|
||||||
match peer.recv().await? {
|
|
||||||
Message::KeepAlive => {}
|
|
||||||
Message::Notification(n) => return Ok(n.error),
|
|
||||||
other => bail!("expected NOTIFICATION, got {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_losing_the_local_rib_stops_the_server_and_tells_the_peers() -> Result<()> {
|
async fn test_losing_the_local_rib_stops_the_server_and_tells_the_peers() -> Result<()> {
|
||||||
let mut server = TestServer::start(LOCAL_ASN).await?;
|
let mut server = TestServer::start(LOCAL_ASN).await?;
|
||||||
let (base, _http) = server.serve_http().await?;
|
let (base, _http) = server.serve_http().await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = server
|
||||||
|
.established(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1))
|
||||||
|
.await?;
|
||||||
|
|
||||||
server.local_rib.stop_for_testing();
|
server.local_rib.stop_for_testing();
|
||||||
|
|
||||||
@@ -61,7 +33,7 @@ async fn test_losing_the_local_rib_stops_the_server_and_tells_the_peers() -> Res
|
|||||||
"{outcome:?}"
|
"{outcome:?}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
farewell(&mut peer).await?,
|
peer.expect_notification().await?.error,
|
||||||
BgpError::Cease(CeaseSubcode::AdminShutdown)
|
BgpError::Cease(CeaseSubcode::AdminShutdown)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,20 +4,17 @@
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::{Announcement, announcement};
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::TestServer;
|
||||||
|
use bgp_integration_tests::{TIMEOUT, wait_until as until};
|
||||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{Message, NotificationMessage, UpdateMessage};
|
||||||
AsPathAttribute, Message, NextHopPathAttribute, NotificationMessage, OriginPathAttribute,
|
|
||||||
PathAttribute, UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_packet::open::{Capability, GracefulRestartEntry};
|
use bgp_packet::open::{Capability, GracefulRestartEntry};
|
||||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_server::restart::Leave;
|
use bgp_server::restart::Leave;
|
||||||
use eyre::{Result, bail};
|
use eyre::{Result, bail};
|
||||||
|
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||||
const PREFIXES: [&str; 3] = ["10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16"];
|
const PREFIXES: [&str; 3] = ["10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16"];
|
||||||
@@ -57,37 +54,13 @@ fn capabilities(more: impl IntoIterator<Item = Capability>) -> Vec<Capability> {
|
|||||||
capabilities
|
capabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
fn announcement(prefixes: &[&str]) -> Message {
|
/// The peer's own routes.
|
||||||
Message::Update(UpdateMessage {
|
fn own(prefixes: &[&str]) -> Announcement {
|
||||||
withdrawn_routes: vec![],
|
announcement(prefixes, &[PEER_ASN])
|
||||||
path_attributes: vec![
|
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
],
|
|
||||||
announced_routes: prefixes
|
|
||||||
.iter()
|
|
||||||
.map(|p| IpPrefix::try_from(*p).unwrap().into())
|
|
||||||
.collect(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn end_of_rib(family: AfiSafi) -> Message {
|
fn end_of_rib(family: AfiSafi) -> UpdateMessage {
|
||||||
Message::Update(UpdateMessage::end_of_rib(family))
|
UpdateMessage::end_of_rib(family)
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits until `done` holds, or fails with `what`.
|
|
||||||
async fn until(what: &str, mut done: impl FnMut() -> bool) -> Result<()> {
|
|
||||||
let waited = tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while !done() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
if waited.is_err() {
|
|
||||||
bail!("timed out waiting until {what}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The prefixes in the server's table, and how many of its routes are
|
/// The prefixes in the server's table, and how many of its routes are
|
||||||
@@ -109,7 +82,7 @@ async fn peer_with_routes(server: &TestServer, offer: Capability) -> Result<Test
|
|||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
peer.handshake_with(PEER_ASN, PEER_ID, 90, capabilities([offer]))
|
peer.handshake_with(PEER_ASN, PEER_ID, 90, capabilities([offer]))
|
||||||
.await?;
|
.await?;
|
||||||
peer.send(&announcement(&PREFIXES)).await?;
|
peer.announce(own(&PREFIXES)).await?;
|
||||||
until("the routes are in", || server.rib.len() == 3).await?;
|
until("the routes are in", || server.rib.len() == 3).await?;
|
||||||
Ok(peer)
|
Ok(peer)
|
||||||
}
|
}
|
||||||
@@ -155,12 +128,12 @@ async fn test_a_restarting_peer_keeps_its_routes_until_it_has_sent_them_again()
|
|||||||
server.registry.list().len() == 1 && server.restarts.kept().is_empty()
|
server.registry.list().len() == 1 && server.restarts.kept().is_empty()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
peer.send(&announcement(&PREFIXES[..2])).await?;
|
peer.announce(own(&PREFIXES[..2])).await?;
|
||||||
until("two are fresh", || table(&server).1 == 1).await?;
|
until("two are fresh", || table(&server).1 == 1).await?;
|
||||||
// The peer has them all, as far as its limit goes.
|
// The peer has them all, as far as its limit goes.
|
||||||
assert_eq!(server.registry.list()[0].counters.routes(), 3);
|
assert_eq!(server.registry.list()[0].counters.routes(), 3);
|
||||||
|
|
||||||
peer.send(&end_of_rib(AfiSafi::IPV4_UNICAST)).await?;
|
peer.announce(end_of_rib(AfiSafi::IPV4_UNICAST)).await?;
|
||||||
until("the one not sent again is gone", || {
|
until("the one not sent again is gone", || {
|
||||||
table(&server) == (vec!["10.1.0.0/16".into(), "10.2.0.0/16".into()], 0)
|
table(&server) == (vec!["10.1.0.0/16".into(), "10.2.0.0/16".into()], 0)
|
||||||
})
|
})
|
||||||
@@ -213,7 +186,7 @@ async fn test_only_a_restart_keeps_the_routes() -> Result<()> {
|
|||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
peer.handshake_with(PEER_ASN, PEER_ID, 90, capabilities(offer))
|
peer.handshake_with(PEER_ASN, PEER_ID, 90, capabilities(offer))
|
||||||
.await?;
|
.await?;
|
||||||
peer.send(&announcement(&PREFIXES)).await?;
|
peer.announce(own(&PREFIXES)).await?;
|
||||||
until("the routes are in", || server.rib.len() == 3).await?;
|
until("the routes are in", || server.rib.len() == 3).await?;
|
||||||
if let Some(goodbye) = &goodbye {
|
if let Some(goodbye) = &goodbye {
|
||||||
peer.send(&Message::Notification(goodbye.clone())).await?;
|
peer.send(&Message::Notification(goodbye.clone())).await?;
|
||||||
@@ -310,7 +283,7 @@ async fn test_we_offer_graceful_restart_and_say_when_our_routes_are_all_sent() -
|
|||||||
// Nothing is originated: the markers are all there is.
|
// Nothing is originated: the markers are all there is.
|
||||||
let mut markers = Vec::new();
|
let mut markers = Vec::new();
|
||||||
while markers.len() < 2 {
|
while markers.len() < 2 {
|
||||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
let update = peer.recv_update().await?;
|
||||||
match update.end_of_rib_family() {
|
match update.end_of_rib_family() {
|
||||||
Some(family) => markers.push(family),
|
Some(family) => markers.push(family),
|
||||||
None => bail!("expected End-of-RIB, got {update:?}"),
|
None => bail!("expected End-of-RIB, got {update:?}"),
|
||||||
@@ -346,14 +319,14 @@ async fn test_leaving_tells_each_peer_as_graceful_restart_has_it() -> Result<()>
|
|||||||
server.registry.leave(leave, Duration::from_secs(2)).await;
|
server.registry.leave(leave, Duration::from_secs(2)).await;
|
||||||
let mut notification = None;
|
let mut notification = None;
|
||||||
// Whatever came before (End-of-RIB markers) is passed over.
|
// Whatever came before (End-of-RIB markers) is passed over.
|
||||||
while let Ok(Ok(message)) = tokio::time::timeout(TIMEOUT, peer.recv()).await {
|
while let Ok(message) = peer.recv().await {
|
||||||
if let Message::Notification(n) = message {
|
if let Message::Notification(n) = message {
|
||||||
notification = Some(n.error);
|
notification = Some(n.error);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(notification, told, "{leave:?} with {offer:?}");
|
assert_eq!(notification, told, "{leave:?} with {offer:?}");
|
||||||
assert!(peer.is_closed().await);
|
assert!(peer.closed_within(TIMEOUT).await);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -3,46 +3,22 @@
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::announcement;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::TestServer;
|
||||||
use bgp_packet::message::{
|
use bgp_integration_tests::{TIMEOUT, get_json, prefix};
|
||||||
AsPathAttribute, CommunitiesPathAttribute, LargeCommunity, Message, NextHopPathAttribute,
|
use bgp_packet::message::LargeCommunity;
|
||||||
OriginPathAttribute, PathAttribute, UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn established_peer(server: &TestServer) -> Result<TestPeer> {
|
async fn established_peer(server: &TestServer) -> Result<TestPeer> {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
server
|
||||||
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
|
.established(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1))
|
||||||
.await?;
|
.await
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
|
|
||||||
Ok(client
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?
|
|
||||||
.json()
|
|
||||||
.await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -155,16 +131,11 @@ async fn test_received_routes_summary() -> Result<()> {
|
|||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let mut peer = established_peer(&server).await?;
|
let mut peer = established_peer(&server).await?;
|
||||||
|
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(
|
||||||
withdrawn_routes: vec![],
|
announcement(&["203.0.113.0/24"], &[PEER_ASN, 65002])
|
||||||
path_attributes: vec![
|
.with_next_hop(Ipv4Addr::new(10, 0, 0, 1))
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
.with_communities(&[(65001, 7)]),
|
||||||
AsPathAttribute::from_asns(vec![PEER_ASN, 65002]),
|
)
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))),
|
|
||||||
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65001, 7)])),
|
|
||||||
],
|
|
||||||
announced_routes: vec![prefix("203.0.113.0/24").into()],
|
|
||||||
}))
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let received = tokio::time::timeout(TIMEOUT, async {
|
let received = tokio::time::timeout(TIMEOUT, async {
|
||||||
|
|||||||
@@ -5,100 +5,51 @@ use std::collections::BTreeSet;
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bgp_integration_tests::default_policy;
|
use bgp_integration_tests::announcement::announcement;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
|
use bgp_integration_tests::{default_policy, prefix, wait_for, wait_until};
|
||||||
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
use bgp_packet::errors::{BgpError, CeaseSubcode};
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{
|
||||||
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
|
CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity, PathAttribute,
|
||||||
Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute, UpdateMessage,
|
|
||||||
};
|
};
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_server::config::PeerConfig;
|
|
||||||
use bgp_server::family::FamilySet;
|
|
||||||
use bgp_server::marks::{Marks, Patterns, Scrub};
|
use bgp_server::marks::{Marks, Patterns, Scrub};
|
||||||
use bgp_server::policy::{ImportPolicy, ImportRule, PrefixSet};
|
use bgp_server::policy::{ImportPolicy, ImportRule, PrefixSet};
|
||||||
use bgp_server::session::DialState;
|
use bgp_server::session::DialState;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
/// AS numbers as they are on the Internet: the checks refuse the private
|
/// AS numbers as they are on the Internet: the checks refuse the private
|
||||||
/// ones the other tests use.
|
/// ones the other tests use.
|
||||||
const LOCAL_ASN: u32 = 1299;
|
const LOCAL_ASN: u32 = 1299;
|
||||||
const PEER_ASN: u32 = 3333;
|
const PEER_ASN: u32 = 3333;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A server with one configured peer, at the address the test connects
|
/// A server with one configured peer, at the address the test connects
|
||||||
/// from, whose routes are judged by `import`. Returns the listener that
|
/// from, whose routes are judged by `import`. The listener that keeps the
|
||||||
/// keeps the server's own dialling occupied.
|
/// server's own dialling occupied comes with it.
|
||||||
async fn server_with(import: ImportPolicy) -> Result<(TestServer, TcpListener)> {
|
async fn server_with(import: ImportPolicy) -> Result<ServerWithPeer> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||||
let endpoint = listener.local_addr()?;
|
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
||||||
c.accept_unconfigured_peers = false;
|
|
||||||
c.connect_retry = Duration::from_millis(50);
|
c.connect_retry = Duration::from_millis(50);
|
||||||
c.peers = vec![PeerConfig {
|
peer.import = import;
|
||||||
graceful_restart: Default::default(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import,
|
|
||||||
role: None,
|
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await
|
||||||
Ok((server, listener))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
async fn established(server: &TestServer) -> Result<TestPeer> {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
server.established(PEER_ASN, PEER_ID).await
|
||||||
peer.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(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Announces IPv4 `prefixes` with the AS path `path`.
|
/// Announces IPv4 `prefixes` with the AS path `path`.
|
||||||
async fn announce(peer: &mut TestPeer, prefixes: &[&str], path: &[u32]) -> Result<()> {
|
async fn announce(peer: &mut TestPeer, prefixes: &[&str], path: &[u32]) -> Result<()> {
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(announcement(prefixes, path)).await
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes: vec![
|
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(path.to_vec()),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
],
|
|
||||||
announced_routes: prefixes.iter().map(|p| prefix(p).into()).collect(),
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits until the server has dealt with `count` announced routes of the
|
/// Waits until the server has dealt with `count` announced routes of the
|
||||||
/// session's, taken or refused, and returns the prefixes in its table.
|
/// session's, taken or refused, and returns the prefixes in its table.
|
||||||
async fn table_after(server: &TestServer, taken: u64, refused: u64) -> Result<BTreeSet<String>> {
|
async fn table_after(server: &TestServer, taken: u64, refused: u64) -> Result<BTreeSet<String>> {
|
||||||
let counters = server.registry.list()[0].counters.clone();
|
let counters = server.registry.list()[0].counters.clone();
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("the routes are dealt with", || {
|
||||||
while (counters.routes(), counters.rejected()) != (taken, refused) {
|
(counters.routes(), counters.rejected()) == (taken, refused)
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
@@ -126,7 +77,10 @@ fn set(prefixes: &[&str]) -> BTreeSet<String> {
|
|||||||
/// through us. Everything a peer sent used to be taken.
|
/// through us. Everything a peer sent used to be taken.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_routes_that_no_peer_should_announce_are_refused() -> Result<()> {
|
async fn test_routes_that_no_peer_should_announce_are_refused() -> Result<()> {
|
||||||
let (server, _listener) = server_with(default_policy()).await?;
|
let ServerWithPeer {
|
||||||
|
server,
|
||||||
|
listener: _listener,
|
||||||
|
} = server_with(default_policy()).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
// One UPDATE with good and bad prefixes: the good ones are taken.
|
// One UPDATE with good and bad prefixes: the good ones are taken.
|
||||||
@@ -164,7 +118,10 @@ async fn test_routes_that_no_peer_should_announce_are_refused() -> Result<()> {
|
|||||||
/// and the new one is not acceptable.
|
/// and the new one is not acceptable.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_refused_route_replaces_the_one_that_was_taken() -> Result<()> {
|
async fn test_a_refused_route_replaces_the_one_that_was_taken() -> Result<()> {
|
||||||
let (server, _listener) = server_with(default_policy()).await?;
|
let ServerWithPeer {
|
||||||
|
server,
|
||||||
|
listener: _listener,
|
||||||
|
} = server_with(default_policy()).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
announce(&mut peer, &["193.0.0.0/21", "193.0.16.0/24"], &[PEER_ASN]).await?;
|
announce(&mut peer, &["193.0.0.0/21", "193.0.16.0/24"], &[PEER_ASN]).await?;
|
||||||
@@ -181,7 +138,10 @@ async fn test_a_refused_route_replaces_the_one_that_was_taken() -> Result<()> {
|
|||||||
async fn test_a_peer_is_held_to_its_prefix_list_and_origins() -> Result<()> {
|
async fn test_a_peer_is_held_to_its_prefix_list_and_origins() -> Result<()> {
|
||||||
let mut prefixes = PrefixSet::default();
|
let mut prefixes = PrefixSet::default();
|
||||||
prefixes.allow(prefix("193.0.0.0/21"), 24);
|
prefixes.allow(prefix("193.0.0.0/21"), 24);
|
||||||
let (server, _listener) = server_with(ImportPolicy {
|
let ServerWithPeer {
|
||||||
|
server,
|
||||||
|
listener: _listener,
|
||||||
|
} = server_with(ImportPolicy {
|
||||||
prefixes: Some(prefixes),
|
prefixes: Some(prefixes),
|
||||||
origin_asns: Some([PEER_ASN, 2121].into()),
|
origin_asns: Some([PEER_ASN, 2121].into()),
|
||||||
..default_policy()
|
..default_policy()
|
||||||
@@ -211,7 +171,7 @@ async fn test_a_peer_is_held_to_its_prefix_list_and_origins() -> Result<()> {
|
|||||||
/// again.
|
/// again.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_peer_over_its_limit_is_cut_off_and_kept_down() -> Result<()> {
|
async fn test_a_peer_over_its_limit_is_cut_off_and_kept_down() -> Result<()> {
|
||||||
let (server, listener) = server_with(ImportPolicy {
|
let ServerWithPeer { server, listener } = server_with(ImportPolicy {
|
||||||
max_prefixes: Some(3),
|
max_prefixes: Some(3),
|
||||||
hold_down: Duration::from_millis(1500),
|
hold_down: Duration::from_millis(1500),
|
||||||
..default_policy()
|
..default_policy()
|
||||||
@@ -219,7 +179,7 @@ async fn test_a_peer_over_its_limit_is_cut_off_and_kept_down() -> Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
// The server dials the peer as well; that connection is left unanswered
|
// The server dials the peer as well; that connection is left unanswered
|
||||||
// until the end.
|
// until the end.
|
||||||
let dialled = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
let dialled = TestPeer::accept(&listener).await?;
|
||||||
|
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
// Refused routes do not count towards the limit, and replacing a
|
// Refused routes do not count towards the limit, and replacing a
|
||||||
@@ -234,58 +194,43 @@ async fn test_a_peer_over_its_limit_is_cut_off_and_kept_down() -> Result<()> {
|
|||||||
assert_eq!(table_after(&server, 3, 1).await?.len(), 3);
|
assert_eq!(table_after(&server, 3, 1).await?.len(), 3);
|
||||||
|
|
||||||
announce(&mut peer, &["193.0.3.0/24"], &[PEER_ASN]).await?;
|
announce(&mut peer, &["193.0.3.0/24"], &[PEER_ASN]).await?;
|
||||||
let notification = tokio::time::timeout(TIMEOUT, async {
|
let notification = peer.expect_notification().await?;
|
||||||
loop {
|
|
||||||
if let Message::Notification(notification) = peer.recv().await? {
|
|
||||||
return Ok::<_, eyre::Report>(notification);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
notification.error,
|
notification.error,
|
||||||
BgpError::Cease(CeaseSubcode::MaxPrefixes)
|
BgpError::Cease(CeaseSubcode::MaxPrefixes)
|
||||||
);
|
);
|
||||||
drop(peer);
|
drop(peer);
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("the peer's routes are gone", || {
|
||||||
while !server.received_routes().is_empty() {
|
server.received_routes().is_empty()
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Held down: a new connection gets nothing.
|
// Held down: a new connection gets nothing.
|
||||||
let mut early = TestPeer::connect(server.addr).await?;
|
let mut early = TestPeer::connect(server.addr).await?;
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
early.closed_unanswered().await,
|
||||||
tokio::time::timeout(TIMEOUT, early.recv()).await,
|
|
||||||
Ok(Err(_))
|
|
||||||
),
|
|
||||||
"a peer that is held down is not answered"
|
"a peer that is held down is not answered"
|
||||||
);
|
);
|
||||||
// Nor is it dialled: once the connection the server had open to it is
|
// Nor is it dialled: once the connection the server had open to it is
|
||||||
// gone, the dialer waits out the hold-down and says so.
|
// gone, the dialer waits out the hold-down and says so.
|
||||||
drop(dialled);
|
drop(dialled);
|
||||||
let reason = tokio::time::timeout(TIMEOUT, async {
|
let reason = wait_for("the dialer says the peer is held down", || {
|
||||||
loop {
|
match server.registry.configured_peers().into_iter().next() {
|
||||||
if let Some((_, DialState::Backoff { last_error, .. })) =
|
Some((_, DialState::Backoff { last_error, .. }))
|
||||||
server.registry.configured_peers().into_iter().next()
|
if last_error.contains("held down") =>
|
||||||
&& last_error.contains("held down")
|
|
||||||
{
|
{
|
||||||
return last_error;
|
Some(last_error)
|
||||||
}
|
}
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
_ => None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
assert!(reason.contains("limit of routes"), "{reason}");
|
assert!(reason.contains("limit of routes"), "{reason}");
|
||||||
|
|
||||||
// When the time is up the peer is dialled again, and taken back.
|
// When the time is up the peer is dialled again, and taken back.
|
||||||
tokio::time::timeout(TIMEOUT, listener.accept()).await??;
|
TestPeer::accept(&listener).await?;
|
||||||
let mut later = TestPeer::connect(server.addr).await?;
|
let mut later = TestPeer::connect(server.addr).await?;
|
||||||
later
|
later.handshake(PEER_ASN, PEER_ID, 90).await?;
|
||||||
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +241,10 @@ async fn test_a_peer_over_its_limit_is_cut_off_and_kept_down() -> Result<()> {
|
|||||||
async fn test_a_peers_routes_are_marked_and_ranked_as_configured() -> Result<()> {
|
async fn test_a_peers_routes_are_marked_and_ranked_as_configured() -> Result<()> {
|
||||||
use bgp_packet::message::{CommunitiesPathAttribute, LocalPrefPathAttribute};
|
use bgp_packet::message::{CommunitiesPathAttribute, LocalPrefPathAttribute};
|
||||||
use bgp_server::marks::Marks;
|
use bgp_server::marks::Marks;
|
||||||
let (server, _listener) = server_with(ImportPolicy {
|
let ServerWithPeer {
|
||||||
|
server,
|
||||||
|
listener: _listener,
|
||||||
|
} = server_with(ImportPolicy {
|
||||||
marks: Marks::parse(
|
marks: Marks::parse(
|
||||||
&["1299:*".to_string()],
|
&["1299:*".to_string()],
|
||||||
&["1299:1001".to_string()],
|
&["1299:1001".to_string()],
|
||||||
@@ -308,16 +256,10 @@ async fn test_a_peers_routes_are_marked_and_ranked_as_configured() -> Result<()>
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(
|
||||||
withdrawn_routes: vec![],
|
announcement(&["193.0.0.0/21", "193.0.8.0/22"], &[PEER_ASN])
|
||||||
path_attributes: vec![
|
.with_communities(&[(3333, 5), (1299, 2002)]),
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
)
|
||||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
PathAttribute::Communities(CommunitiesPathAttribute(vec![(3333, 5), (1299, 2002)])),
|
|
||||||
],
|
|
||||||
announced_routes: vec![prefix("193.0.0.0/21").into(), prefix("193.0.8.0/22").into()],
|
|
||||||
}))
|
|
||||||
.await?;
|
.await?;
|
||||||
table_after(&server, 2, 0).await?;
|
table_after(&server, 2, 0).await?;
|
||||||
let routes = server.received_routes();
|
let routes = server.received_routes();
|
||||||
@@ -388,7 +330,10 @@ async fn test_our_communities_are_not_taken_from_other_networks() -> Result<()>
|
|||||||
}),
|
}),
|
||||||
..default_policy()
|
..default_policy()
|
||||||
};
|
};
|
||||||
let (server, _listener) = server_with(scrubbed).await?;
|
let ServerWithPeer {
|
||||||
|
server,
|
||||||
|
listener: _listener,
|
||||||
|
} = server_with(scrubbed).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
announce_with(&mut peer, &["193.0.0.0/21"], ours_and_theirs()).await?;
|
announce_with(&mut peer, &["193.0.0.0/21"], ours_and_theirs()).await?;
|
||||||
table_after(&server, 1, 0).await?;
|
table_after(&server, 1, 0).await?;
|
||||||
@@ -402,7 +347,10 @@ async fn test_our_communities_are_not_taken_from_other_networks() -> Result<()>
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
let (server, _listener) = server_with(default_policy()).await?;
|
let ServerWithPeer {
|
||||||
|
server,
|
||||||
|
listener: _listener,
|
||||||
|
} = server_with(default_policy()).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
announce_with(&mut peer, &["193.0.0.0/21"], ours_and_theirs()).await?;
|
announce_with(&mut peer, &["193.0.0.0/21"], ours_and_theirs()).await?;
|
||||||
table_after(&server, 1, 0).await?;
|
table_after(&server, 1, 0).await?;
|
||||||
@@ -424,18 +372,8 @@ async fn announce_with(
|
|||||||
prefixes: &[&str],
|
prefixes: &[&str],
|
||||||
more: Vec<PathAttribute>,
|
more: Vec<PathAttribute>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut path_attributes = vec![
|
peer.announce(announcement(prefixes, &[PEER_ASN]).with_attributes(more))
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
.await
|
||||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
];
|
|
||||||
path_attributes.extend(more);
|
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes,
|
|
||||||
announced_routes: prefixes.iter().map(|p| prefix(p).into()).collect(),
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rules do to some routes what the rest of the policy does to all: by
|
/// Rules do to some routes what the rest of the policy does to all: by
|
||||||
@@ -465,7 +403,10 @@ async fn test_rules_apply_to_the_routes_they_hold_for() -> Result<()> {
|
|||||||
],
|
],
|
||||||
..default_policy()
|
..default_policy()
|
||||||
};
|
};
|
||||||
let (server, _listener) = server_with(policy).await?;
|
let ServerWithPeer {
|
||||||
|
server,
|
||||||
|
listener: _listener,
|
||||||
|
} = server_with(policy).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
announce_with(
|
announce_with(
|
||||||
&mut peer,
|
&mut peer,
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ use bgp_primitives::ip_prefix::IpPrefix;
|
|||||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
|
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
/// The OPEN a BIRD 2 router sends for an IPv6-only session from AS210036:
|
/// The OPEN a BIRD 2 router sends for an IPv6-only session from AS210036:
|
||||||
/// one optional parameter carrying MP(IPv6 unicast), route refresh,
|
/// one optional parameter carrying MP(IPv6 unicast), route refresh,
|
||||||
/// extended message, graceful restart (aware only), 4-octet AS, enhanced
|
/// extended message, graceful restart (aware only), 4-octet AS, enhanced
|
||||||
@@ -77,19 +75,13 @@ async fn test_bird_open_and_end_of_rib_are_accepted() -> Result<()> {
|
|||||||
|
|
||||||
peer.send_raw(BIRD_OPEN).await?;
|
peer.send_raw(BIRD_OPEN).await?;
|
||||||
peer.recv_open().await?;
|
peer.recv_open().await?;
|
||||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
match peer.recv().await? {
|
||||||
Message::KeepAlive => {}
|
Message::KeepAlive => {}
|
||||||
other => eyre::bail!("server did not accept BIRD's OPEN: {other:?}"),
|
other => eyre::bail!("server did not accept BIRD's OPEN: {other:?}"),
|
||||||
}
|
}
|
||||||
peer.send(&Message::KeepAlive).await?;
|
peer.send(&Message::KeepAlive).await?;
|
||||||
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
let session = server.wait_for_session().await?;
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let session = server.registry.list().remove(0);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
session.peer.asn, 210036,
|
session.peer.asn, 210036,
|
||||||
"4-octet ASN must come from the capability, not AS_TRANS"
|
"4-octet ASN must come from the capability, not AS_TRANS"
|
||||||
|
|||||||
@@ -12,16 +12,14 @@ use std::process::Command;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use bgp_fib::FibConfig;
|
use bgp_fib::FibConfig;
|
||||||
|
use bgp_integration_tests::announcement::{announcement, withdrawal};
|
||||||
|
use bgp_integration_tests::prefix;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::TestServer;
|
||||||
use bgp_packet::message::{
|
|
||||||
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
|
||||||
UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// The kernel is given more time than the loopback tests allow.
|
||||||
const TIMEOUT: Duration = Duration::from_secs(10);
|
const TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
const TABLE: u32 = 1100;
|
const TABLE: u32 = 1100;
|
||||||
const INTERFACE: &str = "ferrix100";
|
const INTERFACE: &str = "ferrix100";
|
||||||
@@ -87,19 +85,8 @@ impl Drop for Interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn announce(peer: &mut TestPeer, prefixes: &[&str], next_hop: Ipv4Addr) -> Result<()> {
|
async fn announce(peer: &mut TestPeer, prefixes: &[&str], next_hop: Ipv4Addr) -> Result<()> {
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(announcement(prefixes, &[65001]).with_next_hop(next_hop))
|
||||||
withdrawn_routes: vec![],
|
.await
|
||||||
path_attributes: vec![
|
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(vec![65001]),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(next_hop)),
|
|
||||||
],
|
|
||||||
announced_routes: prefixes
|
|
||||||
.iter()
|
|
||||||
.map(|p| IpPrefix::try_from(*p).unwrap().into())
|
|
||||||
.collect(),
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A peer's routes go from the session to the kernel, and out of it again
|
/// A peer's routes go from the session to the kernel, and out of it again
|
||||||
@@ -174,12 +161,8 @@ async fn test_routes_from_a_session_reach_the_kernel() -> Result<()> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// One is withdrawn.
|
// One is withdrawn.
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(withdrawal([prefix("198.18.0.0/15").into()]))
|
||||||
withdrawn_routes: vec![IpPrefix::try_from("198.18.0.0/15").unwrap().into()],
|
.await?;
|
||||||
path_attributes: vec![],
|
|
||||||
announced_routes: vec![],
|
|
||||||
}))
|
|
||||||
.await?;
|
|
||||||
kernel_becomes(&["203.0.113.0/24 via 10.100.0.2 dev ferrix100 proto bgp metric 20"]);
|
kernel_becomes(&["203.0.113.0/24 via 10.100.0.2 dev ferrix100 proto bgp metric 20"]);
|
||||||
|
|
||||||
// The session ends: packets must not go on following its routes.
|
// The session ends: packets must not go on following its routes.
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::announcement;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::{ReceivedRoute, TestServer};
|
||||||
|
use bgp_integration_tests::{peer_config, prefix, wait_for};
|
||||||
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{
|
||||||
AsPathAttribute, Message, MpReachNlriPathAttribute, NextHopPathAttribute, NlriNextHop,
|
AsPathAttribute, Message, MpReachNlriPathAttribute, NlriNextHop, PathAttribute, UpdateMessage,
|
||||||
OriginPathAttribute, PathAttribute, UpdateMessage,
|
|
||||||
};
|
};
|
||||||
use bgp_packet::open::Capability;
|
use bgp_packet::open::Capability;
|
||||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||||
use bgp_server::config::PeerConfig;
|
use bgp_server::config::PeerConfig;
|
||||||
use bgp_server::family::{FamilySet, SUPPORTED};
|
use bgp_server::family::{FamilySet, SUPPORTED};
|
||||||
@@ -23,11 +23,6 @@ use eyre::{Result, bail};
|
|||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn only(family: AfiSafi) -> FamilySet {
|
fn only(family: AfiSafi) -> FamilySet {
|
||||||
[family].into_iter().collect()
|
[family].into_iter().collect()
|
||||||
@@ -37,35 +32,26 @@ fn four_octet() -> Capability {
|
|||||||
Capability::FourByteAsn { asn: PEER_ASN }
|
Capability::FourByteAsn { asn: PEER_ASN }
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_session(server: &TestServer) -> Result<bgp_server::session::SessionHandle> {
|
/// Waits until the server has a route, and returns what it has.
|
||||||
Ok(tokio::time::timeout(TIMEOUT, async {
|
async fn some_routes(server: &TestServer) -> Result<Vec<ReceivedRoute>> {
|
||||||
loop {
|
wait_for("a route is in the table", || {
|
||||||
if let Some(session) = server.registry.list().into_iter().next() {
|
let routes = server.received_routes();
|
||||||
return session;
|
(!routes.is_empty()).then_some(routes)
|
||||||
}
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await?)
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An UPDATE announcing one IPv4 route in the body and one IPv6 route in
|
/// An UPDATE announcing one IPv4 route in the body and one IPv6 route in
|
||||||
/// MP_REACH_NLRI.
|
/// MP_REACH_NLRI.
|
||||||
fn dual_stack_update() -> UpdateMessage {
|
fn dual_stack_update() -> UpdateMessage {
|
||||||
UpdateMessage {
|
let ipv6 = PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||||
withdrawn_routes: vec![],
|
family: AfiSafi::IPV6_UNICAST,
|
||||||
path_attributes: vec![
|
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
prefixes: vec![prefix("2001:db8:1::/48").into()],
|
||||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
});
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
announcement(&["192.0.2.0/24"], &[PEER_ASN])
|
||||||
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
.with_attributes([ipv6])
|
||||||
family: AfiSafi::IPV6_UNICAST,
|
.into()
|
||||||
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
|
|
||||||
prefixes: vec![prefix("2001:db8:1::/48").into()],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
announced_routes: vec![prefix("192.0.2.0/24").into()],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The session carries the families both OPENs offered, and the server's
|
/// The session carries the families both OPENs offered, and the server's
|
||||||
@@ -93,7 +79,7 @@ async fn test_families_are_the_ones_both_sides_offered() -> Result<()> {
|
|||||||
"the server's OPEN offers {family}"
|
"the server's OPEN offers {family}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let session = wait_for_session(&server).await?;
|
let session = server.wait_for_session().await?;
|
||||||
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -114,20 +100,11 @@ async fn test_routes_of_a_family_not_negotiated_are_not_accepted() -> Result<()>
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_session(&server).await?;
|
server.wait_for_session().await?;
|
||||||
|
|
||||||
peer.send(&Message::Update(dual_stack_update())).await?;
|
peer.announce(dual_stack_update()).await?;
|
||||||
|
|
||||||
let routes = tokio::time::timeout(TIMEOUT, async {
|
let routes = some_routes(&server).await?;
|
||||||
loop {
|
|
||||||
let routes = server.received_routes();
|
|
||||||
if !routes.is_empty() {
|
|
||||||
return routes;
|
|
||||||
}
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
// One UPDATE is applied as a whole, so the IPv6 route is not on its way.
|
// One UPDATE is applied as a whole, so the IPv6 route is not on its way.
|
||||||
assert_eq!(routes.len(), 1);
|
assert_eq!(routes.len(), 1);
|
||||||
assert_eq!(routes[0].prefix, prefix("192.0.2.0/24"));
|
assert_eq!(routes[0].prefix, prefix("192.0.2.0/24"));
|
||||||
@@ -159,7 +136,7 @@ async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let session = wait_for_session(&server).await?;
|
let session = server.wait_for_session().await?;
|
||||||
assert_eq!(session.families, only(AfiSafi::IPV6_UNICAST));
|
assert_eq!(session.families, only(AfiSafi::IPV6_UNICAST));
|
||||||
|
|
||||||
// A second peer that takes IPv4 is sent the route. The advertiser
|
// A second peer that takes IPv4 is sent the route. The advertiser
|
||||||
@@ -168,7 +145,7 @@ async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
|
|||||||
let mut dual = TestPeer::connect(server.addr).await?;
|
let mut dual = TestPeer::connect(server.addr).await?;
|
||||||
dual.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
dual.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
|
||||||
.await?;
|
.await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, dual.recv_update()).await??;
|
let update = dual.recv_update().await?;
|
||||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||||
|
|
||||||
let nothing = tokio::time::timeout(Duration::from_millis(200), v6_only.recv_update()).await;
|
let nothing = tokio::time::timeout(Duration::from_millis(200), v6_only.recv_update()).await;
|
||||||
@@ -182,23 +159,11 @@ async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
|
|||||||
async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
|
async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
||||||
// Inbound connections from this address inherit the peer's settings.
|
// Inbound connections from this address inherit the peer's settings.
|
||||||
|
let at = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1);
|
||||||
c.peers = vec![PeerConfig {
|
c.peers = vec![PeerConfig {
|
||||||
graceful_restart: Default::default(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "v6-only".into(),
|
|
||||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: only(AfiSafi::IPV6_UNICAST),
|
families: only(AfiSafi::IPV6_UNICAST),
|
||||||
add_path: None,
|
|
||||||
import: ImportPolicy::permissive(),
|
import: ImportPolicy::permissive(),
|
||||||
role: None,
|
..peer_config("v6-only", at, PEER_ASN)
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
}];
|
||||||
// Keep the dialer out of the way.
|
// Keep the dialer out of the way.
|
||||||
c.connect_retry = Duration::from_secs(3600);
|
c.connect_retry = Duration::from_secs(3600);
|
||||||
@@ -223,7 +188,7 @@ async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
|
|||||||
.collect();
|
.collect();
|
||||||
assert_eq!(offered, [&Capability::multiprotocol(AfiSafi::IPV6_UNICAST)]);
|
assert_eq!(offered, [&Capability::multiprotocol(AfiSafi::IPV6_UNICAST)]);
|
||||||
|
|
||||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
match peer.recv().await? {
|
||||||
Message::Notification(n) => {
|
Message::Notification(n) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
n.error,
|
n.error,
|
||||||
@@ -256,7 +221,7 @@ async fn test_a_peer_offering_an_unsupported_family_is_still_accepted() -> Resul
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let session = wait_for_session(&server).await?;
|
let session = server.wait_for_session().await?;
|
||||||
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
||||||
assert!(
|
assert!(
|
||||||
session.four_octet_asn,
|
session.four_octet_asn,
|
||||||
@@ -281,13 +246,13 @@ async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()
|
|||||||
|
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
peer.handshake_with(PEER_ASN, PEER_ID, 90, vec![]).await?;
|
peer.handshake_with(PEER_ASN, PEER_ID, 90, vec![]).await?;
|
||||||
let session = wait_for_session(&server).await?;
|
let session = server.wait_for_session().await?;
|
||||||
assert!(!session.four_octet_asn);
|
assert!(!session.four_octet_asn);
|
||||||
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
|
||||||
|
|
||||||
// The test peer now decodes AS_PATH two octets wide, as such a speaker
|
// The test peer now decodes AS_PATH two octets wide, as such a speaker
|
||||||
// would; an UPDATE encoded four octets wide would not parse.
|
// would; an UPDATE encoded four octets wide would not parse.
|
||||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
let update = peer.recv_update().await?;
|
||||||
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
|
||||||
assert!(
|
assert!(
|
||||||
update
|
update
|
||||||
@@ -302,26 +267,9 @@ async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()
|
|||||||
assert_eq!(as4_path, Some(vec![WIDE_ASN]));
|
assert_eq!(as4_path, Some(vec![WIDE_ASN]));
|
||||||
|
|
||||||
// And its own two-octet paths are read correctly.
|
// And its own two-octet paths are read correctly.
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(announcement(&["192.0.2.0/24"], &[PEER_ASN, 64512]))
|
||||||
withdrawn_routes: vec![],
|
.await?;
|
||||||
path_attributes: vec![
|
let routes = some_routes(&server).await?;
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(vec![PEER_ASN, 64512]),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
],
|
|
||||||
announced_routes: vec![prefix("192.0.2.0/24").into()],
|
|
||||||
}))
|
|
||||||
.await?;
|
|
||||||
let routes = tokio::time::timeout(TIMEOUT, async {
|
|
||||||
loop {
|
|
||||||
let routes = server.received_routes();
|
|
||||||
if !routes.is_empty() {
|
|
||||||
return routes;
|
|
||||||
}
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
assert!(
|
assert!(
|
||||||
routes[0]
|
routes[0]
|
||||||
.attributes
|
.attributes
|
||||||
|
|||||||
@@ -3,74 +3,36 @@
|
|||||||
//! marked as only to go to customers.
|
//! marked as only to go to customers.
|
||||||
|
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use bgp_integration_tests::default_policy;
|
use bgp_integration_tests::announcement::announcement;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
|
use bgp_integration_tests::{prefix, wait_until};
|
||||||
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{
|
||||||
AsPathAttribute, Message, MpReachNlriPathAttribute, NextHopPathAttribute, NlriNextHop,
|
AsPathAttribute, Message, MpReachNlriPathAttribute, NlriNextHop, OriginPathAttribute,
|
||||||
OriginPathAttribute, PathAttribute, UpdateMessage,
|
PathAttribute, UpdateMessage,
|
||||||
};
|
};
|
||||||
use bgp_packet::open::{BgpRole, Capability};
|
use bgp_packet::open::{BgpRole, Capability};
|
||||||
use bgp_primitives::constants::AfiSafi;
|
use bgp_primitives::constants::AfiSafi;
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rib::LocalRoute;
|
use bgp_rib::LocalRoute;
|
||||||
use bgp_server::config::PeerConfig;
|
|
||||||
use bgp_server::family::FamilySet;
|
|
||||||
use bgp_server::policy::PeerRole;
|
use bgp_server::policy::PeerRole;
|
||||||
use eyre::{Result, bail};
|
use eyre::{Result, bail};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 1299;
|
const LOCAL_ASN: u32 = 1299;
|
||||||
const PEER_ASN: u32 = 3333;
|
const PEER_ASN: u32 = 3333;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What a test holds on to: the server, and the listener its dialling
|
|
||||||
/// goes to.
|
|
||||||
struct Setup {
|
|
||||||
server: TestServer,
|
|
||||||
_listener: TcpListener,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A server with one configured peer at the address the test connects
|
/// A server with one configured peer at the address the test connects
|
||||||
/// from, which is `role` to it, and which originates 193.0.0.0/21.
|
/// from, which is `role` to it, and which originates 193.0.0.0/21.
|
||||||
async fn server_with(role: Option<PeerRole>, require_role: bool) -> Result<Setup> {
|
async fn server_with(role: Option<PeerRole>, require_role: bool) -> Result<ServerWithPeer> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||||
let endpoint = listener.local_addr()?;
|
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
|
||||||
c.accept_unconfigured_peers = false;
|
|
||||||
c.originate = vec![LocalRoute::new(prefix("193.0.0.0/21"), Default::default())];
|
c.originate = vec![LocalRoute::new(prefix("193.0.0.0/21"), Default::default())];
|
||||||
c.peers = vec![PeerConfig {
|
peer.role = role;
|
||||||
graceful_restart: Default::default(),
|
peer.require_role = require_role;
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import: default_policy(),
|
|
||||||
role,
|
|
||||||
require_role,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(Setup {
|
|
||||||
server,
|
|
||||||
_listener: listener,
|
|
||||||
})
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The capabilities of an ordinary peer, and with `role` what it says it
|
/// The capabilities of an ordinary peer, and with `role` what it says it
|
||||||
@@ -88,16 +50,9 @@ fn capabilities(role: Option<BgpRole>) -> Vec<Capability> {
|
|||||||
|
|
||||||
/// A session with the server, on which the peer says it is `says`.
|
/// A session with the server, on which the peer says it is `says`.
|
||||||
async fn established(server: &TestServer, says: Option<BgpRole>) -> Result<TestPeer> {
|
async fn established(server: &TestServer, says: Option<BgpRole>) -> Result<TestPeer> {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
server
|
||||||
peer.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90, capabilities(says))
|
.established_with(PEER_ASN, PEER_ID, capabilities(says))
|
||||||
.await?;
|
.await
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Announces `prefixes` with the AS path `path` and, with `marked_by`, an
|
/// Announces `prefixes` with the AS path `path` and, with `marked_by`, an
|
||||||
@@ -108,30 +63,17 @@ async fn announce(
|
|||||||
path: &[u32],
|
path: &[u32],
|
||||||
marked_by: Option<u32>,
|
marked_by: Option<u32>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut path_attributes = vec![
|
let mut announcement = announcement(prefixes, path);
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
if let Some(asn) = marked_by {
|
||||||
AsPathAttribute::from_asns(path.to_vec()),
|
announcement = announcement.with_only_to_customer(asn);
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
}
|
||||||
];
|
peer.announce(announcement).await
|
||||||
path_attributes.extend(marked_by.map(PathAttribute::OnlyToCustomer));
|
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes,
|
|
||||||
announced_routes: prefixes.iter().map(|p| prefix(p).into()).collect(),
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits until the table holds `count` routes, and returns the mark each
|
/// Waits until the table holds `count` routes, and returns the mark each
|
||||||
/// has, by prefix.
|
/// has, by prefix.
|
||||||
async fn marks(server: &TestServer, count: usize) -> Result<Vec<(String, Option<u32>)>> {
|
async fn marks(server: &TestServer, count: usize) -> Result<Vec<(String, Option<u32>)>> {
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
server.wait_for_routes(count).await.map_err(|_| {
|
||||||
while server.received_routes().len() != count {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| {
|
|
||||||
eyre::eyre!(
|
eyre::eyre!(
|
||||||
"expected {count} routes, the table has {:?}",
|
"expected {count} routes, the table has {:?}",
|
||||||
server.received_routes()
|
server.received_routes()
|
||||||
@@ -167,26 +109,16 @@ fn announced_role(open: &bgp_packet::open::OpenMessage) -> Option<BgpRole> {
|
|||||||
/// shown as one whose peer confirmed its role.
|
/// shown as one whose peer confirmed its role.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_roles_that_fit_together_are_confirmed() -> Result<()> {
|
async fn test_roles_that_fit_together_are_confirmed() -> Result<()> {
|
||||||
let Setup { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
||||||
let (base, _http) = server.serve_http().await?;
|
let (base, _http) = server.serve_http().await?;
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
let open = peer
|
let open = peer
|
||||||
.handshake_with(
|
.handshake_with(PEER_ASN, PEER_ID, 90, capabilities(Some(BgpRole::Customer)))
|
||||||
PEER_ASN,
|
|
||||||
Ipv4Addr::new(10, 0, 0, 2),
|
|
||||||
90,
|
|
||||||
capabilities(Some(BgpRole::Customer)),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
// The peer is our customer: we are its provider.
|
// The peer is our customer: we are its provider.
|
||||||
assert_eq!(announced_role(&open), Some(BgpRole::Provider));
|
assert_eq!(announced_role(&open), Some(BgpRole::Provider));
|
||||||
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
server.wait_for_session().await?;
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let peers: Value = reqwest::get(format!("{base}/api/peers"))
|
let peers: Value = reqwest::get(format!("{base}/api/peers"))
|
||||||
.await?
|
.await?
|
||||||
.json()
|
.json()
|
||||||
@@ -201,18 +133,13 @@ async fn test_roles_that_fit_together_are_confirmed() -> Result<()> {
|
|||||||
/// configured as is refused, with the error RFC 9234 gives for it.
|
/// configured as is refused, with the error RFC 9234 gives for it.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_peer_that_says_it_is_something_else_is_refused() -> Result<()> {
|
async fn test_a_peer_that_says_it_is_something_else_is_refused() -> Result<()> {
|
||||||
let Setup { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
// It thinks it is our peer; we have it down as a customer.
|
// It thinks it is our peer; we have it down as a customer.
|
||||||
peer.send_open_with(
|
peer.send_open_with(PEER_ASN, PEER_ID, 90, capabilities(Some(BgpRole::Peer)))
|
||||||
PEER_ASN,
|
.await?;
|
||||||
Ipv4Addr::new(10, 0, 0, 2),
|
|
||||||
90,
|
|
||||||
capabilities(Some(BgpRole::Peer)),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
peer.recv_open().await?;
|
peer.recv_open().await?;
|
||||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
match peer.recv().await? {
|
||||||
Message::Notification(n) => {
|
Message::Notification(n) => {
|
||||||
assert_eq!(n.error, BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch));
|
assert_eq!(n.error, BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch));
|
||||||
// Subcode 11 on the wire.
|
// Subcode 11 on the wire.
|
||||||
@@ -228,7 +155,7 @@ async fn test_a_peer_that_says_it_is_something_else_is_refused() -> Result<()> {
|
|||||||
/// our word for what it is, unless the configuration requires it to say.
|
/// our word for what it is, unless the configuration requires it to say.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_peer_that_does_not_say_is_refused_only_where_required() -> Result<()> {
|
async fn test_a_peer_that_does_not_say_is_refused_only_where_required() -> Result<()> {
|
||||||
let Setup { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
||||||
let (base, _http) = server.serve_http().await?;
|
let (base, _http) = server.serve_http().await?;
|
||||||
let _peer = established(&server, None).await?;
|
let _peer = established(&server, None).await?;
|
||||||
let peers: Value = reqwest::get(format!("{base}/api/peers"))
|
let peers: Value = reqwest::get(format!("{base}/api/peers"))
|
||||||
@@ -238,12 +165,12 @@ async fn test_a_peer_that_does_not_say_is_refused_only_where_required() -> Resul
|
|||||||
assert_eq!(peers[0]["role"], "customer");
|
assert_eq!(peers[0]["role"], "customer");
|
||||||
assert_eq!(peers[0]["role_confirmed"], false);
|
assert_eq!(peers[0]["role_confirmed"], false);
|
||||||
|
|
||||||
let Setup { server, .. } = server_with(Some(PeerRole::Customer), true).await?;
|
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), true).await?;
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
peer.send_open_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90, capabilities(None))
|
peer.send_open_with(PEER_ASN, PEER_ID, 90, capabilities(None))
|
||||||
.await?;
|
.await?;
|
||||||
peer.recv_open().await?;
|
peer.recv_open().await?;
|
||||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
match peer.recv().await? {
|
||||||
Message::Notification(n) => {
|
Message::Notification(n) => {
|
||||||
assert_eq!(n.error, BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch));
|
assert_eq!(n.error, BgpError::OpenMsg(OpenMsgSubcode::RoleMismatch));
|
||||||
}
|
}
|
||||||
@@ -257,7 +184,7 @@ async fn test_a_peer_that_does_not_say_is_refused_only_where_required() -> Resul
|
|||||||
/// whatever else the peer's policy takes.
|
/// whatever else the peer's policy takes.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_marked_route_from_a_customer_is_a_leak() -> Result<()> {
|
async fn test_a_marked_route_from_a_customer_is_a_leak() -> Result<()> {
|
||||||
let Setup { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Customer), false).await?;
|
||||||
let mut peer = established(&server, Some(BgpRole::Customer)).await?;
|
let mut peer = established(&server, Some(BgpRole::Customer)).await?;
|
||||||
|
|
||||||
// The customer's own route, and one its other provider gave it.
|
// The customer's own route, and one its other provider gave it.
|
||||||
@@ -275,12 +202,7 @@ async fn test_a_marked_route_from_a_customer_is_a_leak() -> Result<()> {
|
|||||||
marks(&server, 1).await?,
|
marks(&server, 1).await?,
|
||||||
[("194.0.0.0/24".to_string(), None)]
|
[("194.0.0.0/24".to_string(), None)]
|
||||||
);
|
);
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("a route is refused", || !server.refusals.peers().is_empty()).await?;
|
||||||
while server.refusals.peers().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let refused = &server.refusals.peers()[0].1;
|
let refused = &server.refusals.peers()[0].1;
|
||||||
assert_eq!(refused.total(), 1);
|
assert_eq!(refused.total(), 1);
|
||||||
assert_eq!(refused.recent()[0].prefix, prefix("8.8.8.0/24"));
|
assert_eq!(refused.recent()[0].prefix, prefix("8.8.8.0/24"));
|
||||||
@@ -296,7 +218,7 @@ async fn test_a_marked_route_from_a_customer_is_a_leak() -> Result<()> {
|
|||||||
/// the route from above or from another peer.
|
/// the route from above or from another peer.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_a_route_marked_by_someone_else_is_a_leak_from_a_peer() -> Result<()> {
|
async fn test_a_route_marked_by_someone_else_is_a_leak_from_a_peer() -> Result<()> {
|
||||||
let Setup { server, .. } = server_with(Some(PeerRole::Peer), false).await?;
|
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Peer), false).await?;
|
||||||
let mut peer = established(&server, Some(BgpRole::Peer)).await?;
|
let mut peer = established(&server, Some(BgpRole::Peer)).await?;
|
||||||
|
|
||||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN], Some(PEER_ASN)).await?;
|
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN], Some(PEER_ASN)).await?;
|
||||||
@@ -326,14 +248,13 @@ async fn test_a_route_marked_by_someone_else_is_a_leak_from_a_peer() -> Result<(
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_routes_from_a_provider_are_marked_on_the_way_in() -> Result<()> {
|
async fn test_routes_from_a_provider_are_marked_on_the_way_in() -> Result<()> {
|
||||||
for four_octet in [true, false] {
|
for four_octet in [true, false] {
|
||||||
let Setup { server, .. } = server_with(Some(PeerRole::Provider), false).await?;
|
let ServerWithPeer { server, .. } = server_with(Some(PeerRole::Provider), false).await?;
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = TestPeer::connect(server.addr).await?;
|
||||||
let mut offered = capabilities(Some(BgpRole::Provider));
|
let mut offered = capabilities(Some(BgpRole::Provider));
|
||||||
if !four_octet {
|
if !four_octet {
|
||||||
offered.retain(|capability| !matches!(capability, Capability::FourByteAsn { .. }));
|
offered.retain(|capability| !matches!(capability, Capability::FourByteAsn { .. }));
|
||||||
}
|
}
|
||||||
peer.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90, offered)
|
peer.handshake_with(PEER_ASN, PEER_ID, 90, offered).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121], None).await?;
|
announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121], None).await?;
|
||||||
// One that someone further up has marked already keeps that mark.
|
// One that someone further up has marked already keeps that mark.
|
||||||
@@ -387,9 +308,9 @@ async fn test_our_routes_are_marked_on_the_way_down_or_across() -> Result<()> {
|
|||||||
(PeerRole::Provider, BgpRole::Provider, false),
|
(PeerRole::Provider, BgpRole::Provider, false),
|
||||||
(PeerRole::RouteServer, BgpRole::RouteServer, false),
|
(PeerRole::RouteServer, BgpRole::RouteServer, false),
|
||||||
] {
|
] {
|
||||||
let Setup { server, .. } = server_with(Some(peer_is), false).await?;
|
let ServerWithPeer { server, .. } = server_with(Some(peer_is), false).await?;
|
||||||
let mut peer = established(&server, Some(says)).await?;
|
let mut peer = established(&server, Some(says)).await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
let update = peer.recv_update().await?;
|
||||||
assert_eq!(update.announced_routes, [prefix("193.0.0.0/21")]);
|
assert_eq!(update.announced_routes, [prefix("193.0.0.0/21")]);
|
||||||
let mark = update
|
let mark = update
|
||||||
.path_attributes
|
.path_attributes
|
||||||
@@ -401,9 +322,9 @@ async fn test_our_routes_are_marked_on_the_way_down_or_across() -> Result<()> {
|
|||||||
assert_eq!(mark, marked.then_some(LOCAL_ASN), "to our {peer_is:?}");
|
assert_eq!(mark, marked.then_some(LOCAL_ASN), "to our {peer_is:?}");
|
||||||
}
|
}
|
||||||
// Nothing is said to a peer whose role is not known.
|
// Nothing is said to a peer whose role is not known.
|
||||||
let Setup { server, .. } = server_with(None, false).await?;
|
let ServerWithPeer { server, .. } = server_with(None, false).await?;
|
||||||
let mut peer = established(&server, None).await?;
|
let mut peer = established(&server, None).await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
|
let update = peer.recv_update().await?;
|
||||||
assert!(
|
assert!(
|
||||||
!update
|
!update
|
||||||
.path_attributes
|
.path_attributes
|
||||||
|
|||||||
@@ -2,34 +2,23 @@
|
|||||||
//! cache, a peer that announces routes, and the validation data changing
|
//! cache, a peer that announces routes, and the validation data changing
|
||||||
//! under both.
|
//! under both.
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bgp_integration_tests::default_policy;
|
use bgp_integration_tests::announcement::{announcement, withdrawal};
|
||||||
use bgp_integration_tests::test_cache::TestCache;
|
use bgp_integration_tests::test_cache::TestCache;
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::{ServerWithPeer, TestServer};
|
||||||
use bgp_packet::message::{
|
use bgp_integration_tests::{default_policy, prefix, wait_until};
|
||||||
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
use bgp_packet::message::{AsPathAttribute, PathAttribute};
|
||||||
UpdateMessage,
|
|
||||||
};
|
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rpki::{CacheState, RpkiConfig, Vrp};
|
use bgp_rpki::{CacheState, RpkiConfig, Vrp};
|
||||||
use bgp_server::config::PeerConfig;
|
|
||||||
use bgp_server::family::FamilySet;
|
|
||||||
use bgp_server::policy::ImportPolicy;
|
use bgp_server::policy::ImportPolicy;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 1299;
|
const LOCAL_ASN: u32 = 1299;
|
||||||
const PEER_ASN: u32 = 3333;
|
const PEER_ASN: u32 = 3333;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn vrp(p: &str, max_length: u8, asn: u32) -> Vrp {
|
fn vrp(p: &str, max_length: u8, asn: u32) -> Vrp {
|
||||||
Vrp {
|
Vrp {
|
||||||
@@ -39,13 +28,6 @@ fn vrp(p: &str, max_length: u8, asn: u32) -> Vrp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a test holds on to: the server, and the listener its dialling
|
|
||||||
/// goes to.
|
|
||||||
struct Setup {
|
|
||||||
server: TestServer,
|
|
||||||
_listener: TcpListener,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A server that validates against `cache`, with one configured peer at
|
/// A server that validates against `cache`, with one configured peer at
|
||||||
/// the address the test connects from. Returns once the server has the
|
/// the address the test connects from. Returns once the server has the
|
||||||
/// cache's data.
|
/// cache's data.
|
||||||
@@ -53,12 +35,9 @@ async fn server_with(
|
|||||||
cache: &TestCache,
|
cache: &TestCache,
|
||||||
expire: Option<Duration>,
|
expire: Option<Duration>,
|
||||||
import: ImportPolicy,
|
import: ImportPolicy,
|
||||||
) -> Result<Setup> {
|
) -> Result<ServerWithPeer> {
|
||||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
||||||
let endpoint = listener.local_addr()?;
|
|
||||||
let cache = cache.address();
|
let cache = cache.address();
|
||||||
let server = TestServer::start_with(LOCAL_ASN, |c| {
|
let setup = TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
||||||
c.accept_unconfigured_peers = false;
|
|
||||||
c.rpki = Some(RpkiConfig {
|
c.rpki = Some(RpkiConfig {
|
||||||
caches: vec![cache],
|
caches: vec![cache],
|
||||||
// Data never expires sooner than a refresh and a retry take.
|
// Data never expires sooner than a refresh and a retry take.
|
||||||
@@ -66,50 +45,19 @@ async fn server_with(
|
|||||||
retry: Some(Duration::from_millis(100)),
|
retry: Some(Duration::from_millis(100)),
|
||||||
expire,
|
expire,
|
||||||
});
|
});
|
||||||
c.peers = vec![PeerConfig {
|
peer.import = import;
|
||||||
graceful_restart: Default::default(),
|
|
||||||
groups: Vec::new(),
|
|
||||||
export_limits: None,
|
|
||||||
passive: false,
|
|
||||||
reflector_client: false,
|
|
||||||
name: "r1".into(),
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
remote_asn: PEER_ASN,
|
|
||||||
hold_time_secs: 90,
|
|
||||||
local_address: None,
|
|
||||||
families: FamilySet::supported(),
|
|
||||||
add_path: None,
|
|
||||||
import,
|
|
||||||
role: None,
|
|
||||||
require_role: false,
|
|
||||||
export: Default::default(),
|
|
||||||
}];
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let rpki = server.rpki.clone().expect("validation is configured");
|
let rpki = setup.server.rpki.clone().expect("validation is configured");
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("the server has the cache's data", || {
|
||||||
while rpki.status().state != CacheState::Synchronised {
|
rpki.status().state == CacheState::Synchronised
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Setup {
|
Ok(setup)
|
||||||
server,
|
|
||||||
_listener: listener,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn established(server: &TestServer) -> Result<TestPeer> {
|
async fn established(server: &TestServer) -> Result<TestPeer> {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
server.established(PEER_ASN, PEER_ID).await
|
||||||
peer.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(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Announces `prefixes` as originated by `origin`, reached through the
|
/// Announces `prefixes` as originated by `origin`, reached through the
|
||||||
@@ -120,53 +68,7 @@ async fn announce(peer: &mut TestPeer, prefixes: &[&str], origin: u32) -> Result
|
|||||||
} else {
|
} else {
|
||||||
vec![PEER_ASN, origin]
|
vec![PEER_ASN, origin]
|
||||||
};
|
};
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(announcement(prefixes, &path)).await
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes: vec![
|
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
||||||
AsPathAttribute::from_asns(path),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
],
|
|
||||||
announced_routes: prefixes.iter().map(|p| prefix(p).into()).collect(),
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set(prefixes: &[&str]) -> BTreeSet<String> {
|
|
||||||
prefixes.iter().map(|p| p.to_string()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits until the table holds exactly `expected` and the peer has `held`
|
|
||||||
/// routes held aside.
|
|
||||||
async fn table_becomes(server: &TestServer, expected: &[&str], held: u64) -> Result<()> {
|
|
||||||
let expected = set(expected);
|
|
||||||
let table = || -> BTreeSet<String> {
|
|
||||||
server
|
|
||||||
.received_routes()
|
|
||||||
.iter()
|
|
||||||
.map(|route| route.prefix.to_string())
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
let held_now = || {
|
|
||||||
server
|
|
||||||
.registry
|
|
||||||
.list()
|
|
||||||
.first()
|
|
||||||
.map_or(0, |session| session.counters.held())
|
|
||||||
};
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while table() != expected || held_now() != held {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| {
|
|
||||||
eyre::eyre!(
|
|
||||||
"the table has {:?} with {} held; expected {expected:?} with {held} held",
|
|
||||||
table(),
|
|
||||||
held_now()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A route whose origin is not the one the holder of the addresses
|
/// A route whose origin is not the one the holder of the addresses
|
||||||
@@ -175,7 +77,7 @@ async fn table_becomes(server: &TestServer, expected: &[&str], held: u64) -> Res
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_an_invalid_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
async fn test_an_invalid_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
||||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
||||||
let Setup { server, .. } = server_with(&cache, None, default_policy()).await?;
|
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||||
let (base, _http) = server.serve_http().await?;
|
let (base, _http) = server.serve_http().await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
@@ -184,7 +86,9 @@ async fn test_an_invalid_route_is_refused_and_the_rest_are_taken() -> Result<()>
|
|||||||
// AS, more specific than it may announce.
|
// AS, more specific than it may announce.
|
||||||
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
||||||
announce(&mut peer, &["193.0.6.0/25"], PEER_ASN).await?;
|
announce(&mut peer, &["193.0.6.0/25"], PEER_ASN).await?;
|
||||||
table_becomes(&server, &["193.0.0.0/21", "194.0.0.0/24"], 1).await?;
|
server
|
||||||
|
.table_becomes(&["193.0.0.0/21", "194.0.0.0/24"], 1)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let get = |path: &str| {
|
let get = |path: &str| {
|
||||||
@@ -230,18 +134,18 @@ async fn test_an_invalid_route_is_refused_and_the_rest_are_taken() -> Result<()>
|
|||||||
async fn test_routes_follow_the_validation_data() -> Result<()> {
|
async fn test_routes_follow_the_validation_data() -> Result<()> {
|
||||||
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
||||||
let cache = TestCache::start(vec![ripe]).await;
|
let cache = TestCache::start(vec![ripe]).await;
|
||||||
let Setup { server, .. } = server_with(&cache, None, default_policy()).await?;
|
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
// Nothing is known about 8.8.8.0/24, so it is taken. 193.0.4.0/24 is
|
// Nothing is known about 8.8.8.0/24, so it is taken. 193.0.4.0/24 is
|
||||||
// covered, and not for this origin.
|
// covered, and not for this origin.
|
||||||
announce(&mut peer, &["8.8.8.0/24", "193.0.4.0/24"], 15169).await?;
|
announce(&mut peer, &["8.8.8.0/24", "193.0.4.0/24"], 15169).await?;
|
||||||
table_becomes(&server, &["8.8.8.0/24"], 1).await?;
|
server.table_becomes(&["8.8.8.0/24"], 1).await?;
|
||||||
|
|
||||||
// The holder of 8.8.8.0/24 authorises another AS: the route in the
|
// The holder of 8.8.8.0/24 authorises another AS: the route in the
|
||||||
// table is now invalid, and goes.
|
// table is now invalid, and goes.
|
||||||
cache.set(vec![ripe, vrp("8.8.8.0/24", 24, 13335)]);
|
cache.set(vec![ripe, vrp("8.8.8.0/24", 24, 13335)]);
|
||||||
table_becomes(&server, &[], 2).await?;
|
server.table_becomes(&[], 2).await?;
|
||||||
let refused = &server.refusals.peers()[0].1;
|
let refused = &server.refusals.peers()[0].1;
|
||||||
assert_eq!(refused.total(), 2);
|
assert_eq!(refused.total(), 2);
|
||||||
assert_eq!(refused.recent()[0].prefix, prefix("8.8.8.0/24"));
|
assert_eq!(refused.recent()[0].prefix, prefix("8.8.8.0/24"));
|
||||||
@@ -254,7 +158,9 @@ async fn test_routes_follow_the_validation_data() -> Result<()> {
|
|||||||
vrp("8.8.8.0/24", 24, 15169),
|
vrp("8.8.8.0/24", 24, 15169),
|
||||||
vrp("193.0.4.0/24", 24, 15169),
|
vrp("193.0.4.0/24", 24, 15169),
|
||||||
]);
|
]);
|
||||||
table_becomes(&server, &["8.8.8.0/24", "193.0.4.0/24"], 0).await?;
|
server
|
||||||
|
.table_becomes(&["8.8.8.0/24", "193.0.4.0/24"], 0)
|
||||||
|
.await?;
|
||||||
for route in server.received_routes() {
|
for route in server.received_routes() {
|
||||||
assert!(
|
assert!(
|
||||||
route
|
route
|
||||||
@@ -273,27 +179,23 @@ async fn test_routes_follow_the_validation_data() -> Result<()> {
|
|||||||
async fn test_a_held_route_the_peer_withdrew_does_not_come_back() -> Result<()> {
|
async fn test_a_held_route_the_peer_withdrew_does_not_come_back() -> Result<()> {
|
||||||
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
||||||
let cache = TestCache::start(vec![ripe]).await;
|
let cache = TestCache::start(vec![ripe]).await;
|
||||||
let Setup { server, .. } = server_with(&cache, None, default_policy()).await?;
|
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
announce(&mut peer, &["193.0.4.0/24", "193.0.5.0/24"], 15169).await?;
|
announce(&mut peer, &["193.0.4.0/24", "193.0.5.0/24"], 15169).await?;
|
||||||
table_becomes(&server, &[], 2).await?;
|
server.table_becomes(&[], 2).await?;
|
||||||
|
|
||||||
// One is withdrawn, the other announced again with the right origin.
|
// One is withdrawn, the other announced again with the right origin.
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(withdrawal([prefix("193.0.4.0/24").into()]))
|
||||||
withdrawn_routes: vec![prefix("193.0.4.0/24").into()],
|
.await?;
|
||||||
path_attributes: vec![],
|
|
||||||
announced_routes: vec![],
|
|
||||||
}))
|
|
||||||
.await?;
|
|
||||||
announce(&mut peer, &["193.0.5.0/24"], PEER_ASN).await?;
|
announce(&mut peer, &["193.0.5.0/24"], PEER_ASN).await?;
|
||||||
table_becomes(&server, &["193.0.5.0/24"], 0).await?;
|
server.table_becomes(&["193.0.5.0/24"], 0).await?;
|
||||||
|
|
||||||
// Now the old origin would be valid for both. Neither old route is
|
// Now the old origin would be valid for both. Neither old route is
|
||||||
// there to be brought back.
|
// there to be brought back.
|
||||||
cache.set(vec![ripe, vrp("193.0.4.0/23", 24, 15169)]);
|
cache.set(vec![ripe, vrp("193.0.4.0/23", 24, 15169)]);
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
table_becomes(&server, &["193.0.5.0/24"], 0).await?;
|
server.table_becomes(&["193.0.5.0/24"], 0).await?;
|
||||||
assert!(
|
assert!(
|
||||||
server.received_routes()[0]
|
server.received_routes()[0]
|
||||||
.attributes
|
.attributes
|
||||||
@@ -312,12 +214,12 @@ async fn test_a_peer_can_be_exempt_from_validation() -> Result<()> {
|
|||||||
accept_rpki_invalid: true,
|
accept_rpki_invalid: true,
|
||||||
..default_policy()
|
..default_policy()
|
||||||
};
|
};
|
||||||
let Setup { server, .. } = server_with(&cache, None, import).await?;
|
let ServerWithPeer { server, .. } = server_with(&cache, None, import).await?;
|
||||||
let (base, _http) = server.serve_http().await?;
|
let (base, _http) = server.serve_http().await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
|
|
||||||
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
||||||
table_becomes(&server, &["193.0.4.0/24"], 0).await?;
|
server.table_becomes(&["193.0.4.0/24"], 0).await?;
|
||||||
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
|
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
|
||||||
.await?
|
.await?
|
||||||
.json()
|
.json()
|
||||||
@@ -333,14 +235,14 @@ async fn test_a_peer_can_be_exempt_from_validation() -> Result<()> {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_held_routes_are_taken_when_the_validation_data_expires() -> Result<()> {
|
async fn test_held_routes_are_taken_when_the_validation_data_expires() -> Result<()> {
|
||||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
||||||
let Setup { server, .. } =
|
let ServerWithPeer { server, .. } =
|
||||||
server_with(&cache, Some(Duration::from_millis(1500)), default_policy()).await?;
|
server_with(&cache, Some(Duration::from_millis(1500)), default_policy()).await?;
|
||||||
let mut peer = established(&server).await?;
|
let mut peer = established(&server).await?;
|
||||||
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
||||||
table_becomes(&server, &[], 1).await?;
|
server.table_becomes(&[], 1).await?;
|
||||||
|
|
||||||
drop(cache);
|
drop(cache);
|
||||||
table_becomes(&server, &["193.0.4.0/24"], 0).await?;
|
server.table_becomes(&["193.0.4.0/24"], 0).await?;
|
||||||
assert!(server.rpki.as_ref().unwrap().current().vrps.is_empty());
|
assert!(server.rpki.as_ref().unwrap().current().vrps.is_empty());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -356,37 +258,21 @@ async fn test_an_unused_as4_path_does_not_make_a_route_valid() -> Result<()> {
|
|||||||
use bgp_packet::message::AsPathSegment;
|
use bgp_packet::message::AsPathSegment;
|
||||||
const HOLDER: u32 = 15169;
|
const HOLDER: u32 = 15169;
|
||||||
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, HOLDER)]).await;
|
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, HOLDER)]).await;
|
||||||
let Setup { server, .. } = server_with(&cache, None, default_policy()).await?;
|
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
||||||
|
|
||||||
// No capabilities: AS numbers are two octets wide on this session.
|
// No capabilities: AS numbers are two octets wide on this session.
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
let mut peer = server.established_with(PEER_ASN, PEER_ID, vec![]).await?;
|
||||||
peer.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90, vec![])
|
|
||||||
.await?;
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
assert!(!server.registry.list()[0].four_octet_asn);
|
assert!(!server.registry.list()[0].four_octet_asn);
|
||||||
|
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
let as4_path = PathAttribute::As4Path(AsPathAttribute {
|
||||||
withdrawn_routes: vec![],
|
segments: vec![AsPathSegment {
|
||||||
path_attributes: vec![
|
ordered: true,
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
path: vec![3356, HOLDER],
|
||||||
AsPathAttribute::from_asns(vec![PEER_ASN]),
|
}],
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
});
|
||||||
PathAttribute::As4Path(AsPathAttribute {
|
peer.announce(announcement(&["193.0.4.0/24"], &[PEER_ASN]).with_attributes([as4_path]))
|
||||||
segments: vec![AsPathSegment {
|
.await?;
|
||||||
ordered: true,
|
server.table_becomes(&[], 1).await?;
|
||||||
path: vec![3356, HOLDER],
|
|
||||||
}],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
announced_routes: vec![prefix("193.0.4.0/24").into()],
|
|
||||||
}))
|
|
||||||
.await?;
|
|
||||||
table_becomes(&server, &[], 1).await?;
|
|
||||||
|
|
||||||
// The data changes, and every held route is looked at again.
|
// The data changes, and every held route is looked at again.
|
||||||
cache.set(vec![
|
cache.set(vec![
|
||||||
@@ -394,14 +280,12 @@ async fn test_an_unused_as4_path_does_not_make_a_route_valid() -> Result<()> {
|
|||||||
vrp("198.51.100.0/24", 24, 64500),
|
vrp("198.51.100.0/24", 24, 64500),
|
||||||
]);
|
]);
|
||||||
let rpki = server.rpki.clone().unwrap();
|
let rpki = server.rpki.clone().unwrap();
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
wait_until("the server has the new data", || {
|
||||||
while rpki.current().vrps.len() != 2 {
|
rpki.current().vrps.len() == 2
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
// Time enough for the look, and then some.
|
// Time enough for the look, and then some.
|
||||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||||
table_becomes(&server, &[], 1).await?;
|
server.table_becomes(&[], 1).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -6,25 +6,21 @@ use std::net::Ipv4Addr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bgp_integration_tests::announcement::{announcement, withdrawal};
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::TestServer;
|
use bgp_integration_tests::test_server::TestServer;
|
||||||
|
use bgp_integration_tests::{TIMEOUT, prefix};
|
||||||
use bgp_packet::message::{
|
use bgp_packet::message::{
|
||||||
AsPathAttribute, CommunitiesPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute,
|
CommunitiesPathAttribute, Message, NextHopPathAttribute, PathAttribute, RouteRefreshMessage,
|
||||||
PathAttribute, RouteRefreshMessage, UpdateMessage,
|
UpdateMessage,
|
||||||
};
|
};
|
||||||
use bgp_primitives::constants::AfiSafi;
|
use bgp_primitives::constants::AfiSafi;
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
|
||||||
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
||||||
use bgp_server::marks::{Control, Marks};
|
use bgp_server::marks::{Control, Marks};
|
||||||
use bgp_server::policy::ExportPolicy;
|
use bgp_server::policy::ExportPolicy;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 64601;
|
const LOCAL_ASN: u32 = 64601;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A server that passes on to every peer what it learned from the others.
|
/// A server that passes on to every peer what it learned from the others.
|
||||||
async fn transit(customize: impl FnOnce(&mut bgp_server::config::Config)) -> Result<TestServer> {
|
async fn transit(customize: impl FnOnce(&mut bgp_server::config::Config)) -> Result<TestServer> {
|
||||||
@@ -40,17 +36,9 @@ async fn transit(customize: impl FnOnce(&mut bgp_server::config::Config)) -> Res
|
|||||||
|
|
||||||
/// The peer with AS `64700 + n`, its session up.
|
/// The peer with AS `64700 + n`, its session up.
|
||||||
async fn peer(server: &TestServer, n: u8) -> Result<TestPeer> {
|
async fn peer(server: &TestServer, n: u8) -> Result<TestPeer> {
|
||||||
let before = server.registry.list().len();
|
server
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
.established(64700 + u32::from(n), Ipv4Addr::new(10, 0, 0, n))
|
||||||
peer.handshake(64700 + u32::from(n), Ipv4Addr::new(10, 0, 0, n), 90)
|
.await
|
||||||
.await?;
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while server.registry.list().len() != before + 1 {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn announce(
|
async fn announce(
|
||||||
@@ -59,31 +47,13 @@ async fn announce(
|
|||||||
path: &[u32],
|
path: &[u32],
|
||||||
communities: &[(u16, u16)],
|
communities: &[(u16, u16)],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut path_attributes = vec![
|
peer.announce(announcement(prefixes, path).with_communities(communities))
|
||||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
.await
|
||||||
AsPathAttribute::from_asns(path.to_vec()),
|
|
||||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
||||||
];
|
|
||||||
if !communities.is_empty() {
|
|
||||||
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
|
||||||
communities.to_vec(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
|
||||||
withdrawn_routes: vec![],
|
|
||||||
path_attributes,
|
|
||||||
announced_routes: prefixes.iter().map(|p| prefix(p).into()).collect(),
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn withdraw(peer: &mut TestPeer, prefixes: &[&str]) -> Result<()> {
|
async fn withdraw(peer: &mut TestPeer, prefixes: &[&str]) -> Result<()> {
|
||||||
peer.send(&Message::Update(UpdateMessage {
|
peer.announce(withdrawal(prefixes.iter().map(|p| prefix(p).into())))
|
||||||
withdrawn_routes: prefixes.iter().map(|p| prefix(p).into()).collect(),
|
.await
|
||||||
path_attributes: vec![],
|
|
||||||
announced_routes: vec![],
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a peer has been told so far, as the prefixes it would now have
|
/// What a peer has been told so far, as the prefixes it would now have
|
||||||
@@ -180,7 +150,7 @@ async fn test_routes_from_one_peer_reach_the_others() -> Result<()> {
|
|||||||
|
|
||||||
// The next hop is ourselves: the address of our end of the session.
|
// The next hop is ourselves: the address of our end of the session.
|
||||||
announce(&mut one, &["10.4.0.0/16"], &[64701], &[]).await?;
|
announce(&mut one, &["10.4.0.0/16"], &[64701], &[]).await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, two.recv_update()).await??;
|
let update = two.recv_update().await?;
|
||||||
assert!(
|
assert!(
|
||||||
update
|
update
|
||||||
.path_attributes
|
.path_attributes
|
||||||
@@ -220,7 +190,7 @@ async fn test_a_peer_is_kept_at_the_best_route() -> Result<()> {
|
|||||||
|
|
||||||
// A better route for it comes: that is the one told now.
|
// A better route for it comes: that is the one told now.
|
||||||
announce(&mut near, &["10.1.0.0/16"], &[64701, 65001], &[]).await?;
|
announce(&mut near, &["10.1.0.0/16"], &[64701, 65001], &[]).await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, watcher.recv_update()).await??;
|
let update = watcher.recv_update().await?;
|
||||||
told.take(&update);
|
told.take(&update);
|
||||||
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64701, 65001]);
|
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64701, 65001]);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -230,7 +200,7 @@ async fn test_a_peer_is_kept_at_the_best_route() -> Result<()> {
|
|||||||
|
|
||||||
// And goes again: back to the other, with no gap.
|
// And goes again: back to the other, with no gap.
|
||||||
withdraw(&mut near, &["10.1.0.0/16"]).await?;
|
withdraw(&mut near, &["10.1.0.0/16"]).await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, watcher.recv_update()).await??;
|
let update = watcher.recv_update().await?;
|
||||||
told.take(&update);
|
told.take(&update);
|
||||||
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64702, 65000, 65001]);
|
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64702, 65000, 65001]);
|
||||||
|
|
||||||
@@ -239,7 +209,7 @@ async fn test_a_peer_is_kept_at_the_best_route() -> Result<()> {
|
|||||||
// told of.
|
// told of.
|
||||||
let mut told_far = Told::default();
|
let mut told_far = Told::default();
|
||||||
told_becomes(&mut far, &mut told_far, &["10.1.0.0/16"]).await?;
|
told_becomes(&mut far, &mut told_far, &["10.1.0.0/16"]).await?;
|
||||||
let gone = tokio::time::timeout(TIMEOUT, far.recv_update()).await??;
|
let gone = far.recv_update().await?;
|
||||||
assert_eq!(gone.withdrawn_routes, vec![prefix("10.1.0.0/16")]);
|
assert_eq!(gone.withdrawn_routes, vec![prefix("10.1.0.0/16")]);
|
||||||
told_nothing(&mut far).await;
|
told_nothing(&mut far).await;
|
||||||
|
|
||||||
@@ -251,7 +221,7 @@ async fn test_a_peer_is_kept_at_the_best_route() -> Result<()> {
|
|||||||
AfiSafi::IPV4_UNICAST,
|
AfiSafi::IPV4_UNICAST,
|
||||||
)))
|
)))
|
||||||
.await?;
|
.await?;
|
||||||
let again = tokio::time::timeout(TIMEOUT, late.recv_update()).await??;
|
let again = late.recv_update().await?;
|
||||||
assert_eq!(again.announced_routes, vec![prefix("10.1.0.0/16")]);
|
assert_eq!(again.announced_routes, vec![prefix("10.1.0.0/16")]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -349,7 +319,7 @@ async fn test_communities_say_who_is_told() -> Result<()> {
|
|||||||
&[(0, 64702), (3356, 3)],
|
&[(0, 64702), (3356, 3)],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let update = tokio::time::timeout(TIMEOUT, three.recv_update()).await??;
|
let update = three.recv_update().await?;
|
||||||
assert!(
|
assert!(
|
||||||
update
|
update
|
||||||
.path_attributes
|
.path_attributes
|
||||||
@@ -378,12 +348,7 @@ async fn test_nothing_learned_is_passed_on_unless_asked_for() -> Result<()> {
|
|||||||
let mut told = Told::default();
|
let mut told = Told::default();
|
||||||
told_becomes(&mut two, &mut told, &["10.99.0.0/16"]).await?;
|
told_becomes(&mut two, &mut told, &["10.99.0.0/16"]).await?;
|
||||||
announce(&mut one, &["10.1.0.0/16"], &[64701], &[]).await?;
|
announce(&mut one, &["10.1.0.0/16"], &[64701], &[]).await?;
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
server.wait_for_routes(1).await?;
|
||||||
while server.rib.len() != 1 {
|
|
||||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
told_nothing(&mut two).await;
|
told_nothing(&mut two).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2,19 +2,18 @@
|
|||||||
//! cost the routes in the UPDATE, not the session.
|
//! cost the routes in the UPDATE, not the session.
|
||||||
|
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use bgp_integration_tests::test_peer::TestPeer;
|
use bgp_integration_tests::test_peer::TestPeer;
|
||||||
use bgp_integration_tests::test_server::{ReceivedRoute, TestServer};
|
use bgp_integration_tests::test_server::{ReceivedRoute, TestServer};
|
||||||
|
use bgp_integration_tests::{prefix, wait_for};
|
||||||
use bgp_packet::errors::{BgpError, UpdateMsgSubcode};
|
use bgp_packet::errors::{BgpError, UpdateMsgSubcode};
|
||||||
use bgp_packet::message::{LocalPrefPathAttribute, Message, PathAttribute};
|
use bgp_packet::message::{LocalPrefPathAttribute, PathAttribute};
|
||||||
use bgp_primitives::ip_prefix::IpPrefix;
|
use bgp_primitives::ip_prefix::IpPrefix;
|
||||||
use bgp_server::metrics::UPDATE_ERRORS;
|
use bgp_server::metrics::UPDATE_ERRORS;
|
||||||
use eyre::{Result, bail};
|
use eyre::Result;
|
||||||
|
|
||||||
const LOCAL_ASN: u32 = 65000;
|
const LOCAL_ASN: u32 = 65000;
|
||||||
const PEER_ASN: u32 = 65001;
|
const PEER_ASN: u32 = 65001;
|
||||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
const WELL_KNOWN: u8 = 0x40;
|
const WELL_KNOWN: u8 = 0x40;
|
||||||
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
|
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
|
||||||
@@ -58,31 +57,24 @@ fn update(attributes: &[Vec<u8>], nlri: &[u8]) -> Vec<u8> {
|
|||||||
const ROUTE_A: [u8; 3] = [16, 10, 1];
|
const ROUTE_A: [u8; 3] = [16, 10, 1];
|
||||||
const ROUTE_B: [u8; 3] = [16, 10, 2];
|
const ROUTE_B: [u8; 3] = [16, 10, 2];
|
||||||
|
|
||||||
fn prefix(s: &str) -> IpPrefix {
|
|
||||||
IpPrefix::try_from(s).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn established(server: &TestServer, asn: u32) -> Result<TestPeer> {
|
async fn established(server: &TestServer, asn: u32) -> Result<TestPeer> {
|
||||||
let mut peer = TestPeer::connect(server.addr).await?;
|
server.established(asn, Ipv4Addr::new(10, 0, 0, 1)).await
|
||||||
peer.handshake(asn, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
|
||||||
Ok(peer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits until the server's Adj-RIB-In holds exactly `prefixes`.
|
/// Waits until the server's Adj-RIB-In holds exactly `prefixes`.
|
||||||
async fn wait_for_routes(server: &TestServer, prefixes: &[&str]) -> Result<Vec<ReceivedRoute>> {
|
async fn wait_for_routes(server: &TestServer, prefixes: &[&str]) -> Result<Vec<ReceivedRoute>> {
|
||||||
let mut expected: Vec<IpPrefix> = prefixes.iter().map(|p| prefix(p)).collect();
|
let mut expected: Vec<IpPrefix> = prefixes.iter().map(|p| prefix(p)).collect();
|
||||||
expected.sort_by_key(|p| p.to_string());
|
expected.sort_by_key(|p| p.to_string());
|
||||||
Ok(tokio::time::timeout(TIMEOUT, async {
|
wait_for(&format!("the table holds {prefixes:?}"), || {
|
||||||
loop {
|
let mut routes = server.received_routes();
|
||||||
let mut routes = server.received_routes();
|
routes.sort_by_key(|r| r.prefix.to_string());
|
||||||
routes.sort_by_key(|r| r.prefix.to_string());
|
routes
|
||||||
if routes.iter().map(|r| &r.prefix).eq(expected.iter()) {
|
.iter()
|
||||||
return routes;
|
.map(|r| &r.prefix)
|
||||||
}
|
.eq(expected.iter())
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
.then_some(routes)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await?)
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The situation RFC 7606 exists for: a route is in the table, and then an
|
/// The situation RFC 7606 exists for: a route is in the table, and then an
|
||||||
@@ -205,24 +197,11 @@ async fn test_an_unreadable_prefix_still_resets_the_session() -> Result<()> {
|
|||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
loop {
|
let notification = peer.expect_notification().await?;
|
||||||
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
assert_eq!(
|
||||||
Message::KeepAlive => {}
|
notification.error,
|
||||||
Message::Notification(n) => {
|
BgpError::UpdateMsg(UpdateMsgSubcode::InvalidNetworkField)
|
||||||
assert_eq!(
|
);
|
||||||
n.error,
|
server.wait_for_sessions(0).await?;
|
||||||
BgpError::UpdateMsg(UpdateMsgSubcode::InvalidNetworkField)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
other => bail!("expected NOTIFICATION, got {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tokio::time::timeout(TIMEOUT, async {
|
|
||||||
while !server.registry.list().is_empty() {
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user