From 20571e453b3002cc7dcc38c021144cf784fa952c Mon Sep 17 00:00:00 2001 From: Rob Murray Date: Thu, 10 Oct 2024 18:39:40 +0100 Subject: [PATCH] Generate iptables documentation In an integration test - run a daemon, capture iptables, and feed them to a markdown text/template describing them. Prep for repeating that, for different network configurations. Fail the test if the generated markdown differs from a "golden" version. (So, at-least the golden markdown will need to be updated if the iptables rules are deliberately changed - hopefully the corresponding description in the template will also be updated.) Signed-off-by: Rob Murray --- .../iptablesdoc/generated/new-daemon.md | 159 ++++++++++++ .../network/bridge/iptablesdoc/index.md | 41 +++ .../iptablesdoc/iptablesdoc_linux_test.go | 233 ++++++++++++++++++ .../bridge/iptablesdoc/main_linux_test.go | 56 +++++ .../iptablesdoc/templates/new-daemon.md | 83 +++++++ .../testutils/networking/l3_segment_linux.go | 11 +- 6 files changed, 579 insertions(+), 4 deletions(-) create mode 100644 integration/network/bridge/iptablesdoc/generated/new-daemon.md create mode 100644 integration/network/bridge/iptablesdoc/index.md create mode 100644 integration/network/bridge/iptablesdoc/iptablesdoc_linux_test.go create mode 100644 integration/network/bridge/iptablesdoc/main_linux_test.go create mode 100644 integration/network/bridge/iptablesdoc/templates/new-daemon.md 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.