diff --git a/Cargo.lock b/Cargo.lock index e6d9cb7..d94b4c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,6 +269,7 @@ dependencies = [ "libc", "log", "macro_rules_attribute", + "map-macro", "nftables", "nix", "num", @@ -437,6 +438,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dd856d451cc0da70e2ef2ce95a18e39a93b7558bedf10201ad28503f918568" +[[package]] +name = "map-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb950a42259642e5a3483115aca87eebed2a64886993463af9c9739c205b8d3a" + [[package]] name = "memchr" version = "2.7.4" diff --git a/Cargo.toml b/Cargo.toml index 641c671..9d2e0eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ test-case = "3.3.1" version-compare = "0.2.0" tokio = { version = "1.38.0", features = ["time"] } macro_rules_attribute = "0.2.0" +map-macro = "0.3.0" [profile.release] opt-level = "z" diff --git a/src/bgp/flow.rs b/src/bgp/flow.rs index c6f221e..e64a241 100644 --- a/src/bgp/flow.rs +++ b/src/bgp/flow.rs @@ -15,7 +15,7 @@ use strum::{EnumDiscriminants, FromRepr}; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt}; -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[derive(Clone, PartialOrd, Ord, Serialize, Deserialize)] pub struct Flowspec { afi: Afi, inner: BTreeSet, @@ -143,8 +143,18 @@ impl Display for Flowspec { } } -#[derive(Debug, Clone, Hash, Serialize, Deserialize)] -#[allow(clippy::derived_hash_with_manual_eq)] +impl PartialEq for Flowspec { + fn eq(&self, other: &Self) -> bool { + // ComponentStore's PartialEq only compares kind, so manually implement instead + self.afi == other.afi + && self.inner.len() == other.inner.len() + && self.inner.iter().zip(other.inner.iter()).all(|(a, b)| a.0 == b.0) + } +} + +impl Eq for Flowspec {} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComponentStore(pub Component); impl PartialEq for ComponentStore { diff --git a/src/bgp/mod.rs b/src/bgp/mod.rs index 9dd4747..0bdba39 100644 --- a/src/bgp/mod.rs +++ b/src/bgp/mod.rs @@ -35,7 +35,7 @@ use tokio::time::{interval, Duration, Instant, Interval}; use State::*; #[cfg(test)] -use tokio::sync::mpsc; +use {crate::integration_tests::TestEvent, tokio::sync::mpsc}; /// A (currently passive only) BGP session. /// @@ -60,11 +60,11 @@ pub struct Session { state: State, routes: Routes, #[cfg(test)] - event_tx: mpsc::Sender<()>, + event_tx: mpsc::Sender, } impl Session { - pub async fn new(config: RunArgs, #[cfg(test)] event_tx: mpsc::Sender<()>) -> Result { + pub async fn new(config: RunArgs, #[cfg(test)] event_tx: mpsc::Sender) -> Result { let kernel = if config.dry_run { KernelAdapter::Noop } else { @@ -192,9 +192,14 @@ impl Session { Ok(Message::Update(msg)) => if let Some((afi, safi)) = msg.is_end_of_rib() { debug!("received End-of-RIB of ({afi}, {safi:?})"); #[cfg(test)] - let _ = self.event_tx.send(()).await; + let _ = self.event_tx.send(TestEvent::EndOfRib(afi, safi)).await; + } else { debug!("received update: {msg:?}"); + #[cfg(test)] + let _ = self.event_tx.send(TestEvent::Update(msg.clone())).await; + + // here `msg` is partially moved if msg.nlri.is_some() || msg.old_nlri.is_some() { let route_info = Rc::new(msg.route_info); for n in msg.nlri.into_iter().chain(msg.old_nlri) { diff --git a/src/bgp/msg.rs b/src/bgp/msg.rs index 43319d3..e726c51 100644 --- a/src/bgp/msg.rs +++ b/src/bgp/msg.rs @@ -721,7 +721,7 @@ impl MessageSend for UpdateMessage<'_> { /// test the correctness of the deserialization logic. With that said, it is /// important to make this function correct as well. fn write_data(&self, buf: &mut Vec) { - let old_nlri = match &self.old_nlri.as_ref().map(Nlri::kind) { + let old_nlri = match &self.old_nlri.as_ref().map(Nlri::content) { Some(NlriContent::Unicast { prefixes, next_hop: NextHop::V4(next_hop), .. }) => Some((prefixes, next_hop)), Some(_) => panic!("BGP-4 NLRI supports IPv4 only"), None => None, @@ -731,7 +731,7 @@ impl MessageSend for UpdateMessage<'_> { // BGP-4 withdrawn routes extend_with_u16_len(buf, |buf| { - let Some(NlriContent::Unicast { prefixes, .. }) = &self.old_withdrawn.as_ref().map(Nlri::kind) else { + let Some(NlriContent::Unicast { prefixes, .. }) = &self.old_withdrawn.as_ref().map(Nlri::content) else { panic!("BGP-4 withdrawn routes support IPv4 only"); }; prefixes.iter().for_each(|p| { diff --git a/src/bgp/nlri.rs b/src/bgp/nlri.rs index 9219e98..7b79b87 100644 --- a/src/bgp/nlri.rs +++ b/src/bgp/nlri.rs @@ -54,10 +54,14 @@ impl Nlri { Ok(Self { afi, content: NlriContent::Flow { specs } }) } - pub fn kind(&self) -> &NlriContent { + pub fn content(&self) -> &NlriContent { &self.content } + pub fn into_content(self) -> NlriContent { + self.content + } + pub fn write_mp_reach(&self, buf: &mut Vec) { self.write_mp(buf, true); } @@ -74,7 +78,7 @@ impl Nlri { } as u8, ]); buf.extend(u16::to_be_bytes(self.afi as _)); - match self.kind() { + match self.content() { NlriContent::Unicast { prefixes, next_hop } => { extend_with_u16_len(buf, |buf| { buf.push(NlriKind::Unicast as u8); diff --git a/src/integration_tests/basic.rs b/src/integration_tests/basic.rs index 4945e24..d527400 100644 --- a/src/integration_tests/basic.rs +++ b/src/integration_tests/basic.rs @@ -1,43 +1,145 @@ use super::helpers::bird::ensure_bird_2; use super::helpers::cli::run_cli_with_bird; use super::helpers::kernel::{ensure_loopback_up, pick_port}; -use super::helpers::str_to_file; -use super::test_local; +use super::{test_local, TestEvent}; use crate::args::Cli; +use crate::bgp::flow::Component::*; +use crate::bgp::flow::{Flowspec, Op, Ops}; +use crate::bgp::nlri::NlriContent; use anyhow::bail; use clap::Parser; use macro_rules_attribute::apply; +use map_macro::btree_map; +use std::collections::BTreeSet; use std::time::Duration; use tokio::select; use tokio::time::sleep; +const BIRD_FILE: &str = "\ +router id 10.234.56.78; + +flow4 table myflow4; +flow6 table myflow6; + +protocol static f4 { + flow4 { table myflow4; }; + @@FLOW4@@; +} + +protocol static f6 { + flow6 { table myflow6; }; + @@FLOW6@@; +} + +protocol bgp flow_test { + debug all; + connect delay time 1; + + local ::1 port @@BIRD_PORT@@ as 65000; + neighbor ::1 port @@FLOW_PORT@@ as 65000; + multihop; + + flow4 { table myflow4; import none; export all; }; + flow6 { table myflow6; import none; export all; }; +}"; + #[apply(test_local!)] -async fn test_basic() -> anyhow::Result<()> { +async fn test_routes() -> anyhow::Result<()> { ensure_bird_2()?; ensure_loopback_up().await?; + let flows = btree_map! { + Flowspec::new_v4() + .with(DstPrefix("10.0.0.0/8".parse()?, 0))? + .with(PacketLen(Ops::new(Op::gt(1024))))? => "flow4 { dst 10.0.0.0/8; length > 1024; }", + Flowspec::new_v4() + .with(SrcPrefix("123.45.67.192/26".parse()?, 0))? => "flow4 { src 123.45.67.192/26; }", + Flowspec::new_v6() + .with(DstPrefix("fec0:1122:3344:5566:7788:99aa:bbcc:ddee/128".parse()?, 0))? + .with(TcpFlags(Op::all(0x3).and(Op::not_any(0x1)).and(Op::any(0xff)).or(Op::all(0x33))))? + .with(DstPort(Ops::new(Op::eq(6000))))? + .with(Fragment(Op::not_any(0b10).or(Op::not_any(0b100))))? => "flow6 { \ + dst fec0:1122:3344:5566:7788:99aa:bbcc:ddee/128; \ + tcp flags 0x03/0x0f && !0/0xff || 0x33/0x33; \ + dport = 6000; \ + fragment !is_fragment || !first_fragment; }" + }; + + let (flow4, flow6) = flows.iter().fold((String::new(), String::new()), |(v4, v6), (k, v)| { + if k.is_ipv4() { + (v4 + "route " + v + ";", v6) + } else { + (v4, v6 + "route " + v + ";") + } + }); + let flow_port = pick_port().await?.to_string(); let cli = Cli::try_parse_from(["flow", "run", "-v", "--dry-run", &format!("--bind=[::1]:{flow_port}")])?; - let bird = include_str!("config/basic.bird.conf.in") + let bird = BIRD_FILE .replace("@@BIRD_PORT@@", &pick_port().await?.to_string()) - .replace("@@FLOW_PORT@@", &flow_port); - let bird = str_to_file(bird.as_bytes()).await?; - let (mut cli, mut bird, mut events, _temp_dir) = run_cli_with_bird(cli, bird.file_path()).await?; + .replace("@@FLOW_PORT@@", &flow_port) + .replace("@@FLOW4@@", &flow4) + .replace("@@FLOW6@@", &flow6); + + let (mut cli, mut bird, mut events, _g) = run_cli_with_bird(cli, &bird).await?; let mut end_of_rib_count = 0; + let mut visited = BTreeSet::new(); loop { select! { - Some(()) = events.recv(), if !events.is_closed() => { - end_of_rib_count += 1; - if end_of_rib_count >= 2 { - break; + Some(event) = events.recv(), if !events.is_closed() => match event { + TestEvent::EndOfRib(_afi, _safi) => { + end_of_rib_count += 1; + if end_of_rib_count >= 2 { + break; + } } - } + TestEvent::Update(msg) => { + for nlri in msg.nlri.into_iter().chain(msg.old_nlri) { + let NlriContent::Flow { specs } = nlri.into_content() else { + bail!("received NLRI other than flowspec"); + }; + for spec in specs { + if flows.contains_key(dbg!(&spec)) { + println!("contains"); + if !visited.insert(spec.clone()) { + bail!("received duplicate flowspec: {spec}"); + } + } else { + bail!("received unknown flowspec: {spec}"); + } + } + } + } + }, _ = sleep(Duration::from_secs(10)) => bail!("timed out"), code = &mut cli => bail!("CLI exited early with code {}", code??), status = bird.wait() => bail!("BIRD exited early with {}", status?), } } + if flows.len() != visited.len() { + let orig_set = flows.into_iter().map(|x| x.0).collect::>(); + let diff = orig_set.difference(&visited).collect::>(); + bail!("some flowspecs not received: {diff:?}"); + } + + Ok(()) +} + +#[test] +fn test_test() -> anyhow::Result<()> { + assert_eq!( + Flowspec::new_v6() + .with(DstPrefix("fec0:1122:3344:5566:7788:99aa:bbcc:ddee/128".parse()?, 0))? + .with(TcpFlags(Op::all(0x3).and(Op::not_any(0x1)).and(Op::any(0xff)).or(Op::all(0x33))))? + .with(DstPort(Ops::new(Op::eq(6000))))? + .with(Fragment(Op::not_any(0b10).or(Op::not_any(0b100))))?, + Flowspec::new_v6() + .with(DstPrefix("fec0:1122:3344:5566:7788:99aa:bbcc:ddee/128".parse()?, 0))? + .with(TcpFlags(Op::all(0x3).and(Op::not_any(0xc)).and(Op::any(0xff)).or(Op::all(0x33))))? + .with(DstPort(Ops::new(Op::eq(6000))))? + .with(Fragment(Op::not_any(0b10).or(Op::not_any(0b100))))? + ); Ok(()) } diff --git a/src/integration_tests/config/basic.bird.conf.in b/src/integration_tests/config/basic.bird.conf.in deleted file mode 100644 index b2d761a..0000000 --- a/src/integration_tests/config/basic.bird.conf.in +++ /dev/null @@ -1,45 +0,0 @@ -router id 10.234.56.78; - -flow4 table myflow4; -flow6 table myflow6; - -protocol static f4 { - flow4 { table myflow4; }; - - route flow4 { - dst 10.0.0.0/8; - length > 1024; - } { - bgp_ext_community.add(( - unknown 0x810c, # redirect-to-ip - 1.1.1.1, 0 - )); - }; -} - -protocol static f6 { - flow6 { table myflow6; }; - - route flow6 { - dst fec0:1122:3344:5566:7788:99aa:bbcc:ddee/128; - tcp flags 0x03/0x0f && !0/0xff || 0x33/0x33; - dport = 6000; - fragment !is_fragment || !first_fragment; - } { - bgp_community.add((65001, 11451)); - bgp_ext_community.add((generic, 0x080060000, 0)); - bgp_ext_community.add((ro, 65002, 1919)); - }; -} - -protocol bgp flow_test { - debug all; - connect delay time 1; - - local ::1 port @@BIRD_PORT@@ as 65000; - neighbor ::1 port @@FLOW_PORT@@ as 65000; - multihop; - - flow4 { table myflow4; import none; export all; }; - flow6 { table myflow6; import none; export all; }; -} diff --git a/src/integration_tests/helpers/cli.rs b/src/integration_tests/helpers/cli.rs index 2349488..6d605a8 100644 --- a/src/integration_tests/helpers/cli.rs +++ b/src/integration_tests/helpers/cli.rs @@ -1,15 +1,16 @@ use super::bird::run_bird; +use super::str_to_file; use crate::args::Cli; use crate::cli_entry; -use async_tempfile::TempDir; -use std::path::Path; +use crate::integration_tests::TestEvent; +use async_tempfile::{TempDir, TempFile}; use tokio::process::Child; use tokio::sync::mpsc; use tokio::task::JoinHandle; pub type CliChild = JoinHandle>; -fn run_cli(options: Cli, event_tx: mpsc::Sender<()>) -> CliChild { +fn run_cli(options: Cli, event_tx: mpsc::Sender) -> CliChild { tokio::task::spawn_local(async { let exit_code = cli_entry(options, event_tx).await; anyhow::Ok(exit_code) @@ -18,13 +19,15 @@ fn run_cli(options: Cli, event_tx: mpsc::Sender<()>) -> CliChild { pub async fn run_cli_with_bird( mut cli_opt: Cli, - bird_conf_path: impl AsRef, -) -> anyhow::Result<(CliChild, Child, mpsc::Receiver<()>, TempDir)> { + bird_conf: &str, +) -> anyhow::Result<(CliChild, Child, mpsc::Receiver, (TempFile, TempDir))> { + let bird_conf_file = str_to_file(bird_conf.as_bytes()).await?; + let sock_dir = TempDir::new().await?; cli_opt.run_dir = sock_dir.as_ref().into(); - let bird = run_bird(bird_conf_path.as_ref(), sock_dir.join("bird.sock")).await?; + let bird = run_bird(bird_conf_file.file_path(), sock_dir.join("bird.sock")).await?; let (event_tx, event_rx) = mpsc::channel(127); let cli = run_cli(cli_opt, event_tx); - Ok((cli, bird, event_rx, sock_dir)) + Ok((cli, bird, event_rx, (bird_conf_file, sock_dir))) } diff --git a/src/integration_tests/mod.rs b/src/integration_tests/mod.rs index 54dcd98..1c6d41a 100644 --- a/src/integration_tests/mod.rs +++ b/src/integration_tests/mod.rs @@ -3,7 +3,15 @@ mod helpers; -mod basic; +use crate::bgp::msg::UpdateMessage; +use crate::bgp::nlri::NlriKind; +use crate::net::Afi; + +#[derive(Debug, Clone)] +pub enum TestEvent { + EndOfRib(Afi, NlriKind), + Update(UpdateMessage<'static>), +} macro_rules! test_local { ( @@ -20,3 +28,6 @@ macro_rules! test_local { } pub(crate) use test_local; + +// Test files +mod basic; diff --git a/src/main.rs b/src/main.rs index b1c5190..a72896f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ pub mod util; mod args; #[cfg(test)] -mod integration_tests; +pub mod integration_tests; use anstyle::{Reset, Style}; use anyhow::Context; @@ -29,17 +29,20 @@ use tokio::select; use util::{BOLD, FG_GREEN_BOLD, RESET}; #[cfg(test)] -use {std::future::pending, tokio::sync::mpsc}; +use {integration_tests::TestEvent, std::future::pending, tokio::sync::mpsc}; #[cfg(not(test))] use { futures::future::{select, FutureExt}, - std::process::ExitCode, tokio::pin, tokio::signal::unix::{signal, SignalKind}, }; -async fn run(mut args: RunArgs, sock_path: &Path, #[cfg(test)] event_tx: mpsc::Sender<()>) -> anyhow::Result { +async fn run( + mut args: RunArgs, + sock_path: &Path, + #[cfg(test)] event_tx: mpsc::Sender, +) -> anyhow::Result { if let Some(file) = args.file { let cmd = std::env::args().next().unwrap(); args = RunArgs::parse_from( @@ -198,7 +201,7 @@ fn format_log(f: &mut Formatter, record: &Record<'_>) -> io::Result<()> { } } -pub async fn cli_entry(cli: Cli, #[cfg(test)] event_tx: mpsc::Sender<()>) -> u8 { +pub async fn cli_entry(cli: Cli, #[cfg(test)] event_tx: mpsc::Sender) -> u8 { let mut builder = env_logger::builder(); builder .filter_level(cli.verbosity.log_level_filter()) diff --git a/src/util.rs b/src/util.rs index 5ecd2a6..d56cbda 100644 --- a/src/util.rs +++ b/src/util.rs @@ -304,11 +304,9 @@ mod tests { assert_eq!( tt, - dbg!(TruthTable::new( - 0b1110, - false, - [0b0000, 0b0010, 0b0100, 0b0110, 0b1000, 0b1100, 0b1110] - )), + dbg!(TruthTable::new(0b1110, false, [ + 0b0000, 0b0010, 0b0100, 0b0110, 0b1000, 0b1100, 0b1110 + ])), ); } }