diff --git a/integration/network/bridge/iptablesdoc/generated/new-daemon.md b/integration/network/bridge/iptablesdoc/generated/new-daemon.md
new file mode 100644
index 0000000000..74087516a2
--- /dev/null
+++ b/integration/network/bridge/iptablesdoc/generated/new-daemon.md
@@ -0,0 +1,159 @@
+## iptables for a new Daemon
+
+When the daemon starts, it creates custom chains, and rules for the
+default bridge network.
+
+Table `filter`:
+
+ Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
+ num pkts bytes target prot opt in out source destination
+
+ Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
+ num pkts bytes target prot opt in out source destination
+ 1 0 0 DOCKER-USER 0 -- * * 0.0.0.0/0 0.0.0.0/0
+ 2 0 0 DOCKER-ISOLATION-STAGE-1 0 -- * * 0.0.0.0/0 0.0.0.0/0
+ 3 0 0 ACCEPT 0 -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
+ 4 0 0 DOCKER 0 -- * docker0 0.0.0.0/0 0.0.0.0/0
+ 5 0 0 ACCEPT 0 -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
+ 6 0 0 ACCEPT 0 -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
+
+ Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
+ num pkts bytes target prot opt in out source destination
+
+ Chain DOCKER (1 references)
+ num pkts bytes target prot opt in out source destination
+
+ Chain DOCKER-ISOLATION-STAGE-1 (1 references)
+ num pkts bytes target prot opt in out source destination
+ 1 0 0 DOCKER-ISOLATION-STAGE-2 0 -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
+ 2 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
+
+ Chain DOCKER-ISOLATION-STAGE-2 (1 references)
+ num pkts bytes target prot opt in out source destination
+ 1 0 0 DROP 0 -- * docker0 0.0.0.0/0 0.0.0.0/0
+ 2 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
+
+ Chain DOCKER-USER (1 references)
+ num pkts bytes target prot opt in out source destination
+ 1 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
+
+
+
+iptables commands
+
+ -P INPUT ACCEPT
+ -P FORWARD ACCEPT
+ -P OUTPUT ACCEPT
+ -N DOCKER
+ -N DOCKER-ISOLATION-STAGE-1
+ -N DOCKER-ISOLATION-STAGE-2
+ -N DOCKER-USER
+ -A FORWARD -j DOCKER-USER
+ -A FORWARD -j DOCKER-ISOLATION-STAGE-1
+ -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+ -A FORWARD -o docker0 -j DOCKER
+ -A FORWARD -i docker0 ! -o docker0 -j ACCEPT
+ -A FORWARD -i docker0 -o docker0 -j ACCEPT
+ -A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
+ -A DOCKER-ISOLATION-STAGE-1 -j RETURN
+ -A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
+ -A DOCKER-ISOLATION-STAGE-2 -j RETURN
+ -A DOCKER-USER -j RETURN
+
+
+
+
+The FORWARD chain's policy shown above is ACCEPT. However:
+
+ - For IPv4, [setupIPForwarding][1] sets the POLICY to DROP if the sysctl
+ net.ipv4.ip_forward was not set to '1', and the daemon set it itself.
+ - For IPv6, the policy is always DROP.
+
+[1]: https://github.com/moby/moby/blob/cff4f20c44a3a7c882ed73934dec6a77246c6323/libnetwork/drivers/bridge/setup_ip_forwarding.go#L44
+
+The FORWARD chain rules are numbered in the output above, they are:
+
+ 1. Unconditional jump to DOCKER-USER.
+ This is set up by libnetwork, in [setupUserChain][10].
+ Docker won't add rules to the DOCKER-USER chain, it's only for user-defined rules.
+ It's (mostly) kept at the top of the by deleting it and re-creating after each
+ new network is created, while traffic may be running for other networks.
+ 2. Unconditional jump to DOCKER-ISOLATION-STAGE-1.
+ Set up during network creation by [setupIPTables][11], which ensures it appears
+ after the jump to DOCKER-USER (by deleting it and re-creating, while traffic
+ may be running for other networks).
+ 3. ACCEPT RELATED,ESTABLISHED packets into a specific bridge network.
+ Allows responses to outgoing requests, and continuation of incoming requests,
+ without needing to process any further rules.
+ This rule is also added during network creation, but the code to do it
+ is in libnetwork, [ProgramChain][12].
+ 4. Jump to DOCKER, for any packet destined for a bridge network. Added when
+ the network is created, in [ProgramChain][13] ("filterChain" is the DOCKER chain).
+ The DOCKER chain implements per-port/protocol filtering for each container.
+ 5. ACCEPT any packet leaving a network, also set up when the network is created, in
+ [setupIPTablesInternal][14].
+ 6. ACCEPT packets flowing between containers within a network, because by default
+ container isolation is disabled. Also set up when the network is created, in
+ [setIcc][15].
+
+[10]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/firewall_linux.go#L50
+[11]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L201
+[12]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/iptables/iptables.go#L270
+[13]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/iptables/iptables.go#L251-L255
+[14]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L264
+[15]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L343
+
+_With ICC enabled 5 and 6 could be combined, to ACCEPT anything from the bridge.
+But, when ICC is disabled, rule 6 is DROP, so it would need to be placed before
+rule 5. Because the rules are generated in different places, that's a slightly
+bigger change than it should be._
+
+The DOCKER chain is empty, because there are no containers with port mappings yet.
+
+The DOCKER-ISOLATION chains implement inter-network isolation, all (unrelated)
+packets are processed by these chains. The rule are inserted at the head of the
+chain when a network is created, in [setINC][20].
+ - DOCKER-ISOLATION-STAGE-1 jumps to DOCKER-ISOLATION-STAGE-2 for any packet
+ routed to a docker network that has not come from that docker network.
+ - DOCKER-ISOLATION-STAGE-2 processes all packets leaving a bridge network,
+ packets that are destined for any other network are dropped.
+
+[20]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L369
+
+Table nat:
+
+ Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
+ num pkts bytes target prot opt in out source destination
+ 1 0 0 DOCKER 0 -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
+
+ Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
+ num pkts bytes target prot opt in out source destination
+
+ Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
+ num pkts bytes target prot opt in out source destination
+ 1 0 0 DOCKER 0 -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
+
+ Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
+ num pkts bytes target prot opt in out source destination
+ 1 0 0 MASQUERADE 0 -- * !docker0 172.17.0.0/16 0.0.0.0/0
+
+ Chain DOCKER (2 references)
+ num pkts bytes target prot opt in out source destination
+ 1 0 0 RETURN 0 -- docker0 * 0.0.0.0/0 0.0.0.0/0
+
+
+
+iptables commands
+
+ -P PREROUTING ACCEPT
+ -P INPUT ACCEPT
+ -P OUTPUT ACCEPT
+ -P POSTROUTING ACCEPT
+ -N DOCKER
+ -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
+ -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
+ -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
+ -A DOCKER -i docker0 -j RETURN
+
+
+
diff --git a/integration/network/bridge/iptablesdoc/index.md b/integration/network/bridge/iptablesdoc/index.md
new file mode 100644
index 0000000000..b8b87149bb
--- /dev/null
+++ b/integration/network/bridge/iptablesdoc/index.md
@@ -0,0 +1,41 @@
+# Docker Engine's use of iptables
+
+> [!WARNING]
+> This is intended for development use - the structure of docker's iptables
+> (and ip6tables) rules will change between releases, it is not a stable
+> interface.
+
+> [!NOTE]
+> This document is generated by `TestBridgeIptablesDoc` by running a
+> daemon, creating networks and containers, and capturing iptables.
+> The iptables are then merged with a text/template for each section.
+> The resulting document is diffed against one in the repo, so the
+> test will fail if there are differences in the generated rules (but
+> changes in the templates may go unnoticed).
+>
+> Links to code are permalinks - they will be out of date, and may not
+> point to the master branch. But, it's difficult to work out where
+> some of the rules come from, the links are intended as hints.
+
+ip6tables rules follow the same pattern as iptables rules. So, only the
+IPv4 rules are shown here.
+
+The bridge driver deletes its custom chains during its initialisation, in
+[configure][100]. Rules are then re-created as networks are restored. However,
+the filter-FORWARD chain is not cleared. The order in which networks are
+re-created is not the order in which they were originally created. So,
+rules may be arranged differently following a daemon restart.
+
+When firewalld is running, if it's reloaded, iptables rules are cleared.
+The daemon registers handlers for its reload event (received via dbus)
+to reconstruct the rules.
+
+The filter-INPUT chain is not used by Docker. Packets arriving from the host's
+physical network or the host itself hit the filter-FORWARD chain, as they are
+routed into the bridge network. Similarly, filter-OUTPUT is not used.
+
+[100]: https://github.com/moby/moby/blob/fe09cab7fe04c3911417061f7c7ef60a8acc6bf3/libnetwork/drivers/bridge/bridge_linux.go#L508
+
+Scenarios:
+
+ - [New daemon](generated/new-daemon.md)
diff --git a/integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go b/integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go
new file mode 100644
index 0000000000..2a89c0a7bd
--- /dev/null
+++ b/integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go
@@ -0,0 +1,233 @@
+// Package iptablesdoc runs docker, creates networks, runs containers and
+// captures iptables output for various configurations.
+//
+// The iptables output is then used with a markdown text/template from the
+// "templates" directory for each configuration (for each "section" in "index"),
+// to generate a markdown document for each section.
+//
+// The newly generated documents are placed in:
+//
+// bundles/test-integration/TestBridgeIptablesDoc/iptables.md
+//
+// If the generated doc differs from the "golden" reference in "generated/",
+// the test fails. When that happens:
+//
+// - check the iptables rules changes in the diff
+// - update the description in the corresponding "_templ.md" file
+// - re-run with TESTFLAGS='-update' to update the reference docs
+package iptablesdoc
+
+import (
+ "context"
+ "fmt"
+ "net/netip"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+ "text/template"
+
+ containertypes "github.com/docker/docker/api/types/container"
+ networktypes "github.com/docker/docker/api/types/network"
+ "github.com/docker/docker/integration/internal/container"
+ "github.com/docker/docker/integration/internal/network"
+ "github.com/docker/docker/internal/testutils/networking"
+ "github.com/docker/docker/libnetwork/drivers/bridge"
+ "github.com/docker/docker/testutil"
+ "github.com/docker/docker/testutil/daemon"
+ "github.com/docker/go-connections/nat"
+ "gotest.tools/v3/assert"
+ "gotest.tools/v3/golden"
+ "gotest.tools/v3/skip"
+)
+
+var (
+ docNetworks = []string{"192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"}
+ docGateways = []string{"192.0.2.1", "198.51.100.1", "203.0.113.1"}
+)
+
+type ctr struct {
+ name string
+ portMappings nat.PortMap
+}
+
+type bridgeNetwork struct {
+ bridge string
+ gwMode string
+ noICC bool
+ internal bool
+ containers []ctr
+}
+
+type section struct {
+ name string
+ noUserlandProxy bool
+ networks []bridgeNetwork
+}
+
+var index = []section{
+ {
+ name: "new-daemon.md",
+ },
+}
+
+// iptCmdType is used to look up iptCmds in the markdown (can't use an int
+// type, or a new string type, so it's just an alias).
+type iptCmdType = string
+
+const (
+ iptCmdLFilter4 iptCmdType = "LFilter4"
+ iptCmdSFilter4 iptCmdType = "SFilter4"
+ iptCmdSFilterForward4 iptCmdType = "SFilterForward4"
+ iptCmdSFilterDocker4 iptCmdType = "SFilterDocker4"
+ iptCmdLNat4 iptCmdType = "LNat4"
+ iptCmdSNat4 iptCmdType = "SNat4"
+)
+
+var iptCmds = map[iptCmdType][]string{
+ iptCmdLFilter4: {"iptables", "-nvL", "--line-numbers", "-t", "filter"},
+ iptCmdSFilter4: {"iptables", "-S", "-t", "filter"},
+ iptCmdSFilterForward4: {"iptables", "-S", "FORWARD"},
+ iptCmdSFilterDocker4: {"iptables", "-S", "DOCKER"},
+ iptCmdLNat4: {"iptables", "-nvL", "--line-numbers", "-t", "nat"},
+ iptCmdSNat4: {"iptables", "-S", "-t", "nat"},
+}
+
+func TestBridgeIptablesDoc(t *testing.T) {
+ skip.If(t, testEnv.IsRootless)
+ ctx := setupTest(t)
+
+ // Get the full path for "bundles/TestBridgeIptablesDoc".
+ dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST")
+ if dest == "" {
+ dest = os.Getenv("DEST")
+ }
+ dest = filepath.Join(dest, t.Name())
+
+ // Set up an L3Segment, which will have a netns for each "section".
+ addr4 := netip.MustParseAddr("192.168.124.1")
+ addr6 := netip.MustParseAddr("fdc0:36dc:a4dd::1")
+ l3 := networking.NewL3Segment(t, "gen-iptables-doc",
+ netip.PrefixFrom(addr4, 24),
+ netip.PrefixFrom(addr6, 64),
+ )
+ t.Cleanup(func() { l3.Destroy(t) })
+
+ for i, sec := range index {
+ // Create a netns for this section.
+ addr4 = addr4.Next()
+ addr6 = addr6.Next()
+ hostname := fmt.Sprintf("docker%d", i)
+ l3.AddHost(t, hostname, hostname+"-host", "eth0",
+ netip.PrefixFrom(addr4, 24),
+ netip.PrefixFrom(addr6, 64),
+ )
+ host := l3.Hosts[hostname]
+ // Stop the interface, to reduce the chances of stray packets getting counted by iptables.
+ host.Run(t, "ip", "link", "set", "eth0", "down")
+
+ t.Run("gen_"+sec.name, func(t *testing.T) {
+ // t.Parallel() - doesn't speed things up, startup times just extend
+ runTestNet(t, testutil.StartSpan(ctx, t), dest, sec, host)
+ })
+ }
+}
+
+func runTestNet(t *testing.T, ctx context.Context, bundlesDir string, section section, host networking.Host) {
+ var dArgs []string
+ if section.noUserlandProxy {
+ dArgs = append(dArgs, "--userland-proxy=false")
+ }
+
+ // Start the daemon in its own network namespace.
+ var d *daemon.Daemon
+ host.Do(t, func() {
+ // Run without OTEL because there's no routing from this netns for it - which
+ // means the daemon doesn't shut down cleanly, causing the test to fail.
+ d = daemon.New(t, daemon.WithEnvVars("OTEL_EXPORTER_OTLP_ENDPOINT="))
+ d.StartWithBusybox(ctx, t, dArgs...)
+ t.Cleanup(func() { d.Stop(t) })
+ })
+
+ c := d.NewClientT(t)
+ t.Cleanup(func() { c.Close() })
+
+ assert.Assert(t, len(section.networks) < len(docNetworks), "Don't have enough container network addresses")
+ for i, nw := range section.networks {
+ gwMode := nw.gwMode
+ if gwMode == "" {
+ gwMode = "nat"
+ }
+ netOpts := []func(*networktypes.CreateOptions){
+ network.WithIPAM(docNetworks[i], docGateways[i]),
+ network.WithOption(bridge.BridgeName, nw.bridge),
+ network.WithOption(bridge.IPv4GatewayMode, gwMode),
+ }
+ if nw.noICC {
+ netOpts = append(netOpts, network.WithOption(bridge.EnableICC, "false"))
+ }
+ if nw.internal {
+ netOpts = append(netOpts, network.WithInternal())
+ }
+ network.CreateNoError(ctx, t, c, nw.bridge, netOpts...)
+ t.Cleanup(func() { network.RemoveNoError(ctx, t, c, nw.bridge) })
+
+ for _, ctr := range nw.containers {
+ var exposedPorts []string
+ for ep := range ctr.portMappings {
+ exposedPorts = append(exposedPorts, ep.Port()+"/"+ep.Proto())
+ }
+ id := container.Run(ctx, t, c,
+ container.WithNetworkMode(nw.bridge),
+ container.WithExposedPorts(exposedPorts...),
+ container.WithPortMap(ctr.portMappings),
+ )
+ t.Cleanup(func() {
+ c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
+ })
+ }
+ }
+
+ iptablesOutput := runIptables(t, host)
+ generated := generate(t, section.name, iptablesOutput)
+
+ // Write the output to the 'bundles' directory for easy reference.
+ outFile := filepath.Join(bundlesDir, section.name)
+ err := os.WriteFile(outFile, []byte(generated), 0o644)
+ assert.NilError(t, err)
+ t.Log("Wrote ", outFile)
+
+ // Compare against "golden" results.
+ // Use full path so that the directory containing generated docs doesn't
+ // have to be called 'testdata'.
+ wd, err := os.Getwd()
+ assert.NilError(t, err)
+ golden.Assert(t, generated, filepath.Join(wd, "generated", section.name))
+}
+
+var rePacketByteCounts = regexp.MustCompile(`\d+ packets, \d+ bytes`)
+
+func runIptables(t *testing.T, host networking.Host) map[iptCmdType]string {
+ host.Run(t, "iptables", "-Z")
+ host.Run(t, "iptables", "-Z", "-t", "nat")
+ res := map[iptCmdType]string{}
+ for k, cmd := range iptCmds {
+ d := host.Run(t, cmd[0], cmd[1:]...)
+ // In CI, the OUTPUT chain sometimes sees a packet. Remove the counts.
+ d = rePacketByteCounts.ReplaceAllString(d, "0 packets, 0 bytes")
+ // Indent the result, so that it's treated as preformatted markdown.
+ res[k] = strings.ReplaceAll(d, "\n", "\n ")
+ }
+ return res
+}
+
+func generate(t *testing.T, name string, data map[iptCmdType]string) string {
+ t.Helper()
+ templ, err := template.New(name).ParseFiles(filepath.Join("templates", name))
+ assert.NilError(t, err)
+ wr := strings.Builder{}
+ err = templ.ExecuteTemplate(&wr, name, data)
+ assert.NilError(t, err)
+ return wr.String()
+}
diff --git a/integration/network/bridge/iptablesdoc/main_linux_test.go b/integration/network/bridge/iptablesdoc/main_linux_test.go
new file mode 100644
index 0000000000..420604e272
--- /dev/null
+++ b/integration/network/bridge/iptablesdoc/main_linux_test.go
@@ -0,0 +1,56 @@
+package iptablesdoc // import "github.com/docker/docker/integration/network/bridge/iptablesdoc"
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/docker/docker/testutil"
+ "github.com/docker/docker/testutil/environment"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/codes"
+)
+
+var (
+ testEnv *environment.Execution
+ baseContext context.Context
+)
+
+func TestMain(m *testing.M) {
+ shutdown := testutil.ConfigureTracing()
+ ctx, span := otel.Tracer("").Start(context.Background(), "integration/network/bridge/iptablesdoc.TestMain")
+ baseContext = ctx
+
+ var err error
+ testEnv, err = environment.New(ctx)
+ if err != nil {
+ span.SetStatus(codes.Error, err.Error())
+ span.End()
+ shutdown(ctx)
+ panic(err)
+ }
+
+ err = environment.EnsureFrozenImagesLinux(ctx, testEnv)
+ if err != nil {
+ span.SetStatus(codes.Error, err.Error())
+ span.End()
+ shutdown(ctx)
+ panic(err)
+ }
+
+ testEnv.Print()
+ code := m.Run()
+ if code != 0 {
+ span.SetStatus(codes.Error, "m.Run() returned non-zero exit code")
+ }
+ span.End()
+ shutdown(ctx)
+ os.Exit(code)
+}
+
+func setupTest(t *testing.T) context.Context {
+ ctx := testutil.StartSpan(baseContext, t)
+ environment.ProtectAll(ctx, t, testEnv)
+ t.Cleanup(func() { testEnv.Clean(ctx, t) })
+ return ctx
+}
diff --git a/integration/network/bridge/iptablesdoc/templates/new-daemon.md b/integration/network/bridge/iptablesdoc/templates/new-daemon.md
new file mode 100644
index 0000000000..2fc16648ec
--- /dev/null
+++ b/integration/network/bridge/iptablesdoc/templates/new-daemon.md
@@ -0,0 +1,83 @@
+## iptables for a new Daemon
+
+When the daemon starts, it creates custom chains, and rules for the
+default bridge network.
+
+Table `filter`:
+
+ {{index . "LFilter4"}}
+
+
+iptables commands
+
+ {{index . "SFilter4"}}
+
+
+
+The FORWARD chain's policy shown above is ACCEPT. However:
+
+ - For IPv4, [setupIPForwarding][1] sets the POLICY to DROP if the sysctl
+ net.ipv4.ip_forward was not set to '1', and the daemon set it itself.
+ - For IPv6, the policy is always DROP.
+
+[1]: https://github.com/moby/moby/blob/cff4f20c44a3a7c882ed73934dec6a77246c6323/libnetwork/drivers/bridge/setup_ip_forwarding.go#L44
+
+The FORWARD chain rules are numbered in the output above, they are:
+
+ 1. Unconditional jump to DOCKER-USER.
+ This is set up by libnetwork, in [setupUserChain][10].
+ Docker won't add rules to the DOCKER-USER chain, it's only for user-defined rules.
+ It's (mostly) kept at the top of the by deleting it and re-creating after each
+ new network is created, while traffic may be running for other networks.
+ 2. Unconditional jump to DOCKER-ISOLATION-STAGE-1.
+ Set up during network creation by [setupIPTables][11], which ensures it appears
+ after the jump to DOCKER-USER (by deleting it and re-creating, while traffic
+ may be running for other networks).
+ 3. ACCEPT RELATED,ESTABLISHED packets into a specific bridge network.
+ Allows responses to outgoing requests, and continuation of incoming requests,
+ without needing to process any further rules.
+ This rule is also added during network creation, but the code to do it
+ is in libnetwork, [ProgramChain][12].
+ 4. Jump to DOCKER, for any packet destined for a bridge network. Added when
+ the network is created, in [ProgramChain][13] ("filterChain" is the DOCKER chain).
+ The DOCKER chain implements per-port/protocol filtering for each container.
+ 5. ACCEPT any packet leaving a network, also set up when the network is created, in
+ [setupIPTablesInternal][14].
+ 6. ACCEPT packets flowing between containers within a network, because by default
+ container isolation is disabled. Also set up when the network is created, in
+ [setIcc][15].
+
+[10]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/firewall_linux.go#L50
+[11]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L201
+[12]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/iptables/iptables.go#L270
+[13]: https://github.com/moby/moby/blob/e05848c0025b67a16aaafa8cdff95d5e2c064105/libnetwork/iptables/iptables.go#L251-L255
+[14]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L264
+[15]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L343
+
+_With ICC enabled 5 and 6 could be combined, to ACCEPT anything from the bridge.
+But, when ICC is disabled, rule 6 is DROP, so it would need to be placed before
+rule 5. Because the rules are generated in different places, that's a slightly
+bigger change than it should be._
+
+The DOCKER chain is empty, because there are no containers with port mappings yet.
+
+The DOCKER-ISOLATION chains implement inter-network isolation, all (unrelated)
+packets are processed by these chains. The rule are inserted at the head of the
+chain when a network is created, in [setINC][20].
+ - DOCKER-ISOLATION-STAGE-1 jumps to DOCKER-ISOLATION-STAGE-2 for any packet
+ routed to a docker network that has not come from that docker network.
+ - DOCKER-ISOLATION-STAGE-2 processes all packets leaving a bridge network,
+ packets that are destined for any other network are dropped.
+
+[20]: https://github.com/moby/moby/blob/333cfa640239153477bf635a8131734d0e9d099d/libnetwork/drivers/bridge/setup_ip_tables_linux.go#L369
+
+Table nat:
+
+ {{index . "LNat4"}}
+
+
+iptables commands
+
+ {{index . "SNat4"}}
+
+
diff --git a/internal/testutils/networking/l3_segment_linux.go b/internal/testutils/networking/l3_segment_linux.go
index 68d020815d..5c5e9d39d2 100644
--- a/internal/testutils/networking/l3_segment_linux.go
+++ b/internal/testutils/networking/l3_segment_linux.go
@@ -16,7 +16,8 @@ import (
// host lives in the current network namespace (eg. where dockerd runs).
const CurrentNetns = ""
-func runCommand(t *testing.T, cmd string, args ...string) {
+func runCommand(t *testing.T, cmd string, args ...string) string {
+ t.Helper()
t.Log(strings.Join(append([]string{cmd}, args...), " "))
var b bytes.Buffer
@@ -28,6 +29,7 @@ func runCommand(t *testing.T, cmd string, args ...string) {
t.Log(b.String())
t.Fatalf("Error: %v", err)
}
+ return b.String()
}
// L3Segment simulates a switched, dual-stack capable network that
@@ -113,15 +115,16 @@ func newHost(t *testing.T, nsName, ifname string) Host {
}
}
-// Run executes the provided command in the host's network namespace.
-func (h Host) Run(t *testing.T, cmd string, args ...string) {
+// Run executes the provided command in the host's network namespace
+// and returns its combined stdout/stderr.
+func (h Host) Run(t *testing.T, cmd string, args ...string) string {
t.Helper()
if h.ns != CurrentNetns {
args = append([]string{"netns", "exec", h.ns, cmd}, args...)
cmd = "ip"
}
- runCommand(t, cmd, args...)
+ return runCommand(t, cmd, args...)
}
// Do run the provided function in the host's network namespace.