diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux.go b/libnetwork/drivers/bridge/setup_ip_tables_linux.go index b793e119df186..436be73153046 100644 --- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go +++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "strings" "github.com/containerd/log" "github.com/docker/docker/libnetwork/iptables" @@ -241,6 +242,14 @@ func (r iptRule) Delete() error { return r.exec(iptables.Delete) } +func (r iptRule) String() string { + cmd := append([]string{"iptables"}, r.cmdArgs("-A")...) + if r.ipv == iptables.IPv6 { + cmd[0] = "ip6tables" + } + return strings.Join(cmd, " ") +} + func setupIPTablesInternal(ipVer iptables.IPVersion, config *networkConfiguration, addr *net.IPNet, hairpin, enable bool) error { var ( address = addr.String() diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go index ed30addb5fff0..2658a94dc27fb 100644 --- a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go +++ b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go @@ -5,16 +5,36 @@ import ( "testing" "github.com/docker/docker/internal/testutils/netnsutils" + "github.com/docker/docker/libnetwork/driverapi" "github.com/docker/docker/libnetwork/iptables" "github.com/docker/docker/libnetwork/netlabel" "github.com/docker/docker/libnetwork/portmapper" "github.com/vishvananda/netlink" + "gotest.tools/v3/assert" ) const ( iptablesTestBridgeIP = "192.168.42.1" ) +// A testRegisterer implements the driverapi.Registerer interface. +type testRegisterer struct { + t *testing.T + d *driver +} + +func (r *testRegisterer) RegisterDriver(name string, di driverapi.Driver, _ driverapi.Capability) error { + if got, want := name, "bridge"; got != want { + r.t.Fatalf("got driver name %s, want %s", got, want) + } + d, ok := di.(*driver) + if !ok { + r.t.Fatalf("got driver type %T, want %T", di, &driver{}) + } + r.d = d + return nil +} + func TestProgramIPTable(t *testing.T) { // Create a test bridge with a basic bridge configuration (name + IPv4). defer netnsutils.SetupTestOSContext(t)() @@ -184,3 +204,148 @@ func TestSetupIP6TablesWithHostIPv4(t *testing.T) { createTestBridge(nc, br, t) assertBridgeConfig(nc, br, d, t) } + +func TestOutgoingNATRules(t *testing.T) { + br := "br-nattest" + brIPv4 := &net.IPNet{IP: net.ParseIP(iptablesTestBridgeIP), Mask: net.CIDRMask(16, 32)} + brIPv6 := &net.IPNet{IP: net.ParseIP("2001:db8::1"), Mask: net.CIDRMask(64, 128)} + maskedBrIPv4 := &net.IPNet{IP: brIPv4.IP.Mask(brIPv4.Mask), Mask: brIPv4.Mask} + maskedBrIPv6 := &net.IPNet{IP: brIPv6.IP.Mask(brIPv6.Mask), Mask: brIPv6.Mask} + hostIPv4 := net.ParseIP("192.0.2.2") + for _, tc := range []struct { + desc string + enableIPTables bool + enableIP6Tables bool + enableIPv6 bool + enableIPMasquerade bool + hostIPv4 net.IP + // Hairpin NAT rules are not tested here because they are orthogonal to outgoing NAT. They + // exist to support the port forwarding DNAT rules: without any port forwarding there would be + // no need for any hairpin NAT rules, and when there is port forwarding then hairpin NAT rules + // are needed even if outgoing NAT is disabled. Hairpin NAT tests belong with the port + // forwarding DNAT tests. + wantIPv4Masq bool + wantIPv4Snat bool + wantIPv6Masq bool + }{ + { + desc: "everything disabled", + }, + { + desc: "iptables/ip6tables disabled", + enableIPv6: true, + enableIPMasquerade: true, + }, + { + desc: "host IP with iptables/ip6tables disabled", + enableIPv6: true, + enableIPMasquerade: true, + hostIPv4: hostIPv4, + }, + { + desc: "masquerade disabled, no host IP", + enableIPTables: true, + enableIP6Tables: true, + enableIPv6: true, + }, + { + desc: "masquerade disabled, with host IP", + enableIPTables: true, + enableIP6Tables: true, + enableIPv6: true, + hostIPv4: hostIPv4, + }, + { + desc: "IPv4 masquerade, IPv6 disabled", + enableIPTables: true, + enableIPMasquerade: true, + wantIPv4Masq: true, + }, + { + desc: "IPv4 SNAT, IPv6 disabled", + enableIPTables: true, + enableIPMasquerade: true, + hostIPv4: hostIPv4, + wantIPv4Snat: true, + }, + { + desc: "IPv4 masquerade, IPv6 masquerade", + enableIPTables: true, + enableIP6Tables: true, + enableIPv6: true, + enableIPMasquerade: true, + wantIPv4Masq: true, + wantIPv6Masq: true, + }, + { + desc: "IPv4 SNAT, IPv6 masquerade", + enableIPTables: true, + enableIP6Tables: true, + enableIPv6: true, + enableIPMasquerade: true, + hostIPv4: hostIPv4, + wantIPv4Snat: true, + wantIPv6Masq: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + defer netnsutils.SetupTestOSContext(t)() + dc := &configuration{ + EnableIPTables: tc.enableIPTables, + EnableIP6Tables: tc.enableIP6Tables, + } + r := &testRegisterer{t: t} + if err := Register(r, map[string]interface{}{netlabel.GenericData: dc}); err != nil { + t.Fatal(err) + } + if r.d == nil { + t.Fatal("testRegisterer.RegisterDriver never called") + } + nc := &networkConfiguration{ + BridgeName: br, + AddressIPv4: brIPv4, + AddressIPv6: brIPv6, + EnableIPv6: tc.enableIPv6, + EnableIPMasquerade: tc.enableIPMasquerade, + HostIPv4: tc.hostIPv4, + } + ipv4Data := []driverapi.IPAMData{{Pool: maskedBrIPv4, Gateway: brIPv4}} + ipv6Data := []driverapi.IPAMData{{Pool: maskedBrIPv6, Gateway: brIPv6}} + if !nc.EnableIPv6 { + nc.AddressIPv6 = nil + ipv6Data = nil + } + if err := r.d.CreateNetwork("nattest", map[string]interface{}{netlabel.GenericData: nc}, nil, ipv4Data, ipv6Data); err != nil { + t.Fatal(err) + } + defer func() { + if err := r.d.DeleteNetwork("nattest"); err != nil { + t.Fatal(err) + } + }() + // Log the contents of all chains to aid troubleshooting. + for _, ipv := range []iptables.IPVersion{iptables.IPv4, iptables.IPv6} { + ipt := iptables.GetIptable(ipv) + for _, table := range []iptables.Table{iptables.Nat, iptables.Filter, iptables.Mangle} { + out, err := ipt.Raw("-t", string(table), "-S") + if err != nil { + t.Error(err) + } + t.Logf("%s: %s %s table rules:\n%s", tc.desc, ipv, table, string(out)) + } + } + for _, rc := range []struct { + want bool + rule iptRule + }{ + // Rule order doesn't matter: At most one of the following IPv4 rules will exist, and the + // same goes for the IPv6 rules. + {tc.wantIPv4Masq, iptRule{iptables.IPv4, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv4.String(), "!", "-o", br, "-j", "MASQUERADE"}}}, + {tc.wantIPv4Snat, iptRule{iptables.IPv4, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv4.String(), "!", "-o", br, "-j", "SNAT", "--to-source", hostIPv4.String()}}}, + {tc.wantIPv6Masq, iptRule{iptables.IPv6, iptables.Nat, "POSTROUTING", []string{"-s", maskedBrIPv6.String(), "!", "-o", br, "-j", "MASQUERADE"}}}, + } { + assert.Equal(t, rc.rule.Exists(), rc.want) + } + }) + } +}