//! The ping monitor as seen through the HTTP API. The socket is left out //! (it needs privileges); observations are fed in directly. use std::time::Duration; use bgp_integration_tests::test_server::TestServer; use bgp_primitives::ip_prefix::IpPrefix; use bgp_rib::LocalRoute; use bgp_server::ping::PingObservation; use eyre::Result; use serde_json::Value; const TIMEOUT: Duration = Duration::from_secs(5); fn ping(src: &str, dst: &str, hop_limit: u8, seq: u16) -> PingObservation { PingObservation { src: src.parse().unwrap(), dst: dst.parse().unwrap(), interface: Some("swissix0".into()), hop_limit: Some(hop_limit), ident: 1, seq, bytes: 64, packet: [&[128, 0, 0xab, 0xcd, 0, 1][..], &seq.to_be_bytes(), b"hi"].concat(), } } #[tokio::test] async fn pings_are_listed_with_the_route_they_were_sent_to() -> Result<()> { let server = TestServer::start(65000).await?; let (base, _http) = server.serve_http().await?; let client = reqwest::Client::new(); server .local_rib .announce(LocalRoute::new(IpPrefix::try_from("2001:db8:1a::/48").unwrap(), "alice", Default::default())) .await?; server.pings.record(ping("2001:67c:10ec::5", "2001:db8:1a::1", 57, 0)).await; server.pings.record(ping("2001:67c:10ec::5", "2001:db8:1a::1", 57, 1)).await; server.pings.record(ping("2a02:1210::9", "2001:db8:ff::1", 243, 0)).await; server.pings.record(ping("2a02:1210::9", "2a0d:d740::1", 243, 0)).await; // not a watched prefix let view: Value = client.get(format!("{base}/api/pings")).send().await?.error_for_status()?.json().await?; assert_eq!(view["status"], "listening"); assert_eq!(view["prefixes"][0], "2001:db8::/32"); assert_eq!(view["total_flows"], 2); let flows = view["flows"].as_array().unwrap(); let alice = flows.iter().find(|f| f["dst"] == "2001:db8:1a::1").unwrap(); assert_eq!(alice["src"], "2001:67c:10ec::5"); assert_eq!(alice["prefix"], "2001:db8:1a::/48"); assert_eq!(alice["owner"], "alice"); assert_eq!(alice["interface"], "swissix0"); assert_eq!((alice["hop_limit"].as_u64(), alice["hops"].as_u64()), (Some(57), Some(7))); assert_eq!((alice["count"].as_u64(), alice["seq"].as_u64()), (Some(2), Some(1))); assert_eq!(alice["hex"], "8000abcd000100016869"); assert!(alice["last_seen_unix"].as_u64().unwrap() > 1_700_000_000); let stray = flows.iter().find(|f| f["dst"] == "2001:db8:ff::1").unwrap(); assert!(stray["prefix"].is_null() && stray["owner"].is_null()); assert_eq!(stray["hops"], 12); let limited: Value = client.get(format!("{base}/api/pings?limit=1")).send().await?.json().await?; assert_eq!(limited["total_flows"], 2); assert_eq!(limited["flows"].as_array().unwrap().len(), 1); let metrics = client.get(format!("{base}/metricz")).send().await?.text().await?; assert!(metrics.contains("ping_monitor_echo_requests_total")); Ok(()) } #[tokio::test] async fn pings_appear_on_the_event_stream() -> Result<()> { let server = TestServer::start(65000).await?; let (base, _http) = server.serve_http().await?; let mut resp = reqwest::Client::new().get(format!("{base}/api/events")).send().await?.error_for_status()?; // The subscription exists once the response has started; a ping recorded // before that would be missed, so keep pinging until one shows up. let pings = server.pings.clone(); let pinger = tokio::spawn(async move { for seq in 0.. { pings.record(ping("2001:67c:10ec::5", "2001:db8:1a::1", 57, seq)).await; tokio::time::sleep(Duration::from_millis(50)).await; } }); let mut buf = String::new(); let event = tokio::time::timeout(TIMEOUT, async { loop { let chunk = resp.chunk().await?.ok_or_else(|| eyre::eyre!("stream ended"))?; buf.push_str(std::str::from_utf8(&chunk)?); for line in buf.lines().filter_map(|l| l.strip_prefix("data:")) { let ev: Value = serde_json::from_str(line.trim())?; if ev["type"] == "ping_seen" { return Ok::<_, eyre::Error>(ev); } } } }) .await??; pinger.abort(); assert_eq!(event["ping"]["src"], "2001:67c:10ec::5"); assert_eq!(event["ping"]["dst"], "2001:db8:1a::1"); assert_eq!(event["ping"]["interface"], "swissix0"); assert!(event["new_burst"].is_boolean()); Ok(()) }