// Copyright Pit Kleyersburg // SPDX-License-Identifier: MIT OR Apache-2.0 // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified or distributed // except according to those terms. mod common; use common::resource; use dfw::{ process::{Process, ProcessContext}, types::*, util::*, FirewallBackend, }; use serde::Deserialize; #[derive(Debug, Eq, PartialEq)] struct TestBackend; impl FirewallBackend for TestBackend where DFW: Process, { type Rule = String; type Defaults = TestBackendDefaults; fn apply(_rules: Vec, _ctx: &ProcessContext) -> dfw::errors::Result<()> { unimplemented!() } } impl Process for DFW { fn process( &self, _ctx: &ProcessContext, ) -> Result>, failure::Error> { unimplemented!() } } #[derive(Debug, Deserialize, Eq, PartialEq)] struct TestBackendDefaults { test: String, } #[test] fn parse_conf_file() { let global_defaults = GlobalDefaults { external_network_interfaces: Some(vec!["eni".to_owned()]), default_docker_bridge_to_host_policy: ChainPolicy::Accept, ..Default::default() }; let backend_defaults = TestBackendDefaults { test: "custom backend defaults".to_owned(), }; let container_to_container = ContainerToContainer { default_policy: ChainPolicy::Drop, same_network_verdict: None, rules: Some(vec![ContainerToContainerRule { network: "network".to_owned(), src_container: Some("src_container".to_owned()), dst_container: Some("dst_container".to_owned()), matches: Some("FILTER".to_owned()), verdict: RuleVerdict::Accept, }]), }; let container_to_wider_world = ContainerToWiderWorld { default_policy: RuleVerdict::Accept, rules: Some(vec![ContainerToWiderWorldRule { network: Some("network".to_owned()), src_container: Some("src_container".to_owned()), matches: Some("FILTER".to_owned()), verdict: RuleVerdict::Accept, external_network_interface: Some("eni".to_owned()), }]), }; let container_to_host = ContainerToHost { default_policy: RuleVerdict::Accept, rules: Some(vec![ContainerToHostRule { network: "network".to_owned(), src_container: Some("src_container".to_owned()), matches: Some("FILTER".to_owned()), verdict: RuleVerdict::Accept, }]), }; let wider_world_to_container = WiderWorldToContainer { rules: Some(vec![ WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ExposePort { host_port: 80, container_port: None, family: "tcp".to_owned(), }], external_network_interface: Some("eni".to_owned()), expose_via_ipv6: false, source_cidr_v4: None, source_cidr_v6: None, }, WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ExposePort { host_port: 22, container_port: None, family: "tcp".to_owned(), }], external_network_interface: Some("eni".to_owned()), expose_via_ipv6: true, source_cidr_v4: Some(vec!["192.0.2.1/32".to_owned(), "192.0.2.2/32".to_owned()]), source_cidr_v6: Some(vec![ "2001:db8::1/128".to_owned(), "2001:db8::2/128".to_owned(), ]), }, ]), }; let container_dnat = ContainerDNAT { rules: Some(vec![ContainerDNATRule { src_network: Some("src_network".to_owned()), src_container: Some("src_container".to_owned()), dst_network: "dst_network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ExposePort { host_port: 80, container_port: None, family: "tcp".to_owned(), }], }]), }; #[cfg_attr(crate_major_version = "1", allow(deprecated))] let expected: DFW = DFW { global_defaults, backend_defaults: Some(backend_defaults), initialization: None, container_to_container: Some(container_to_container), container_to_wider_world: Some(container_to_wider_world), container_to_host: Some(container_to_host), wider_world_to_container: Some(wider_world_to_container), container_dnat: Some(container_dnat), }; let actual = load_file(&resource("conf-file.toml").unwrap()).unwrap(); assert_eq!(expected, actual); } #[test] fn parse_conf_path() { let global_defaults = GlobalDefaults { external_network_interfaces: Some(vec!["eni".to_owned()]), default_docker_bridge_to_host_policy: ChainPolicy::Accept, ..Default::default() }; let backend_defaults = TestBackendDefaults { test: "custom backend defaults".to_owned(), }; let container_to_container = ContainerToContainer { default_policy: ChainPolicy::Drop, same_network_verdict: None, rules: Some(vec![ContainerToContainerRule { network: "network".to_owned(), src_container: Some("src_container".to_owned()), dst_container: Some("dst_container".to_owned()), matches: Some("FILTER".to_owned()), verdict: RuleVerdict::Accept, }]), }; let container_to_wider_world = ContainerToWiderWorld { default_policy: RuleVerdict::Accept, rules: Some(vec![ContainerToWiderWorldRule { network: Some("network".to_owned()), src_container: Some("src_container".to_owned()), matches: Some("FILTER".to_owned()), verdict: RuleVerdict::Accept, external_network_interface: Some("eni".to_owned()), }]), }; let container_to_host = ContainerToHost { default_policy: RuleVerdict::Accept, rules: Some(vec![ContainerToHostRule { network: "network".to_owned(), src_container: Some("src_container".to_owned()), matches: Some("FILTER".to_owned()), verdict: RuleVerdict::Accept, }]), }; let wider_world_to_container = WiderWorldToContainer { rules: Some(vec![ WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ExposePort { host_port: 80, container_port: None, family: "tcp".to_owned(), }], external_network_interface: Some("eni".to_owned()), expose_via_ipv6: false, source_cidr_v4: None, source_cidr_v6: None, }, WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ExposePort { host_port: 22, container_port: None, family: "tcp".to_owned(), }], external_network_interface: Some("eni".to_owned()), expose_via_ipv6: true, source_cidr_v4: Some(vec!["192.0.2.1/32".to_owned(), "192.0.2.2/32".to_owned()]), source_cidr_v6: Some(vec![ "2001:db8::1/128".to_owned(), "2001:db8::2/128".to_owned(), ]), }, ]), }; let container_dnat = ContainerDNAT { rules: Some(vec![ContainerDNATRule { src_network: Some("src_network".to_owned()), src_container: Some("src_container".to_owned()), dst_network: "dst_network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ExposePort { host_port: 80, container_port: None, family: "tcp".to_owned(), }], }]), }; #[cfg_attr(crate_major_version = "1", allow(deprecated))] let expected: DFW = DFW { global_defaults, backend_defaults: Some(backend_defaults), initialization: None, container_to_container: Some(container_to_container), container_to_wider_world: Some(container_to_wider_world), container_to_host: Some(container_to_host), wider_world_to_container: Some(wider_world_to_container), container_dnat: Some(container_dnat), }; let actual = load_path(&resource("conf_path").unwrap()).unwrap(); assert_eq!(expected, actual); } #[test] fn parse_expose_port_single_int() { let fragment = r#" network = "network" dst_container = "dst_container" expose_port = 80 "#; let expected = WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ExposePort { host_port: 80, container_port: None, family: "tcp".to_owned(), }], external_network_interface: None, expose_via_ipv6: true, source_cidr_v4: None, source_cidr_v6: None, }; let actual: WiderWorldToContainerRule = toml::from_str(fragment).unwrap(); assert_eq!(expected, actual); } #[test] fn parse_expose_port_seq_int() { let fragment = r#" network = "network" dst_container = "dst_container" expose_port = [80, 81] "#; let expected = WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ ExposePort { host_port: 80, container_port: None, family: "tcp".to_owned(), }, ExposePort { host_port: 81, container_port: None, family: "tcp".to_owned(), }, ], external_network_interface: None, expose_via_ipv6: true, source_cidr_v4: None, source_cidr_v6: None, }; let actual: WiderWorldToContainerRule = toml::from_str(fragment).unwrap(); assert_eq!(expected, actual); } #[test] fn parse_expose_port_single_string() { for &(port, family) in &[(80, "tcp"), (53, "udp"), (1234, "other")] { let fragment = format!( r#" network = "network" dst_container = "dst_container" expose_port = "{}/{}" "#, port, family ); let expected = WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ExposePort { host_port: port.to_owned(), container_port: None, family: family.to_owned(), }], external_network_interface: None, expose_via_ipv6: true, source_cidr_v4: None, source_cidr_v6: None, }; let actual: WiderWorldToContainerRule = toml::from_str(&fragment).unwrap(); assert_eq!(expected, actual); } } #[test] fn parse_expose_port_seq_string() { let fragment = r#" network = "network" dst_container = "dst_container" expose_port = ["80/tcp", "53/udp", "1234/other"] "#; let expected = WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ ExposePort { host_port: 80, container_port: None, family: "tcp".to_owned(), }, ExposePort { host_port: 53, container_port: None, family: "udp".to_owned(), }, ExposePort { host_port: 1234, container_port: None, family: "other".to_owned(), }, ], external_network_interface: None, expose_via_ipv6: true, source_cidr_v4: None, source_cidr_v6: None, }; let actual: WiderWorldToContainerRule = toml::from_str(fragment).unwrap(); assert_eq!(expected, actual); } #[test] fn parse_expose_port_single_struct() { for port in &[ "{ host_port = 80 }", r#"{ host_port = 80, family = "tcp" }"#, ] { let fragment = format!( r#" network = "network" dst_container = "dst_container" expose_port = {} "#, port ); let expected = WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ExposePort { host_port: 80, container_port: None, family: "tcp".to_owned(), }], external_network_interface: None, expose_via_ipv6: true, source_cidr_v4: None, source_cidr_v6: None, }; let actual: WiderWorldToContainerRule = toml::from_str(&fragment).unwrap(); assert_eq!(expected, actual); } } #[test] fn parse_expose_port_seq_struct() { let fragment = r#" network = "network" dst_container = "dst_container" expose_port = [ { host_port = 80 }, { host_port = 8080, container_port = 80 }, { host_port = 8081, container_port = 81, family = "udp" }, { host_port = 8082, container_port = 82, family = "other" }, ] "#; let expected = WiderWorldToContainerRule { network: "network".to_owned(), dst_container: "dst_container".to_owned(), expose_port: vec![ ExposePort { host_port: 80, container_port: None, family: "tcp".to_owned(), }, ExposePort { host_port: 8080, container_port: Some(80), family: "tcp".to_owned(), }, ExposePort { host_port: 8081, container_port: Some(81), family: "udp".to_owned(), }, ExposePort { host_port: 8082, container_port: Some(82), family: "other".to_owned(), }, ], external_network_interface: None, expose_via_ipv6: true, source_cidr_v4: None, source_cidr_v6: None, }; let actual: WiderWorldToContainerRule = toml::from_str(fragment).unwrap(); assert_eq!(expected, actual); } #[test] #[should_panic(expected = "port string has invalid format")] fn parse_expose_port_string_invalid_format() { let fragment = r#" network = "network" dst_container = "dst_container" expose_port = "80/tcp/what" "#; toml::from_str::(fragment).unwrap(); } #[test] #[should_panic(expected = "invalid digit found in string")] fn parse_expose_port_string_invalid_int() { let fragment = r#" network = "network" dst_container = "dst_container" expose_port = "noint" "#; toml::from_str::(fragment).unwrap(); } #[test] #[should_panic(expected = "invalid digit found in string")] fn parse_expose_port_string_invalid_int2() { let fragment = r#" network = "network" dst_container = "dst_container" expose_port = "noint/tcp" "#; toml::from_str::(fragment).unwrap(); } #[test] fn parse_external_network_interfaces_single() { let fragment = r#"external_network_interfaces = "eni""#; let expected = GlobalDefaults { external_network_interfaces: Some(vec!["eni".to_owned()]), default_docker_bridge_to_host_policy: ChainPolicy::Accept, ..Default::default() }; let actual: GlobalDefaults = toml::from_str(fragment).unwrap(); assert_eq!(expected, actual); } #[test] fn parse_external_network_interfaces_seq() { let fragment = r#"external_network_interfaces = ["eni1", "eni2"]"#; let expected = GlobalDefaults { external_network_interfaces: Some(vec!["eni1".to_owned(), "eni2".to_owned()]), default_docker_bridge_to_host_policy: ChainPolicy::Accept, ..Default::default() }; let actual: GlobalDefaults = toml::from_str(fragment).unwrap(); assert_eq!(expected, actual); } /// These tests verify that certain features or configuration fields are available within the same /// major version. Additionally these tests are configured to fail if the major version is bumped, /// which ensures that we will be reminded to remove them and the deprecated items. #[cfg_attr(crate_major_version = "1", allow(deprecated))] #[cfg_attr(not(crate_major_version = "1"), should_panic)] #[test] fn ensure_backwards_compatibility_v1() { // `defaults` needs to be valid in addition to `global_defaults` toml::from_str::>( r#" [defaults] default_docker_bridge_to_host_policy = "accept" "#, ) .unwrap(); // The old `initialization` key needs to follow the nftables-spec no matter what. toml::from_str::>( r#" [initialization] rules = [ "custom rule 1", "custom rule 2", ] "#, ) .unwrap(); // `action` needs to be valid in addition to `verdict`. toml::from_str::>( r#" [container_to_container] default_policy = "accept" [[container_to_container.rules]] network = "" action = "accept" [container_to_wider_world] default_policy = "accept" [[container_to_wider_world.rules]] action = "accept" [container_to_host] default_policy = "accept" [[container_to_host.rules]] network = "" action = "accept" "#, ) .unwrap(); // `source_cidr` needs to be valid in addition to `source_cidr_v4`. { let dfw = toml::from_str::>( r#" [[wider_world_to_container.rules]] network = "" dst_container = "" expose_port = 0 source_cidr = "127.0.0.0/8" "#, ) .unwrap(); let rules = dfw.wider_world_to_container.unwrap().rules.unwrap(); let WiderWorldToContainerRule { source_cidr_v4, source_cidr_v6, .. } = rules.first().unwrap(); assert!(source_cidr_v4.is_some()); assert!(source_cidr_v6.is_none()); } // GlobalDefaults::custom_tables needs to be present. let _ = GlobalDefaults { custom_tables: None, ..Default::default() }; }