Selaa lähdekoodia

Merge pull request #46677 from rhansen/nat-test

bridge: Add unit tests for outgoing NAT rules
Sebastiaan van Stijn 1 vuosi sitten
vanhempi
commit
fc4d035e7a

+ 9 - 0
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()

+ 165 - 0
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)
+			}
+		})
+	}
+}