Rhai made a decision about a route cost one to three microseconds, some two hundred times what the same costs in Rust. That was bearable where a function is asked once for an UPDATE, and is not where every route is weighed against every peer, as it is when routes are passed on. What is wanted of a policy (mark routes by where they came from, mark what is announced, let communities say who is told what) needs no language; it is to be configuration, done natively. Kept from that work: route selection goes by a LOCAL_PREF that policy set on a route from an external peer, and the lab's configuration can be added to by a phase. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
296 lines
10 KiB
Rust
296 lines
10 KiB
Rust
//! Finding out what the server has and why: looking routes up, and seeing
|
|
//! what was refused.
|
|
|
|
use std::net::Ipv4Addr;
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::default_policy;
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
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 eyre::Result;
|
|
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".
|
|
async fn routes(client: &reqwest::Client, base: &str, query: &str) -> Result<Vec<String>> {
|
|
let reply = client
|
|
.get(format!("{base}/api/routes/received?{query}"))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
let page: Value = reply.json().await?;
|
|
Ok(page["routes"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|r| {
|
|
format!(
|
|
"{} AS{}{}",
|
|
r["prefix"].as_str().unwrap(),
|
|
r["peer_asn"],
|
|
if r["best"] == true { " best" } else { "" }
|
|
)
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
/// The questions asked when something is wrong with a prefix: who sends it
|
|
/// and which route won, what covers an address, what is inside a block.
|
|
#[tokio::test]
|
|
async fn test_routes_can_be_looked_up() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let (base, _http) = server.serve_http().await?;
|
|
let client = reqwest::Client::new();
|
|
|
|
let mut one = TestPeer::connect(server.addr).await?;
|
|
one.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
|
let mut two = TestPeer::connect(server.addr).await?;
|
|
two.handshake(65002, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
|
|
one.send(&announcement(
|
|
&["10.0.0.0/8", "10.1.0.0/16", "10.1.2.0/24", "192.0.2.0/24"],
|
|
&[65001],
|
|
))
|
|
.await?;
|
|
// A longer path for one of the same prefixes: it loses.
|
|
two.send(&announcement(&["10.1.0.0/16"], &[65002, 65003]))
|
|
.await?;
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while server.rib.len() != 5 {
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
|
|
// One prefix: every route for it, the best first.
|
|
assert_eq!(
|
|
routes(&client, &base, "prefix=10.1.0.0/16").await?,
|
|
["10.1.0.0/16 AS65001 best", "10.1.0.0/16 AS65002"]
|
|
);
|
|
assert!(
|
|
routes(&client, &base, "prefix=10.2.0.0/16")
|
|
.await?
|
|
.is_empty()
|
|
);
|
|
|
|
// An address: every prefix that covers it, the most specific first.
|
|
assert_eq!(
|
|
routes(&client, &base, "address=10.1.2.3").await?,
|
|
[
|
|
"10.1.2.0/24 AS65001 best",
|
|
"10.1.0.0/16 AS65001 best",
|
|
"10.1.0.0/16 AS65002",
|
|
"10.0.0.0/8 AS65001 best"
|
|
]
|
|
);
|
|
assert_eq!(
|
|
routes(&client, &base, "address=10.200.0.1").await?,
|
|
["10.0.0.0/8 AS65001 best"]
|
|
);
|
|
|
|
// A block: itself and everything inside it.
|
|
assert_eq!(
|
|
routes(&client, &base, "within=10.1.0.0/16").await?,
|
|
[
|
|
"10.1.0.0/16 AS65001 best",
|
|
"10.1.0.0/16 AS65002",
|
|
"10.1.2.0/24 AS65001 best"
|
|
]
|
|
);
|
|
|
|
// Only what was chosen; and everything, in address order.
|
|
assert_eq!(
|
|
routes(&client, &base, "within=10.0.0.0/8&best=true").await?,
|
|
[
|
|
"10.0.0.0/8 AS65001 best",
|
|
"10.1.0.0/16 AS65001 best",
|
|
"10.1.2.0/24 AS65001 best"
|
|
]
|
|
);
|
|
assert_eq!(routes(&client, &base, "").await?.len(), 5);
|
|
// A page holds whole prefixes: at most the limit, unless one prefix
|
|
// alone has more.
|
|
assert!((1..=2).contains(&routes(&client, &base, "limit=2").await?.len()));
|
|
|
|
// One peer's routes, by its address; a peer that is not there has none.
|
|
assert_eq!(routes(&client, &base, "peer=127.0.0.1").await?.len(), 5);
|
|
assert!(routes(&client, &base, "peer=192.0.2.9").await?.is_empty());
|
|
|
|
// A query that makes no sense says so.
|
|
for (query, complaint) in [
|
|
("prefix=10.1.0.0", "is not a prefix"),
|
|
("address=example", "is not an IP address"),
|
|
("prefix=10.0.0.0/8&address=10.0.0.1", "at most one of"),
|
|
] {
|
|
let reply = client
|
|
.get(format!("{base}/api/routes/received?{query}"))
|
|
.send()
|
|
.await?;
|
|
assert_eq!(reply.status(), 400, "{query}");
|
|
let text = reply.text().await?;
|
|
assert!(text.contains(complaint), "{query}: {text}");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// A refused route is not in the table, so it has to be findable somewhere
|
|
/// else: which peer, which prefix, with what path, and why. And there is
|
|
/// something to alert on.
|
|
#[tokio::test]
|
|
async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
|
|
let endpoint = listener.local_addr()?;
|
|
let server = TestServer::start_with(1299, |c| {
|
|
c.accept_unconfigured_peers = false;
|
|
c.peers = vec![PeerConfig {
|
|
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,
|
|
}];
|
|
})
|
|
.await?;
|
|
let (base, _http) = server.serve_http().await?;
|
|
let client = reqwest::Client::new();
|
|
let get = |path: &str| {
|
|
let request = client.get(format!("{base}{path}"));
|
|
async move { Ok::<_, eyre::Report>(request.send().await?.error_for_status()?) }
|
|
};
|
|
|
|
let refused: Value = get("/api/routes/rejected").await?.json().await?;
|
|
assert_eq!(refused, serde_json::json!([]), "nothing refused yet");
|
|
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(3333, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
|
|
peer.send(&announcement(&["193.0.0.0/21", "10.0.0.0/8"], &[3333]))
|
|
.await?;
|
|
peer.send(&announcement(
|
|
&["194.0.1.0/24", "194.0.2.0/24"],
|
|
&[3333, 64512],
|
|
))
|
|
.await?;
|
|
peer.send(&announcement(&["193.0.9.0/25"], &[3333, 2121]))
|
|
.await?;
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while server
|
|
.registry
|
|
.list()
|
|
.first()
|
|
.map(|s| s.counters.rejected())
|
|
!= Some(4)
|
|
{
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
|
|
let refused: Value = get("/api/routes/rejected").await?.json().await?;
|
|
let record = &refused[0];
|
|
assert_eq!(record["peer"], "refused-demo");
|
|
assert_eq!(record["peer_addr"], "127.0.0.1");
|
|
assert_eq!(record["total"], 4);
|
|
assert_eq!(
|
|
record["by_reason"],
|
|
serde_json::json!({"bogon AS in path": 2, "bogon prefix": 1, "prefix too specific": 1})
|
|
);
|
|
// The latest first, each with the path it came with.
|
|
let recent: Vec<(String, String, String)> = record["recent"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|r| {
|
|
(
|
|
r["prefix"].as_str().unwrap().to_string(),
|
|
r["reason"].as_str().unwrap().to_string(),
|
|
r["as_path"].to_string(),
|
|
)
|
|
})
|
|
.collect();
|
|
let row = |p: &str, why: &str, path: &str| (p.to_string(), why.to_string(), path.to_string());
|
|
assert_eq!(
|
|
recent,
|
|
[
|
|
row("193.0.9.0/25", "prefix too specific", "[3333,2121]"),
|
|
row("194.0.2.0/24", "bogon AS in path", "[3333,64512]"),
|
|
row("194.0.1.0/24", "bogon AS in path", "[3333,64512]"),
|
|
row("10.0.0.0/8", "bogon prefix", "[3333]"),
|
|
]
|
|
);
|
|
// Asked for by peer, by name or by address.
|
|
for peer in ["refused-demo", "127.0.0.1"] {
|
|
let one: Value = get(&format!("/api/routes/rejected?peer={peer}"))
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
assert_eq!(one.as_array().unwrap().len(), 1, "{peer}");
|
|
}
|
|
let none: Value = get("/api/routes/rejected?peer=someone-else")
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
assert_eq!(none, serde_json::json!([]));
|
|
|
|
// The peer's line in the list of peers has the numbers too.
|
|
let peers: Value = get("/api/peers").await?.json().await?;
|
|
assert_eq!(peers[0]["routes"], 1);
|
|
assert_eq!(peers[0]["rejected_routes"], 4);
|
|
|
|
// What an alert would be made from.
|
|
let metrics = get("/metricz").await?.text().await?;
|
|
for line in [
|
|
r#"bgp_routes_rejected_total{peer="refused-demo",reason="bogon AS in path"} 2"#,
|
|
r#"bgp_routes_rejected_total{peer="refused-demo",reason="bogon prefix"} 1"#,
|
|
r#"bgp_peer_routes{peer="refused-demo"} 1"#,
|
|
r#"bgp_peer_max_prefixes{peer="refused-demo"} 100"#,
|
|
] {
|
|
assert!(metrics.contains(line), "{line} is not in:\n{metrics}");
|
|
}
|
|
|
|
// The record outlives the session; the count of routes does not.
|
|
drop(peer);
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
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?;
|
|
assert_eq!(refused[0]["total"], 4);
|
|
let metrics = get("/metricz").await?.text().await?;
|
|
assert!(!metrics.contains(r#"bgp_peer_routes{peer="refused-demo"}"#));
|
|
Ok(())
|
|
}
|