Skip to content

Only enable bridge netfiltering when needed#48492

Merged
thaJeztah merged 1 commit intomoby:masterfrom
robmry:48375_bridge_netfiltering
Sep 16, 2024
Merged

Only enable bridge netfiltering when needed#48492
thaJeztah merged 1 commit intomoby:masterfrom
robmry:48375_bridge_netfiltering

Conversation

@robmry
Copy link
Contributor

@robmry robmry commented Sep 13, 2024

- What I did

- How I did it

Kernel module br_netfilter is loaded when the daemon starts with either iptables or ip6tables enabled. That automatically sets:

net.bridge.bridge-nf-call-arptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1

So, when:

  • docker was running happily with iptables=false, and
  • no explicit ip6tables=false, and
  • br_netfilter was not loaded

... the change in moby 27.0 to enable ip6tables by default, resulted in net.bridge.bridge-nf-call-iptables being enabled, where it wasn't before.

If the host also had a firewall with default-drop on its forward chain - that resulted in packets getting dropped between containers on a bridge network.

So, only try to load br_netfilter when it's needed - it's only needed to implement --icc=false, which only works when iptables / ip6tables are enabled.

- How to verify it

  • On a freshly booted system (or at-least without br_netfilter loaded).
  • With iptables/nftables rules set up with a default DROP for forwarding.
  • Start dockerd with "iptables":false, and "ip6tables" left as default.
  • Create an IPv4-only network, docker network create br4.
  • With 26.x:
    • Check that containers on network br4 can communicate. For example:
    • docker run --rm -d --name c1 --network br4 nginx; docker run --rm -ti --network br4 alpine wget -O- http://c1
  • Upgrade to 27.x to repro the bug or a build with this fix, re-run the nginx/wget commands.
    • Note that the fix will not work once br_netfilter has been loaded, rmmod br_netfilter or reboot to get rid of it.
  • With this fix:
    • Check that br_netfilter hasn't been loaded.
      • For example sysctl -a | grep net.bridge.bridge-nf-call.
    • Create a no-icc network...
      • For example docker network create --ipv6 -o com.docker.network.bridge.enable_icc=false iccf.
      • Check that br_netfilter has been loaded.

- Description for the changelog

Fix an issue that prevented communication between containers on an IPv4 bridge network
when running with `--iptables=false`, `--ip6tables=true` (the default), a firewall with a
DROP rule for forwarded packets on hosts where the `br_netfilter` kernel module was not
normally loaded.

@robmry robmry added this to the 28.0.0 milestone Sep 13, 2024
@robmry robmry self-assigned this Sep 13, 2024
@robmry robmry force-pushed the 48375_bridge_netfiltering branch from 09119a0 to 0677efa Compare September 13, 2024 13:40
@robmry robmry marked this pull request as ready for review September 13, 2024 14:30
Comment on lines 51 to 52
if out, err := exec.Command("modprobe", "-va", "bridge", "br_netfilter").CombinedOutput(); err != nil {
log.G(context.TODO()).WithError(err).Warnf("Running modprobe bridge br_netfilter failed with message: %s", out)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we should start looking at plumbing through context to these networking options (both for OTEL, as well as potentially context-cancellation (e.g. this could use exec.CommandContext).

Definitely not for this PR, but curious if it would make sense to start looking at.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would make sense.

if _, err := os.Stat("/proc/sys/net/bridge"); err != nil {
if out, err := exec.Command("modprobe", "-va", "bridge", "br_netfilter").CombinedOutput(); err != nil {
log.G(context.TODO()).WithError(err).Warnf("Running modprobe bridge br_netfilter failed with message: %s", out)
return fmt.Errorf("modprobe br_netfilter failed: %w", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now both returns and logs the error, whereas previously we would only log it; is there a risk that we're changing behavior?

(Also wondering if the logging should happen on the caller side now that we return these)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it's an error now; perhaps the Warnf should become an Errorf now as well 🤔 (we tend to use "warning" for errors that we can recover from, but as we now no longer recover, it's an Error I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now both returns and logs the error, whereas previously we would only log it; is there a risk that we're changing behavior?

If the modprobe failed on startup before, network creation with icc=false would have failed here, with the "cannot restrict inter-container communication" error (just below). I'll add "cannot restrict inter-container communication" to the modprobe error.

So, I don't think there's any change in behaviour, apart from the text in the error message.

(Also wondering if the logging should happen on the caller side now that we return these)?

I think that'd mean duplicating the log lines, would it be an improvement? We could just get rid of the log line, but it might still provide a useful clue for something.

As it's an error now; perhaps the Warnf should become an Errorf now as well 🤔 (we tend to use "warning" for errors that we can recover from, but as we now no longer recover, it's an Error I guess?

I'll change it to an Errorf. (If the modprobe fails, it now means an network create "icc=false" didn't work - before it meant a future network create with "icc=false" would fail if attempted. Recovery is the same as before, just reject the network creation. The rest of the daemon wouldn't be affected in either case.)

Kernel module br_netfilter is loaded when the daemon starts with
either iptables or ip6tables enabled. That automatically sets:
  net.bridge.bridge-nf-call-arptables = 1
  net.bridge.bridge-nf-call-ip6tables = 1
  net.bridge.bridge-nf-call-iptables = 1

So, when:
- docker was running happily with iptables=false, and
- no explicit ip6tables=false, and
- br_netfilter was not loaded
... the change in moby 27.0 to enable ip6tables by default, resulted
in net.bridge.bridge-nf-call-iptables being enabled.

If the host also had a firewall with default-drop on its forward
chain - that resulted in packets getting dropped between containers
on a bridge network.

So, only try to load br_netfilter when it's needed - it's only needed
to implement "--icc=false", which can only be used when iptables or
ip6tables is enabled.

Signed-off-by: Rob Murray <[email protected]>
@robmry robmry force-pushed the 48375_bridge_netfiltering branch from 0677efa to db25b0d Compare September 16, 2024 13:32
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Filtered packets between containers in bridged network after upgrade Docker bridge networks not automatically added to appropriate firewalld zone

3 participants