diff --git a/Cargo.lock b/Cargo.lock index 3bbde48..673e14b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -72,6 +72,15 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "async-tempfile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb90d9834a8015109afc79f1f548223a0614edcbab62fb35b62d4b707e975e7" +dependencies = [ + "tokio", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -247,6 +256,7 @@ version = "0.2.0" dependencies = [ "anstyle", "anyhow", + "async-tempfile", "cfg_aliases", "clap", "clap-verbosity-flag", diff --git a/Cargo.toml b/Cargo.toml index ad0ba64..e257eae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "flow" version = "0.2.0" edition = "2021" +default-run = "flow" [[bin]] name = "DONTSHIPIT-flow-gen" @@ -17,6 +18,7 @@ clap = { version = "4.5.20", features = ["derive"] } [dependencies] anstyle = "1.0.8" anyhow = "1.0.86" +async-tempfile = "0.6.0" clap = { workspace = true } clap-verbosity-flag = "3.0.0" clap_complete = "4.5.38" @@ -51,6 +53,7 @@ tokio = { version = "1.38.0", features = [ "sync", "macros", "io-util", + "io-std", "signal", ] } tokio-util = "0.7.11" diff --git a/src/integration_tests/config/basic.bird.conf b/src/integration_tests/config/basic.bird.conf new file mode 100644 index 0000000..12335e6 --- /dev/null +++ b/src/integration_tests/config/basic.bird.conf @@ -0,0 +1,43 @@ +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, 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; + + local as 65000; + neighbor ::1 port 1179 as 65000; + multihop; + + ipv4 { import none; export all; }; + ipv6 { import none; export all; }; + flow4 { table myflow4; import none; export all; }; + flow6 { table myflow6; import none; export all; }; +} diff --git a/src/integration_tests/helpers/bird.rs b/src/integration_tests/helpers/bird.rs index 444e478..a0b5e5e 100644 --- a/src/integration_tests/helpers/bird.rs +++ b/src/integration_tests/helpers/bird.rs @@ -1,9 +1,12 @@ use anyhow::bail; +use async_tempfile::{TempDir, TempFile}; use std::borrow::Cow; use std::ffi::OsStr; -use std::process::{Command, Stdio}; +use std::process::Stdio; use std::sync::LazyLock; use std::{env, io}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::process::{Child, Command}; use version_compare::compare_to; static BIRD_PATH: LazyLock<Cow<'static, OsStr>> = LazyLock::new(|| { @@ -13,7 +16,10 @@ static BIRD_PATH: LazyLock<Cow<'static, OsStr>> = LazyLock::new(|| { }); static BIRD_VERSION: LazyLock<Result<Option<String>, String>> = LazyLock::new(|| { - let output = Command::new(&*BIRD_PATH).arg("--version").stdin(Stdio::null()).output(); + let output = std::process::Command::new(&*BIRD_PATH) + .arg("--version") + .stdin(Stdio::null()) + .output(); let mut stderr = match output { Ok(output) => output.stderr, Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), @@ -111,3 +117,33 @@ fn check_bird_2_16() -> anyhow::Result<()> { for more information.", ) } + +pub async fn run_bird(config: &str) -> anyhow::Result<(Child, (TempFile, TempDir))> { + let mut config_file = TempFile::new().await?; + config_file.write_all(config.as_bytes()).await?; + config_file.flush().await?; + + let sock_dir = TempDir::new().await?; + let sock_path = sock_dir.join("bird.sock"); + + let mut bird = Command::new(&*BIRD_PATH) + .arg("-d") + .args(["-c".as_ref(), config_file.file_path().as_os_str()]) + .args(["-s".as_ref(), sock_path.as_os_str()]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + let mut bird_stderr = BufReader::new(bird.stderr.take().unwrap()); + tokio::spawn(async move { + let mut buf = String::new(); + while bird_stderr.read_line(&mut buf).await? != 0 { + eprint!("{buf}"); + buf.clear(); + } + anyhow::Ok(()) + }); + + Ok((bird, (config_file, sock_dir))) +} diff --git a/src/integration_tests/helpers/cli.rs b/src/integration_tests/helpers/cli.rs new file mode 100644 index 0000000..9a57d67 --- /dev/null +++ b/src/integration_tests/helpers/cli.rs @@ -0,0 +1,20 @@ +use crate::args::Cli; +use crate::cli_entry; +use std::future::Future; +use std::process::ExitCode; +use tokio::task::{JoinHandle, LocalSet}; + +pub type CliChild = JoinHandle<anyhow::Result<ExitCode>>; + +pub async fn run_cli<F, Fut, R>(options: Cli, f: F) -> anyhow::Result<R> +where + F: FnOnce(CliChild, &LocalSet) -> Fut, + Fut: Future<Output = anyhow::Result<R>>, +{ + let ls = LocalSet::new(); + let cli = ls.spawn_local(async { + let exit_code = cli_entry(options).await; + anyhow::Ok(exit_code) + }); + ls.run_until(f(cli, &ls)).await +} diff --git a/src/integration_tests/helpers/kernel.rs b/src/integration_tests/helpers/kernel.rs index 35e711e..6c27c3d 100644 --- a/src/integration_tests/helpers/kernel.rs +++ b/src/integration_tests/helpers/kernel.rs @@ -1,6 +1,26 @@ use anyhow::bail; use nix::unistd::Uid; +#[cfg(rtnetlink_supported)] +pub async fn ensure_loopback_up() -> anyhow::Result<()> { + use rtnetlink::{LinkMessageBuilder, LinkUnspec}; + + if !Uid::effective().is_root() { + return Ok(()); + } + let (conn, handle, _) = rtnetlink::new_connection()?; + tokio::spawn(conn); + handle + .link() + .set(LinkMessageBuilder::<LinkUnspec>::new().index(1).up().build()) + .execute() + .await?; + Ok(()) +} + +#[cfg(not(rtnetlink_supported))] +pub async fn ensure_loopback_up() -> anyhow::Result<()> {} + #[test] fn check_root() -> anyhow::Result<()> { if !Uid::effective().is_root() { diff --git a/src/integration_tests/helpers/mod.rs b/src/integration_tests/helpers/mod.rs index 3f05bbb..dda4cce 100644 --- a/src/integration_tests/helpers/mod.rs +++ b/src/integration_tests/helpers/mod.rs @@ -1,2 +1,3 @@ pub mod bird; +pub mod cli; pub mod kernel; diff --git a/src/integration_tests/mod.rs b/src/integration_tests/mod.rs index 1c47e85..6d92f77 100644 --- a/src/integration_tests/mod.rs +++ b/src/integration_tests/mod.rs @@ -2,3 +2,36 @@ //! tests. mod helpers; + +use crate::args::Cli; +use clap::Parser; +use helpers::bird::{ensure_bird_ver_ge, run_bird}; +use helpers::cli::run_cli; +use helpers::kernel::ensure_loopback_up; +use std::ffi::OsString; +use std::time::Duration; +use tokio::time::sleep; + +#[tokio::test] +async fn test_basic() -> anyhow::Result<()> { + ensure_bird_ver_ge!("2"); + ensure_loopback_up().await?; + + let (mut bird, (_f, temp_dir)) = run_bird(include_str!("config/basic.bird.conf")).await?; + let temp_dir_path = temp_dir.as_os_str().into(); + let cli_opt = Cli::try_parse_from( + ["flow", "run", "-v", "--dry-run", "--bind=[::1]:1179", "--run-dir"] + .into_iter() + .map(OsString::from) + .chain(Some(temp_dir_path)), + )?; + + // TODO: implement events when cfg(test) in CLI + let fut = run_cli(cli_opt, |_cli, _ls| async { + sleep(Duration::from_secs(7)).await; + bird.kill().await?; + Ok(()) + }); + + fut.await +} diff --git a/src/main.rs b/src/main.rs index e9d762e..f768e5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,6 @@ use args::{Cli, Command, RunArgs, ShowArgs}; use bgp::{Session, StateView}; use clap::Parser; use env_logger::fmt::Formatter; -use futures::future::select; -use futures::FutureExt; use ipc::{get_sock_path, IpcServer}; use itertools::Itertools; use log::{error, info, warn, Level, LevelFilter, Record}; @@ -28,10 +26,19 @@ use std::path::Path; use std::process::ExitCode; use tokio::io::BufReader; use tokio::net::TcpListener; -use tokio::signal::unix::{signal, SignalKind}; -use tokio::{pin, select}; +use tokio::select; use util::{BOLD, FG_GREEN_BOLD, RESET}; +#[cfg(test)] +use std::future::pending; + +#[cfg(not(test))] +use { + futures::future::{select, FutureExt}, + tokio::pin, + tokio::signal::unix::{signal, SignalKind}, +}; + async fn run(mut args: RunArgs, sock_path: &str) -> anyhow::Result<ExitCode> { if let Some(file) = args.file { let cmd = std::env::args().next().unwrap(); @@ -63,15 +70,23 @@ async fn run(mut args: RunArgs, sock_path: &str) -> anyhow::Result<ExitCode> { info!("Flow listening to {bind:?} as AS{local_as}, router ID {router_id}"); - let mut sigint = signal(SignalKind::interrupt()).context("failed to register signal handler")?; - let mut sigterm = signal(SignalKind::terminate()).context("failed to register signal handler")?; + #[cfg(not(test))] + let (mut sigint, mut sigterm) = ( + signal(SignalKind::interrupt()).context("failed to register signal handler")?, + signal(SignalKind::terminate()).context("failed to register signal handler")?, + ); loop { let select = async { + #[cfg(not(test))] pin! { let sigint = sigint.recv().map(|_| "SIGINT"); let sigterm = sigterm.recv().map(|_| "SIGTERM"); + let signal_select = select(sigint, sigterm); } + #[cfg(test)] + let signal_select = pending(); + select! { result = listener.accept(), if matches!(bgp.state(), bgp::State::Active) => { let (stream, mut addr) = result.context("failed to accept TCP connection")?; @@ -88,10 +103,14 @@ async fn run(mut args: RunArgs, sock_path: &str) -> anyhow::Result<ExitCode> { let mut stream = result.context("failed to accept IPC connection")?; bgp.write_states(&mut stream).await.context("failed to write to IPC channel")?; }, - signal = select(sigint, sigterm) => { - let (signal, _) = signal.factor_first(); - warn!("{signal} received, exiting"); - return Ok(Some(ExitCode::SUCCESS)) + + _signal = signal_select => { + #[cfg(not(test))] + { + let (signal, _) = _signal.factor_first(); + warn!("{signal} received, exiting"); + return Ok(Some(ExitCode::SUCCESS)) + } } } anyhow::Ok(None) @@ -176,11 +195,14 @@ fn format_log(f: &mut Formatter, record: &Record<'_>) -> io::Result<()> { pub async fn cli_entry(cli: Cli) -> ExitCode { let sock_path = get_sock_path(&cli.run_dir).unwrap(); - env_logger::builder() + let mut builder = env_logger::builder(); + builder .filter_level(cli.verbosity.log_level_filter()) .format(format_log) - .filter_module("netlink", LevelFilter::Off) - .init(); + .filter_module("netlink", LevelFilter::Off); + #[cfg(test)] + builder.is_test(true); + builder.init(); match cli.command { Command::Run(args) => match run(args, &sock_path).await { Ok(x) => x,