#![cfg(feature = "docker-tests")]
mod common;
mod logs;
use bollard::Docker;
use common::*;
use dfw::{
iptables::{Iptables, IptablesRuleDiscriminants},
nftables::Nftables,
process::{ContainerFilter, Process, ProcessContext, ProcessingOptions},
types::*,
util::FutureExt,
FirewallBackend,
};
use itertools::{EitherOrBoth, Itertools};
use logs::*;
use paste::paste;
use serde::de::DeserializeOwned;
use slog::{o, Drain, Fuse, Logger, OwnedKVList, Record};
use std::{
fs::File,
io::{prelude::*, BufReader},
panic::{self, AssertUnwindSafe, UnwindSafe},
process::Command,
};
static PROCESSING_OPTIONS: ProcessingOptions = ProcessingOptions {
container_filter: ContainerFilter::Running,
};
fn logger() -> Logger {
struct NoopDrain;
impl Drain for NoopDrain {
type Ok = ();
type Err = ();
fn log(&self, _record: &Record, _values: &OwnedKVList) -> Result<Self::Ok, Self::Err> {
Ok(())
}
}
let drain = Fuse(NoopDrain);
let logger = Logger::root(drain, o!());
logger
}
fn load_config_and_inject_project_name<T>(
files: &[&str],
project_name: &str,
) -> Result<T, failure::Error>
where
T: DeserializeOwned,
{
let mut contents = String::new();
for file in files {
let mut file = BufReader::new(File::open(file)?);
file.read_to_string(&mut contents)?;
}
let contents = contents.replace("PROJECT_", &format!("{}_", project_name));
Ok(toml::from_str(&contents)?)
}
fn compare_loglines(actual: &Vec<LogLine>, expected: &Vec<LogLine>) {
if actual != expected {
let width = expected
.iter()
.map(|expected| expected.command.len())
.max()
.unwrap_or_default();
println!("LogLines didn't match (expected -- actual)");
println!("---------------------");
for either in expected.iter().zip_longest(actual) {
match either {
EitherOrBoth::Both(expected, actual) => println!(
"{:<width$} {}",
expected.command,
actual.command,
width = width,
),
EitherOrBoth::Left(expected) => {
println!("{:<width$} -", expected.command, width = width)
}
EitherOrBoth::Right(actual) => {
println!("{:<width$} {}", "-", actual.command, width = width)
}
}
}
println!();
}
assert_eq!(actual, expected);
}
fn with_compose_environment<F: FnOnce() -> ()>(compose_path: &str, project_name: &str, body: F)
where
F: UnwindSafe,
{
let mut child = Command::new("docker-compose")
.args(&["--project-name", project_name])
.args(&["--file", compose_path])
.args(&["up", "-d"])
.spawn()
.expect("failed to setup Docker environment using docker-compose");
let up_exit_code = child.wait().expect("failed to wait on docker-compose");
if !up_exit_code.success() {
panic!("docker-compose did not exit successfully");
}
let panic_result = panic::catch_unwind(body);
let mut child = Command::new("docker-compose")
.args(&["--project-name", project_name])
.args(&["--file", compose_path])
.args(&["down", "--volumes"])
.args(&["--rmi", "local"])
.spawn()
.expect("failed to stop Docker environment using docker-compose");
child.wait().expect("failed to wait on docker-compose");
if let Err(err) = panic_result {
panic::resume_unwind(err);
}
}
fn test_backend<B: FirewallBackend, F: FnOnce(&DFW<B>, ProcessContext<B>) -> ()>(
path: &str,
resource_prefix: &str,
body: F,
) where
F: UnwindSafe,
DFW<B>: Process<B>,
{
let project_name = format!(
"dfwtest{}",
path.chars()
.filter(char::is_ascii_alphanumeric)
.collect::<String>(),
);
let toml: DFW<B> = load_config_and_inject_project_name(
&[
&resource(&format!("docker/{}/conf.toml", path)).unwrap(),
&resource(&format!("docker/{}/{}/conf.toml", path, resource_prefix)).unwrap(),
],
&project_name,
)
.unwrap();
let logger = logger();
let docker = Docker::connect_with_http_defaults().expect("Failed to setup Docker instance");
let ping = docker.ping().sync();
assert!(ping.is_ok());
assert!(!ping.unwrap().is_empty());
let docker = AssertUnwindSafe(docker);
let toml = AssertUnwindSafe(toml);
with_compose_environment(
&resource(&format!("docker/{}/docker-compose.yml", path)).unwrap(),
&project_name,
|| {
let dfw =
ProcessContext::new(&docker, &toml, &PROCESSING_OPTIONS, &logger, true).unwrap();
let container_name = format!("{}_a_1", project_name);
let inspect = docker.inspect_container(&container_name, None).sync();
assert!(inspect.is_ok());
let inspect = inspect.unwrap();
assert!(inspect.id.is_some());
assert!(!inspect.id.unwrap().is_empty());
body(&toml, dfw);
},
);
}
fn test_nftables(path: &str) {
test_backend(path, "nftables", |toml, dfw| {
let result = Process::<Nftables>::process(toml, &dfw);
let actual = result
.unwrap()
.unwrap()
.iter()
.map(|nft_command| LogLine {
command: nft_command.clone(),
regex: false,
eval: None,
})
.collect::<Vec<_>>();
let expected = load_loglines(
&resource(&format!("docker/{}/nftables/expected-nftables.txt", path)).unwrap(),
);
compare_loglines(&actual, &expected);
});
}
fn test_nftables_process_should_fail(path: &str) {
test_backend(path, "nftables", |toml, dfw| {
let result = Process::<Nftables>::process(toml, &dfw);
assert!(result.is_err());
});
}
fn test_iptables(path: &str) {
test_backend(path, "iptables", |toml, dfw| {
let rules = Process::<Iptables>::process(toml, &dfw).unwrap().unwrap();
let mut logs4 = Vec::new();
let rules4 = Iptables::get_rules(rules.clone(), IptablesRuleDiscriminants::V4);
for rule in rules4 {
logs4.push(LogLine {
command: rule,
regex: false,
eval: None,
});
}
let expected4 = load_loglines(
&resource(&format!(
"docker/{}/iptables/expected-iptables-v4.txt",
path
))
.unwrap(),
);
compare_loglines(&logs4, &expected4);
let mut logs6 = Vec::new();
let rules6 = Iptables::get_rules(rules, IptablesRuleDiscriminants::V6);
for rule in rules6 {
logs6.push(LogLine {
command: rule,
regex: false,
eval: None,
});
}
let expected6 = load_loglines(
&resource(&format!(
"docker/{}/iptables/expected-iptables-v6.txt",
path
))
.unwrap(),
);
compare_loglines(&logs6, &expected6);
});
}
fn test_iptables_process_should_fail(path: &str) {
test_backend(path, "iptables", |toml, dfw| {
let result = Process::<Iptables>::process(toml, &dfw);
assert!(result.is_err());
});
}
macro_rules! dfw_test {
( R F $backend:ident $name:tt $param:expr $(;)* ) => {
paste! {
#[test]
fn [<regressiontest_ $backend _ $name>]() {
[<test_ $backend _process_should_fail>](concat!("_regression-tests/", $param));
}
}
};
( R F $backend:ident $param:expr $(;)* ) => {
paste! {
#[test]
fn [<regressiontest_ $backend _ $param>]() {
[<test_ $backend _process_should_fail>](concat!("_regression-tests/", $param));
}
}
};
( R $backend:ident $name:tt $param:expr $(;)* ) => {
paste! {
#[test]
fn [<regressiontest_ $backend _ $name>]() {
[<test_ $backend>](concat!("_regression-tests/", $param));
}
}
};
( R $backend:ident $param:expr $(;)* ) => {
paste! {
#[test]
fn [<regressiontest_ $backend _ $param>]() {
[<test_ $backend>](concat!("_regression-tests/", $param));
}
}
};
( F $backend:ident $param:expr $(;)* ) => {
paste! {
#[test]
fn [<test_ $backend _ $param>]() {
[<test_ $backend _process_should_fail>]($param);
}
}
};
( $backend:ident $param:expr $(;)* ) => {
paste! {
#[test]
fn [<test_ $backend _ $param>]() {
[<test_ $backend>]($param);
}
}
};
}
macro_rules! dfw_tests {
( @internal ; $($tail:tt)* ) => {
dfw_tests!( @internal $($tail)*);
};
( @internal , $($tail:tt)* ) => {
dfw_tests!( @internal $($tail)*);
};
( @internal R F $name:tt $param:tt $($tail:tt)* ) => {
dfw_test!( R F nftables $name $param );
dfw_test!( R F iptables $name $param );
dfw_tests!( @internal $($tail)* );
};
( @internal R F $param:tt $($tail:tt)* ) => {
dfw_test!( R F nftables $param );
dfw_test!( R F iptables $param );
dfw_tests!( @internal $($tail)* );
};
( @internal R $name:tt $param:tt $($tail:tt)* ) => {
dfw_test!( R nftables $name $param );
dfw_test!( R iptables $name $param );
dfw_tests!( @internal $($tail)* );
};
( @internal R $param:tt $($tail:tt)* ) => {
dfw_test!( R nftables $param );
dfw_test!( R iptables $param );
dfw_tests!( @internal $($tail)* );
};
( @internal F $param:tt $($tail:tt)* ) => {
dfw_test!( F nftables $param );
dfw_test!( F iptables $param );
dfw_tests!( @internal $($tail)* );
};
( @internal $param:tt $($tail:tt)* ) => {
dfw_test!( nftables $param );
dfw_test!( iptables $param );
dfw_tests!( @internal $($tail)* );
};
( @internal ) => {
};
( $($tts:tt)* ) => {
dfw_tests!( @internal $($tts)* );
};
}
dfw_tests!(
"01";
"02";
"03";
"04";
"05";
"06";
"07";
"ctc-network-policies";
R F "001_gh_166_01" "001-gh-166/01";
R F "001_gh_166_02" "001-gh-166/02";
R F "001_gh_166_03" "001-gh-166/03";
R "001_gh_166_04" "001-gh-166/04";
R "002_gh_265_01" "002-gh-265/01";
R "002_gh_265_02" "002-gh-265/02";
R "002_gh_265_03" "002-gh-265/03";
);