diff --git a/.github/workflows/it.yml b/.github/workflows/it.yml index fb0d1c18..cd756584 100644 --- a/.github/workflows/it.yml +++ b/.github/workflows/it.yml @@ -69,18 +69,6 @@ jobs: runs-on: ubuntu-20.04 - dind-image: docker:18.06-dind runs-on: ubuntu-20.04 - - dind-image: docker:18.03-dind - runs-on: ubuntu-20.04 - - dind-image: docker:17.12-dind - runs-on: ubuntu-20.04 - - dind-image: docker:17.09-dind - runs-on: ubuntu-20.04 - - dind-image: docker:17.07-dind - runs-on: ubuntu-20.04 - - dind-image: docker:17.06-dind - runs-on: ubuntu-20.04 - - dind-image: docker:1.13-dind - runs-on: ubuntu-20.04 services: dind: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1137910f..76689215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Unreleased +* Add [`same_network_verdict` option](https://dfw.rs/latest/dfw/types/struct.ContainerToContainer.html#structfield.same_network_verdict) to container-to-container configuration, enabling users to specify whether traffic between containers within the same network should be allowed or not. * Replace library used to communicate with Docker (which also fixes [#411]). This release replaces the previously used library [shiplift] by [bollard]. diff --git a/README.md b/README.md index 89639c1d..4e516a91 100644 --- a/README.md +++ b/README.md @@ -339,14 +339,8 @@ DFW is continuously and automatically tested with the following stable Docker ve * `19.03` * `18.09` * `18.06` -* `18.03` -* `17.12` -* `17.09` -* `17.07` -* `17.06` -* `1.13` - -Docker version 20.10 is also officially supported by DFW, although it is not yet automatically tested. + +Docker versions between 18.03 and 1.13 should also work, but are not automatically tested anymore due to lacking Docker BuildKit support. ## Version bump policy diff --git a/resources/test/docker/ctc-network-policies/conf.toml b/resources/test/docker/ctc-network-policies/conf.toml new file mode 100644 index 00000000..1e0f36bb --- /dev/null +++ b/resources/test/docker/ctc-network-policies/conf.toml @@ -0,0 +1,7 @@ +[container_to_container] +default_policy = "drop" +same_network_verdict = "accept" + +[[container_to_container.rules]] +network = "PROJECT_default" +verdict = "reject" diff --git a/resources/test/docker/ctc-network-policies/docker-compose.yml b/resources/test/docker/ctc-network-policies/docker-compose.yml new file mode 100644 index 00000000..72fb97aa --- /dev/null +++ b/resources/test/docker/ctc-network-policies/docker-compose.yml @@ -0,0 +1,12 @@ +version: '2' + +services: + a: + image: nginx:alpine + networks: + - default + - other + +networks: + default: + other: diff --git a/resources/test/docker/ctc-network-policies/iptables/conf.toml b/resources/test/docker/ctc-network-policies/iptables/conf.toml new file mode 100644 index 00000000..e69de29b diff --git a/resources/test/docker/ctc-network-policies/iptables/expected-iptables-v4.txt b/resources/test/docker/ctc-network-policies/iptables/expected-iptables-v4.txt new file mode 100644 index 00000000..ed5c577b --- /dev/null +++ b/resources/test/docker/ctc-network-policies/iptables/expected-iptables-v4.txt @@ -0,0 +1,32 @@ +*filter +:DFWRS_FORWARD - [0:0] +:DFWRS_INPUT - [0:0] +:FORWARD - [0:0] +:INPUT - [0:0] +-F DFWRS_FORWARD +-A DFWRS_FORWARD -m state --state INVALID -j DROP +-A DFWRS_FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT +-A DFWRS_FORWARD -i $input=bridge -o $output=bridge -j REJECT "$input" == "$output" +-A DFWRS_FORWARD -i $input=bridge -o $output=bridge -j ACCEPT "$input" == "$output" +-A DFWRS_FORWARD -i $input=bridge -o $output=bridge -j ACCEPT "$input" == "$output" +-A DFWRS_FORWARD -i $input=bridge -o $output=bridge -j ACCEPT "$input" == "$output" +-A DFWRS_FORWARD -i $input=bridge -o $output=bridge -j ACCEPT "$input" == "$output" +-A DFWRS_FORWARD -i $input=bridge -o $output=bridge -j ACCEPT "$input" == "$output" +-A DFWRS_FORWARD -j DROP +-F DFWRS_INPUT +-A DFWRS_INPUT -m state --state INVALID -j DROP +-A DFWRS_INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT +-A DFWRS_INPUT -i docker0 -j ACCEPT +-A FORWARD -j DFWRS_FORWARD +-A INPUT -j DFWRS_INPUT +COMMIT +*nat +:DFWRS_POSTROUTING - [0:0] +:DFWRS_PREROUTING - [0:0] +:POSTROUTING - [0:0] +:PREROUTING - [0:0] +-F DFWRS_POSTROUTING +-F DFWRS_PREROUTING +-A POSTROUTING -j DFWRS_POSTROUTING +-A PREROUTING -j DFWRS_PREROUTING +COMMIT diff --git a/resources/test/docker/ctc-network-policies/iptables/expected-iptables-v6.txt b/resources/test/docker/ctc-network-policies/iptables/expected-iptables-v6.txt new file mode 100644 index 00000000..f369ad52 --- /dev/null +++ b/resources/test/docker/ctc-network-policies/iptables/expected-iptables-v6.txt @@ -0,0 +1,16 @@ +*filter +:DFWRS_FORWARD - [0:0] +:DFWRS_INPUT - [0:0] +-F DFWRS_FORWARD +-A DFWRS_FORWARD -m state --state INVALID -j DROP +-A DFWRS_FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT +-F DFWRS_INPUT +-A DFWRS_INPUT -m state --state INVALID -j DROP +-A DFWRS_INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT +COMMIT +*nat +:DFWRS_POSTROUTING - [0:0] +:DFWRS_PREROUTING - [0:0] +-F DFWRS_POSTROUTING +-F DFWRS_PREROUTING +COMMIT \ No newline at end of file diff --git a/resources/test/docker/ctc-network-policies/nftables/conf.toml b/resources/test/docker/ctc-network-policies/nftables/conf.toml new file mode 100644 index 00000000..e69de29b diff --git a/resources/test/docker/ctc-network-policies/nftables/expected-nftables.txt b/resources/test/docker/ctc-network-policies/nftables/expected-nftables.txt new file mode 100644 index 00000000..1a56213d --- /dev/null +++ b/resources/test/docker/ctc-network-policies/nftables/expected-nftables.txt @@ -0,0 +1,24 @@ +add table inet dfw +flush table inet dfw +add chain inet dfw input { type filter hook input priority -5 ; } +add rule inet dfw input ct state invalid drop +add rule inet dfw input ct state { related, established } accept +add chain inet dfw forward { type filter hook forward priority -5 ; } +add rule inet dfw forward ct state invalid drop +add rule inet dfw forward ct state { related, established } accept +add table ip dfw +flush table ip dfw +add chain ip dfw prerouting { type nat hook prerouting priority -105 ; } +add chain ip dfw postrouting { type nat hook postrouting priority 95 ; } +add table ip6 dfw +flush table ip6 dfw +add chain ip6 dfw prerouting { type nat hook prerouting priority -105 ; } +add chain ip6 dfw postrouting { type nat hook postrouting priority 95 ; } +add rule inet dfw input meta iifname docker0 meta mark set 0xdf accept +add chain inet dfw forward { policy drop ; } +add rule inet dfw forward meta iifname $input=bridge oifname $output=bridge meta mark set 0xdf reject "$input" == "$output" +add rule inet dfw forward meta iifname $input=bridge oifname $output=bridge meta mark set 0xdf accept "$input" == "$output" +add rule inet dfw forward meta iifname $input=bridge oifname $output=bridge meta mark set 0xdf accept "$input" == "$output" +add rule inet dfw forward meta iifname $input=bridge oifname $output=bridge meta mark set 0xdf accept "$input" == "$output" +add rule inet dfw forward meta iifname $input=bridge oifname $output=bridge meta mark set 0xdf accept "$input" == "$output" +add rule inet dfw forward meta iifname $input=bridge oifname $output=bridge meta mark set 0xdf accept "$input" == "$output" diff --git a/src/iptables/process.rs b/src/iptables/process.rs index 9b99d7fd..e10263d4 100644 --- a/src/iptables/process.rs +++ b/src/iptables/process.rs @@ -252,6 +252,30 @@ impl Process for ContainerToContainer { rules.append(&mut ctc_rules); } + if let Some(same_network_verdict) = self.same_network_verdict { + for network in ctx.network_map.values() { + let network_id = network.id.as_ref().expect("Docker network ID missing"); + let bridge_name = get_bridge_name(network_id)?; + trace!(ctx.logger, "Got bridge name"; + o!("network_name" => &network.name, + "bridge_name" => &bridge_name)); + + let ipt_rule = Rule::new("filter", DFW_FORWARD_CHAIN) + .in_interface(&bridge_name) + .out_interface(&bridge_name) + .jump(&same_network_verdict.to_string().to_uppercase()) + .build()?; + + debug!(ctx.logger, "Add forward rule for same network verdict for bridge"; + o!("part" => "container_to_container", + "bridge_name" => bridge_name, + "same_network_verdict" => same_network_verdict, + "rule" => &ipt_rule)); + + rules.push(append_built_rule(IptablesRuleDiscriminants::V4, &ipt_rule)); + } + } + // Enforce default policy for container-to-container communication. rules.push(append_rule( IptablesRuleDiscriminants::V4, diff --git a/src/nftables/process.rs b/src/nftables/process.rs index f9a5564e..3da0fd9b 100644 --- a/src/nftables/process.rs +++ b/src/nftables/process.rs @@ -330,6 +330,30 @@ impl Process for ContainerToContainer { rules.append(&mut ctc_rules); } + if let Some(same_network_verdict) = self.same_network_verdict { + for network in ctx.network_map.values() { + let network_id = network.id.as_ref().expect("Docker network ID missing"); + let bridge_name = get_bridge_name(network_id)?; + trace!(ctx.logger, "Got bridge name"; + o!("network_name" => &network.name, + "bridge_name" => &bridge_name)); + + let rule = RuleBuilder::default() + .in_interface(&bridge_name) + .out_interface(&bridge_name) + .verdict(same_network_verdict) + .build()?; + + debug!(ctx.logger, "Add forward rule for same network verdict for bridge"; + o!("part" => "container_to_container", + "bridge_name" => bridge_name, + "same_network_verdict" => same_network_verdict, + "rule" => &rule)); + + rules.push(add_rule(Family::Inet, "dfw", "forward", &rule)); + } + } + Ok(Some(rules)) } } diff --git a/src/process.rs b/src/process.rs index 57641c88..8d5f1875 100644 --- a/src/process.rs +++ b/src/process.rs @@ -164,7 +164,7 @@ where .cloned(); let primary_external_network_interface = external_network_interfaces .as_ref() - .and_then(|v| v.get(0)) + .and_then(|v| v.first()) .map(|s| s.to_owned()); Ok(ProcessContext { diff --git a/src/types.rs b/src/types.rs index 752fa43b..90f4983b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -192,6 +192,69 @@ pub struct ContainerToContainer { /// /// To permanently set this configuration, take a look at `man sysctl.d` and `man sysctl.conf`. pub default_policy: ChainPolicy, + /// Configure whether traffic between containers within the same network should be allowed or + /// not. + /// + /// This option is more specific than [`default_policy`], applying to traffic between containers + /// on the same network, rather than also applying across networks. This option has precedence + /// over the [`default_policy`] for traffic on the same network. + /// + /// ## Example + /// + /// Setting the `same_network_verdict` to `accept` will allow all traffic between containers on + /// the same network to pass, regardless of the [`default_policy`] configured: + /// + /// ``` + /// # use dfw::nftables::Nftables; + /// # use dfw::types::*; + /// # use toml; + /// # toml::from_str::>(r#" + /// [container_to_container] + /// default_policy = "drop" + /// same_network_verdict = "accept" + /// # "#).unwrap(); + /// ``` + /// + /// If you want to allow traffic between containers on the same networks in general, but want to + /// restrict traffic on some networks, you can additionally add [container-to-container rules] + /// to disallow traffic between containers for the desired networks: + /// + /// ``` + /// # use dfw::nftables::Nftables; + /// # use dfw::types::*; + /// # use toml; + /// # toml::from_str::>(r#" + /// [container_to_container] + /// default_policy = "drop" + /// same_network_verdict = "accept" + /// + /// [[container_to_container.rules]] + /// network = "restricted_network" + /// verdict = "reject" + /// # "#).unwrap(); + /// ``` + /// + /// [container-to-container rules]: struct.ContainerToContainerRule.html + /// + /// ## Host configuration + /// + /// Depending on how your host is configured, traffic whose origin and destination interface are + /// the same bridge (network) is _not_ filtered by the kernel netfilter module. + /// + /// This means that this verdict is only honored if your kernel has the `br_netfilter` + /// kernel-module available and the sysctl `net.bridge.bridge-nf-call-iptables` is set to `1`. + /// Otherwise traffic between containers on the same network will always be allowed. + /// + /// You can set the sysctl-value temporarily like this: + /// + /// ```text + /// sysctl net.bridge.bridge-nf-call-iptables=1 + /// ``` + /// + /// To permanently set this configuration, take a look at `man sysctl.d` and `man sysctl.conf`. + /// + /// [`default_policy`]: #structfield.default_policy + pub same_network_verdict: Option, /// An optional list of rules, see /// [`ContainerToContainerRule`](struct.ContainerToContainerRule.html). /// diff --git a/tests/dfw.rs b/tests/dfw.rs index 27a94a17..01f3181f 100644 --- a/tests/dfw.rs +++ b/tests/dfw.rs @@ -399,6 +399,7 @@ dfw_tests!( "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"; diff --git a/tests/types.rs b/tests/types.rs index 8d646f52..c1a326ce 100644 --- a/tests/types.rs +++ b/tests/types.rs @@ -58,6 +58,7 @@ fn parse_conf_file() { }; 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()), @@ -161,6 +162,7 @@ fn parse_conf_path() { }; 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()), @@ -594,7 +596,7 @@ fn ensure_backwards_compatibility_v1() { source_cidr_v4, source_cidr_v6, .. - } = rules.get(0).unwrap(); + } = rules.first().unwrap(); assert!(source_cidr_v4.is_some()); assert!(source_cidr_v6.is_none()); }