Skip to content

bridge: protect bridge subnet from direct external access in raw PREROUTING#52224

Open
agners wants to merge 6 commits intomoby:masterfrom
agners:add-bridge-network-protection
Open

bridge: protect bridge subnet from direct external access in raw PREROUTING#52224
agners wants to merge 6 commits intomoby:masterfrom
agners:add-bridge-network-protection

Conversation

@agners
Copy link
Copy Markdown

@agners agners commented Mar 25, 2026

- What I did

Added a per-network rule to the raw PREROUTING chain that drops packets destined to any address in the bridge subnet from interfaces other than the bridge itself, loopback, or configured trusted host interfaces. This protects both containers on unpublished ports and the bridge's own gateway address (the host-side bridge IP, e.g. 172.17.0.1 on the default docker0 bridge) from external hosts that have a direct route to the bridge subnet.

- How I did it

Replaced the previous per-container direct access filtering rules (filterDirectAccess in endpoint.go) with a single subnet-level rule created at network setup time.

For iptables, the following rules are added per bridge (plus one ACCEPT per trusted interface):

-t raw -A PREROUTING -d <subnet> -i lo -j ACCEPT
-t raw -A PREROUTING -d <subnet> ! -i <bridge> -j DROP

For nftables, a single compound rule:

<family> daddr <subnet> iifname != { <bridge>, lo, ... } counter drop

Loopback is always permitted because the gateway IP is a valid host address reachable from local processes via lo. The rules are not added for gateway modes routed or nat-unprotected, nor when --allow-direct-routing is set, consistent with the intent of those options. DOCKER_INSECURE_NO_IPTABLES_RAW=1 disables the iptables rules.

Related upstream work:

This PR further consolidates from per-container to per-subnet, and extends coverage to the bridge gateway address.

- How to verify it

  1. Create a bridge network: docker network create --subnet 172.17.0.0/16 testnet
  2. Verify the raw PREROUTING rules are present: iptables -t raw -L PREROUTING -n -v
  3. Confirm loopback traffic to the gateway IP (172.17.0.1) still works from the host.
  4. From a neighbor host with a route to 172.17.0.0/16, verify that direct connections to addresses in the subnet are dropped.
  5. Verify rules are cleaned up on docker network rm testnet.
  6. Verify no rules are added when using --gateway-mode=routed or --allow-direct-routing.

- Human readable description for the release notes

Bridge networks now drop packets from external interfaces destined to any address in the bridge subnet using a single network-level raw PREROUTING rule, protecting both containers and the bridge gateway address from direct external access.

- A picture of a cute animal (not mandatory but encouraged)

Phidippus audax male

Opoterser, CC BY-SA 3.0, via Wikimedia Commons

agners added a commit to home-assistant/operating-system that referenced this pull request Mar 25, 2026
This adds two patches with fixes/improvements for the Docker engine

- `0001-daemon-respect-explicit-AppArmor-profile-on-privileg.patch`:
  Makes sure that AppArmor rules are always loaded, also on reboot. This
  is a long standing bug in Docker and affects Supervisor which is a
  privileged container with an AppArmor profile.
  Upstream PR: moby/moby#52215
- `0002-bridge-protect-bridge-subnet-from-direct-external-ac.patch`:
  Makes sure that the whole network (including gateway IP) of any Docker
  bridge network in NAT mode is firewalled from access from the outside.
  This essentially implements on Docker level what Supervisor applies on
  startup with home-assistant/supervisor#6650.
  Upstream PR: moby/moby#52224.
agners added a commit to home-assistant/operating-system that referenced this pull request Mar 25, 2026
This adds two patches with fixes/improvements for the Docker engine

- `0001-daemon-respect-explicit-AppArmor-profile-on-privileg.patch`:
  Makes sure that AppArmor rules are always loaded, also on reboot. This
  is a long standing bug in Docker and affects Supervisor which is a
  privileged container with an AppArmor profile.
  Upstream PR: moby/moby#52215
- `0002-bridge-protect-bridge-subnet-from-direct-external-ac.patch`:
  Makes sure that the whole network (including gateway IP) of any Docker
  bridge network in NAT mode is firewalled from access from the outside.
  This essentially implements on Docker level what Supervisor applies on
  startup with home-assistant/supervisor#6650.
  Upstream PR: moby/moby#52224.
@thaJeztah thaJeztah requested a review from robmry March 25, 2026 16:12
@github-actions github-actions bot added the area/daemon Core Engine label Mar 27, 2026
Copy link
Copy Markdown
Contributor

@robmry robmry left a comment

Choose a reason for hiding this comment

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

Thank you @agners - this looks good, I've kicked off the tests.

We should have done it like this in the first place! I think it started off as a rule per exposed port and I expanded it to per-container, but should have taken it to per-subnet.

I guess the idea is to add protection for "other things" connected to the bridge, not just containers?

}
func (ic iptablesCleaner) DelEndpoint(_ context.Context, _ firewaller.NetworkConfig, _, _ netip.Addr) {
// Direct access filtering is now done at the network (subnet) level, not
// per-endpoint. There are no per-endpoint raw PREROUTING rules to clean up.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe we should keep cleanup for the old rules, so they're deleted on upgrade?

Copy link
Copy Markdown
Author

@agners agners Mar 27, 2026

Choose a reason for hiding this comment

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

I've put this back DelEndpoint, and renamed filterDirectAccess to deleteLegacyDirectAccess which deletes alwasys (no enable bool flag). Maybe we could remove the AllowDirectRouting or rawRulesDisabled return condition (delete always instead), in case the daemon config changed 🤔 . But then again, maybe something else creates this rules if those options are set, and its better to not touch them at all, as it used to be.

@thaJeztah
Copy link
Copy Markdown
Member

Looks like some golden files need to be re-generated; do you know if we have a make or hack/make.sh target for that @robmry ?

        You can run 'go test . -update' to automatically update testdata/TestNftabler/hairpin=true,internal=false,icc=true,masq=true,snat=true,gwm=routed,bindlh=true,wsl2mirrored=true__ip.golden to the new expected value.'

@robmry
Copy link
Copy Markdown
Contributor

robmry commented Mar 27, 2026

Looks like some golden files need to be re-generated; do you know if we have a make or hack/make.sh target for that @robmry ?

Ah, yes - and no, I don't think we do. I just run "go test" manually.

Looks like I forgot to update the text in these last time around too ...
https://github.com/moby/moby/blob/master/integration/network/bridge/iptablesdoc/templates/usernet-portmap.md
https://github.com/moby/moby/blob/master/integration/network/bridge/nftablesdoc/templates/usernet-portmap.md

At the moment they still both talk about the per-port rules:

A rule in raw-PREROUTING makes sure the container's published port cannot be accessed from outside the host, because the network has the default gateway mode "nat":

Those templates are used by a "go test -update" to generate docs with generated rules filled in...
https://github.com/moby/moby/blob/master/integration/network/bridge/iptablesdoc/generated/usernet-portmap.md
https://github.com/moby/moby/blob/master/integration/network/bridge/nftablesdoc/generated/usernet-portmap.md

@agners
Copy link
Copy Markdown
Author

agners commented Mar 27, 2026

We should have done it like this in the first place! I think it started off as a rule per exposed port and I expanded it to per-container, but should have taken it to per-subnet.

I guess the idea is to add protection for "other things" connected to the bridge, not just containers?

Yeah honestly that was what we expected, that the whole subnet would be internal only. The fact that the gateway IP is actually reachable from outside was a surprise to us, leading to a CVE/security issue on our end (GHSA-gh5m-4m97-c95h). We use the gateway to bind internal services to in Home Assistant Supervisor/Operating System environment.

@agners agners force-pushed the add-bridge-network-protection branch from d930c60 to 12191f9 Compare March 27, 2026 14:48
…OUTING

Add a per-network rule to the raw PREROUTING chain dropping packets
destined to any address in the bridge subnet from interfaces other than
the bridge itself, loopback, or configured trusted host interfaces.

This covers both containers on unpublished ports and the bridge's own
gateway address (the host-side bridge IP, e.g. 172.17.0.1) from external
hosts that have a direct route to the bridge subnet. A single
network-level subnet rule is simpler than per-container rules and ensures
the gateway address is covered by the same protection policy.

For iptables, the following rules are added per bridge (plus one ACCEPT
per trusted interface):
  -t raw -A PREROUTING -d <subnet> -i lo -j ACCEPT
  -t raw -A PREROUTING -d <subnet> ! -i <bridge> -j DROP

For nftables, a single compound rule is used:
  <family> daddr <subnet> iifname != { <bridge>, lo, ... } drop

Loopback is always permitted: the gateway IP is a valid host address
reachable from local processes via lo. The rules are not added for
gateway modes "routed" or "nat-unprotected", nor when
--allow-direct-routing is set, consistent with the intent of those
options. DOCKER_INSECURE_NO_IPTABLES_RAW=1 disables the iptables rules.

Legacy per-container raw PREROUTING rules from older daemon versions are
cleaned up on upgrade via the existing firewall cleaner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stefan Agner <stefan@agner.ch>
@agners agners force-pushed the add-bridge-network-protection branch from 12191f9 to ad20817 Compare March 27, 2026 15:42
Regenerate TestNftabler golden files to reflect the change from
per-container to per-subnet direct access filtering in raw-PREROUTING.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stefan Agner <stefan@agner.ch>
@agners agners force-pushed the add-bridge-network-protection branch from b27fcf7 to efc3993 Compare March 27, 2026 16:43
@thaJeztah
Copy link
Copy Markdown
Member

Thanks for updating! I gave CI a kick, but looks like the windows runners were flaky and need another kick (I'll do so when they finish).

The fact that the gateway IP is actually reachable from outside was a surprise to us, leading to a CVE/security issue on our end (GHSA-gh5m-4m97-c95h).

Yes, we briefly discussed if we needed an advisory for this, but it's a bit of a grey area with things attached to the network outside of the daemon's knowledge.

We'll probably check with our security team as well, and can still decide to do so; it might help raise awareness for other setups doing something similar.

@thaJeztah
Copy link
Copy Markdown
Member

/cc @temenuzhka-thede

sairon pushed a commit to home-assistant/operating-system that referenced this pull request Mar 30, 2026
This adds two patches with fixes/improvements for the Docker engine

- `0001-daemon-respect-explicit-AppArmor-profile-on-privileg.patch`:
  Makes sure that AppArmor rules are always loaded, also on reboot. This
  is a long standing bug in Docker and affects Supervisor which is a
  privileged container with an AppArmor profile.
  Upstream PR: moby/moby#52215
- `0002-bridge-protect-bridge-subnet-from-direct-external-ac.patch`:
  Makes sure that the whole network (including gateway IP) of any Docker
  bridge network in NAT mode is firewalled from access from the outside.
  This essentially implements on Docker level what Supervisor applies on
  startup with home-assistant/supervisor#6650.
  Upstream PR: moby/moby#52224.
Regenerate TestIptabler golden files to reflect the change from
per-container to per-subnet direct access filtering in raw PREROUTING.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stefan Agner <stefan@agner.ch>
@agners agners force-pushed the add-bridge-network-protection branch from a567700 to 134dc2d Compare March 30, 2026 09:50
The iptabler cleaner's DelNetwork must also remove setSubnetProtection
rules when switching from iptables to nftables backend, otherwise
TestFirewallBackendSwitch fails with leftover raw PREROUTING rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stefan Agner <stefan@agner.ch>
@agners agners force-pushed the add-bridge-network-protection branch from 371a8e5 to 469cd0f Compare March 30, 2026 10:41
sairon pushed a commit to home-assistant/operating-system that referenced this pull request Mar 31, 2026
This adds two patches with fixes/improvements for the Docker engine

- `0001-daemon-respect-explicit-AppArmor-profile-on-privileg.patch`:
  Makes sure that AppArmor rules are always loaded, also on reboot. This
  is a long standing bug in Docker and affects Supervisor which is a
  privileged container with an AppArmor profile.
  Upstream PR: moby/moby#52215
- `0002-bridge-protect-bridge-subnet-from-direct-external-ac.patch`:
  Makes sure that the whole network (including gateway IP) of any Docker
  bridge network in NAT mode is firewalled from access from the outside.
  This essentially implements on Docker level what Supervisor applies on
  startup with home-assistant/supervisor#6650.
  Upstream PR: moby/moby#52224.

(cherry picked from commit 50c1efd)
Comment on lines 28 to 30
if n.ipt.config.AllowDirectRouting || rawRulesDisabled(ctx) {
enable = false
return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this check / early return should be dropped, to unconditionally delete the rules.

(It's a very-corner case, but if "AllowDirectRouting" got enabled on upgrade we still need to delete the old rules.)

Copy link
Copy Markdown
Author

@agners agners Mar 31, 2026

Choose a reason for hiding this comment

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

Yeah I had this already in a previous iteration, but I thought maybe if something else created such rules it's better not to touch it (since we can't say for certainty its created by Docker). But either way works for me, I removed this guard to make Docker always cleanup the rules.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

since we can't say for certainty its created by Docker

Yes, fair point. We tend to remove rules we probably-added anyway - subnet addresses get recycled when networks are created/deleted and a stray rule from an old network could get very confusing.

This all gets much easier with iptables. Hopefully we'll be able to start switching over soon.

Thank you for the updates, it's all looking good. I've kicked off another test run.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This all gets much easier with iptables. Hopefully we'll be able to start switching over soon.

You meant to say nftables?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I did!


func (n *network) DelEndpoint(ctx context.Context, epIPv4, epIPv6 netip.Addr) error {
return n.modEndpoint(ctx, epIPv4, epIPv6, false)
func (n *network) AddEndpoint(_ context.Context, _, _ netip.Addr) error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think AddEndpoint needs to call deleteLegacyDirectAccess, to delete the old rules on upgrade if live-restore is enabled.

func (n *network) AddEndpoint(ctx context.Context, epIPv4, epIPv6 netip.Addr) error {
        if n.ipt.config.IPv4 && epIPv4.IsValid() {
                if err := n.deleteLegacyDirectAccess(ctx, iptables.IPv4, n.config.Config4, epIPv4); err != nil {
                        return err
                }
        }
        if n.ipt.config.IPv6 && epIPv6.IsValid() {
                if err := n.deleteLegacyDirectAccess(ctx, iptables.IPv6, n.config.Config6, epIPv6); err != nil {
                        return err
                }
        }
        return nil
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point. Added this accordingly.

agners and others added 2 commits March 31, 2026 19:58
Drop the AllowDirectRouting/rawRulesDisabled early return in
deleteLegacyDirectAccess so that legacy rules are unconditionally
cleaned up even if the daemon config changed on upgrade.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stefan Agner <stefan@agner.ch>
AddEndpoint must call deleteLegacyDirectAccess so that old per-container
raw PREROUTING rules are removed when the daemon restarts with
live-restore enabled. Without this, old rules survive the restart since
containers are not stopped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stefan Agner <stefan@agner.ch>
@agners agners requested a review from robmry March 31, 2026 18:03
@agners
Copy link
Copy Markdown
Author

agners commented Mar 31, 2026

Hm, seems a couple of tests fail, and those are legitimate failures:

2026-03-31T18:30:27.9604357Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=routed/proxy=true/ipv4 (3.29s)
2026-03-31T18:30:27.9605320Z     bridge_linux_test.go:605: assertion failed: string "Connecting to 172.18.0.1:8080 (172.18.0.1:8080)\nwget: download timed out\n" does not contain "404 Not Found": url: http://172.18.0.1:8080
2026-03-31T18:30:27.9606301Z         --- FAIL: TestAccessToPublishedPort/client=routed/proxy=true/ipv4 (3.29s)

I guess we'd need to treat the gateway address just like any other host address so that published ports are still accessible then 🤔

Full details:

Details
2026-03-31T18:30:27.9603944Z === Failed
2026-03-31T18:30:27.9604357Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=routed/proxy=true/ipv4 (3.29s)
2026-03-31T18:30:27.9605320Z     bridge_linux_test.go:605: assertion failed: string "Connecting to 172.18.0.1:8080 (172.18.0.1:8080)\nwget: download timed out\n" does not contain "404 Not Found": url: http://172.18.0.1:8080
2026-03-31T18:30:27.9606301Z         --- FAIL: TestAccessToPublishedPort/client=routed/proxy=true/ipv4 (3.29s)
2026-03-31T18:30:27.9606627Z 
2026-03-31T18:30:27.9607146Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=routed/proxy=true/ipv6 (3.29s)
2026-03-31T18:30:27.9608267Z     bridge_linux_test.go:605: assertion failed: string "Connecting to [fd75:8652:97b7::1]:8080 ([fd75:8652:97b7::1]:8080)\nwget: download timed out\n" does not contain "404 Not Found": url: http://[fd75:8652:97b7::1]:8080
2026-03-31T18:30:27.9609128Z         --- FAIL: TestAccessToPublishedPort/client=routed/proxy=true/ipv6 (3.29s)
2026-03-31T18:30:27.9609415Z 
2026-03-31T18:30:27.9609727Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=routed/proxy=true (8.37s)
2026-03-31T18:30:27.9610304Z     --- FAIL: TestAccessToPublishedPort/client=routed/proxy=true (8.37s)
2026-03-31T18:30:27.9610562Z 
2026-03-31T18:30:27.9610905Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=routed/proxy=false/ipv4 (3.28s)
2026-03-31T18:30:27.9611841Z     bridge_linux_test.go:605: assertion failed: string "Connecting to 172.18.0.1:8080 (172.18.0.1:8080)\nwget: download timed out\n" does not contain "404 Not Found": url: http://172.18.0.1:8080
2026-03-31T18:30:27.9612625Z         --- FAIL: TestAccessToPublishedPort/client=routed/proxy=false/ipv4 (3.28s)
2026-03-31T18:30:27.9612911Z 
2026-03-31T18:30:27.9613249Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=routed/proxy=false/ipv6 (3.29s)
2026-03-31T18:30:27.9637785Z     bridge_linux_test.go:605: assertion failed: string "Connecting to [fd75:8652:97b7::1]:8080 ([fd75:8652:97b7::1]:8080)\nwget: download timed out\n" does not contain "404 Not Found": url: http://[fd75:8652:97b7::1]:8080
2026-03-31T18:30:27.9638875Z         --- FAIL: TestAccessToPublishedPort/client=routed/proxy=false/ipv6 (3.29s)
2026-03-31T18:30:27.9639610Z 
2026-03-31T18:30:27.9639956Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=routed/proxy=false (8.38s)
2026-03-31T18:30:27.9640547Z     --- FAIL: TestAccessToPublishedPort/client=routed/proxy=false (8.38s)
2026-03-31T18:30:27.9644781Z 
2026-03-31T18:30:27.9645124Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=nat/proxy=true/ipv4 (3.31s)
2026-03-31T18:30:27.9646078Z     bridge_linux_test.go:605: assertion failed: string "Connecting to 172.18.0.1:8080 (172.18.0.1:8080)\nwget: download timed out\n" does not contain "404 Not Found": url: http://172.18.0.1:8080
2026-03-31T18:30:27.9647013Z         --- FAIL: TestAccessToPublishedPort/client=nat/proxy=true/ipv4 (3.31s)
2026-03-31T18:30:27.9647293Z 
2026-03-31T18:30:27.9647622Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=nat/proxy=true/ipv6 (3.28s)
2026-03-31T18:30:27.9648660Z     bridge_linux_test.go:605: assertion failed: string "Connecting to [fd75:8652:97b7::1]:8080 ([fd75:8652:97b7::1]:8080)\nwget: download timed out\n" does not contain "404 Not Found": url: http://[fd75:8652:97b7::1]:8080
2026-03-31T18:30:27.9649494Z         --- FAIL: TestAccessToPublishedPort/client=nat/proxy=true/ipv6 (3.28s)
2026-03-31T18:30:27.9650162Z 
2026-03-31T18:30:27.9650472Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=nat/proxy=true (8.48s)
2026-03-31T18:30:27.9651025Z     --- FAIL: TestAccessToPublishedPort/client=nat/proxy=true (8.48s)
2026-03-31T18:30:27.9651273Z 
2026-03-31T18:30:27.9651596Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=nat/proxy=false/ipv4 (3.28s)
2026-03-31T18:30:27.9652512Z     bridge_linux_test.go:605: assertion failed: string "Connecting to 172.18.0.1:8080 (172.18.0.1:8080)\nwget: download timed out\n" does not contain "404 Not Found": url: http://172.18.0.1:8080
2026-03-31T18:30:27.9653285Z         --- FAIL: TestAccessToPublishedPort/client=nat/proxy=false/ipv4 (3.28s)
2026-03-31T18:30:27.9653562Z 
2026-03-31T18:30:27.9653883Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=nat/proxy=false/ipv6 (3.28s)
2026-03-31T18:30:27.9654909Z     bridge_linux_test.go:605: assertion failed: string "Connecting to [fd75:8652:97b7::1]:8080 ([fd75:8652:97b7::1]:8080)\nwget: download timed out\n" does not contain "404 Not Found": url: http://[fd75:8652:97b7::1]:8080
2026-03-31T18:30:27.9655745Z         --- FAIL: TestAccessToPublishedPort/client=nat/proxy=false/ipv6 (3.28s)
2026-03-31T18:30:27.9656019Z 
2026-03-31T18:30:27.9656321Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort/client=nat/proxy=false (8.44s)
2026-03-31T18:30:27.9657107Z     --- FAIL: TestAccessToPublishedPort/client=nat/proxy=false (8.44s)
2026-03-31T18:30:27.9657375Z 
2026-03-31T18:30:27.9657599Z === FAIL: arm64.docker.docker.integration.networking TestAccessToPublishedPort (33.73s)
2026-03-31T18:30:27.9657919Z 
2026-03-31T18:30:27.9658156Z === FAIL: arm64.docker.docker.integration.networking TestSkipRawRules/skip=false (1.34s)
2026-03-31T18:30:27.9658614Z     port_mapping_linux_test.go:1529: ip netns add test-skip-raw
2026-03-31T18:30:27.9659054Z     port_mapping_linux_test.go:1529: ip netns exec test-skip-raw ip link add br0 type bridge
2026-03-31T18:30:27.9659495Z     port_mapping_linux_test.go:1535: ip netns add ns-skip-raw
2026-03-31T18:30:27.9660027Z     port_mapping_linux_test.go:1535: ip netns exec ns-skip-raw ip link add skip-raw netns test-skip-raw type veth peer name eth0
2026-03-31T18:30:27.9660676Z     port_mapping_linux_test.go:1535: ip netns exec test-skip-raw ip link set skip-raw up master br0
2026-03-31T18:30:27.9661190Z     port_mapping_linux_test.go:1535: ip netns exec ns-skip-raw ip link set eth0 up
2026-03-31T18:30:27.9661643Z     port_mapping_linux_test.go:1535: ip netns exec ns-skip-raw ip link set lo up
2026-03-31T18:30:27.9662381Z     port_mapping_linux_test.go:1535: ip netns exec ns-skip-raw sysctl -w net.ipv4.ip_forward=1
2026-03-31T18:30:27.9662963Z     port_mapping_linux_test.go:1535: ip netns exec ns-skip-raw sysctl -w net.ipv6.conf.all.forwarding=1
2026-03-31T18:30:27.9663557Z     port_mapping_linux_test.go:1535: ip netns exec ns-skip-raw ip addr add 192.168.234.0/24 dev eth0 nodad
2026-03-31T18:30:27.9664170Z     port_mapping_linux_test.go:1535: ip netns exec ns-skip-raw ip addr add fd3f:c09d:715b::/64 dev eth0 nodad
2026-03-31T18:30:27.9664628Z     port_mapping_linux_test.go:1554: assertion failed: 
2026-03-31T18:30:27.9664887Z         --- expected
2026-03-31T18:30:27.9665054Z         +++ actual
2026-03-31T18:30:27.9665211Z         @@ -1,5 +1,6 @@
2026-03-31T18:30:27.9665401Z          -P PREROUTING ACCEPT
2026-03-31T18:30:27.9665610Z          -P OUTPUT ACCEPT
2026-03-31T18:30:27.9665843Z         --A PREROUTING -d 192.168.0.2/32 ! -i docker0 -j DROP
2026-03-31T18:30:27.9666149Z         +-A PREROUTING -d 192.168.0.0/24 -i lo -j ACCEPT
2026-03-31T18:30:27.9667134Z         +-A PREROUTING -d 192.168.0.0/24 ! -i docker0 -j DROP
2026-03-31T18:30:27.9667519Z          -A PREROUTING -d 127.0.0.1/32 ! -i lo -p tcp -m tcp --dport 8080 -j DROP
2026-03-31T18:30:27.9667838Z          
2026-03-31T18:30:27.9667978Z         
2026-03-31T18:30:27.9668365Z         
2026-03-31T18:30:27.9668817Z         You can run 'go test . -update' to automatically update testdata/TestSkipRawRules/skip=false_ipv4.golden to the new expected value.'
2026-03-31T18:30:27.9669336Z         
2026-03-31T18:30:27.9669550Z     panic.go:615: ip netns exec ns-skip-raw ip link delete eth0
2026-03-31T18:30:27.9669860Z     panic.go:615: ip netns delete ns-skip-raw
2026-03-31T18:30:27.9670163Z     panic.go:615: ip netns exec test-skip-raw ip link delete br0
2026-03-31T18:30:27.9670471Z     panic.go:615: ip netns delete test-skip-raw
2026-03-31T18:30:27.9670745Z     --- FAIL: TestSkipRawRules/skip=false (1.34s)
2026-03-31T18:30:27.9670931Z 
2026-03-31T18:30:27.9671121Z === FAIL: arm64.docker.docker.integration.networking TestSkipRawRules (2.68s)
2026-03-31T18:30:27.9671414Z 
2026-03-31T18:30:27.9671516Z DONE 292 tests, 2 skipped, 15 failures in 449.572s

@agners
Copy link
Copy Markdown
Author

agners commented Mar 31, 2026

I guess we'd need to treat the gateway address just like any other host address so that published ports are still accessible then 🤔

Actually, traffic addressed to other host addresses go through DNAT.

So this is really a case I haven't considered: One bridge accessing published ports of containers in another bridge, through the gateway address. One way to solve this is to just consider the other bridge as trusted network through com.docker.network.bridge.trusted_host_interfaces=br-client. Or we implicitly consider other Docker bridges as trusted? Thoughts?

@robmry
Copy link
Copy Markdown
Contributor

robmry commented Apr 1, 2026

Actually, traffic addressed to other host addresses go through DNAT.

So this is really a case I haven't considered: One bridge accessing published ports of containers in another bridge, through the gateway address. One way to solve this is to just consider the other bridge as trusted network through com.docker.network.bridge.trusted_host_interfaces=br-client. Or we implicitly consider other Docker bridges as trusted? Thoughts?

Oh, yes - that's a bit tricky. "Something-something trusted interfaces" was my first thought too, it's probably the right way.

But the networks that need to be trusted will come and go as bridge networks are created and deleted

With nftables we can have a set of interfaces to allow.

With iptables, It'd be difficult to insert iptables rules before the DROP in each network's group of prerouting rules. We can't use ipset with iptables (we tried it briefly a while ago and had to revert, lots of users have kernels that don't support it - docker running on NAS drives etc). So I think we'd need another chain of allow rules, or something along those lines... that's a slightly bigger change though.

@akerouanton, @corhere - any bright ideas?

@thaJeztah
Copy link
Copy Markdown
Member

Do we need to keep feature parity between iptables and nftables, or could we make them diverge (use nftables for the improved rules)? Not sure if that complicates the core more to differentiate them, but mostly drawing the parallel with our snapshotter vs graph driver implementations, where not all features are available with graph drivers

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.

3 participants