github.com/moby/docker@v26.1.3+incompatible/integration/network/macvlan/macvlan_test.go (about)

     1  //go:build !windows
     2  
     3  package macvlan // import "github.com/docker/docker/integration/network/macvlan"
     4  
     5  import (
     6  	"context"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/docker/docker/api/types"
    11  	containertypes "github.com/docker/docker/api/types/container"
    12  	"github.com/docker/docker/client"
    13  	"github.com/docker/docker/integration/internal/container"
    14  	net "github.com/docker/docker/integration/internal/network"
    15  	n "github.com/docker/docker/integration/network"
    16  	"github.com/docker/docker/testutil"
    17  	"github.com/docker/docker/testutil/daemon"
    18  	"gotest.tools/v3/assert"
    19  	is "gotest.tools/v3/assert/cmp"
    20  	"gotest.tools/v3/skip"
    21  )
    22  
    23  func TestDockerNetworkMacvlanPersistance(t *testing.T) {
    24  	// verify the driver automatically provisions the 802.1q link (dm-dummy0.60)
    25  	skip.If(t, testEnv.IsRemoteDaemon)
    26  	skip.If(t, testEnv.IsRootless, "rootless mode has different view of network")
    27  
    28  	ctx := testutil.StartSpan(baseContext, t)
    29  
    30  	d := daemon.New(t)
    31  	d.StartWithBusybox(ctx, t)
    32  	defer d.Stop(t)
    33  
    34  	master := "dm-dummy0"
    35  	n.CreateMasterDummy(ctx, t, master)
    36  	defer n.DeleteInterface(ctx, t, master)
    37  
    38  	c := d.NewClientT(t)
    39  
    40  	netName := "dm-persist"
    41  	net.CreateNoError(ctx, t, c, netName,
    42  		net.WithMacvlan("dm-dummy0.60"),
    43  	)
    44  	assert.Check(t, n.IsNetworkAvailable(ctx, c, netName))
    45  	d.Restart(t)
    46  	assert.Check(t, n.IsNetworkAvailable(ctx, c, netName))
    47  }
    48  
    49  func TestDockerNetworkMacvlan(t *testing.T) {
    50  	skip.If(t, testEnv.IsRemoteDaemon)
    51  	skip.If(t, testEnv.IsRootless, "rootless mode has different view of network")
    52  
    53  	ctx := testutil.StartSpan(baseContext, t)
    54  
    55  	for _, tc := range []struct {
    56  		name string
    57  		test func(*testing.T, context.Context, client.APIClient)
    58  	}{
    59  		{
    60  			name: "Subinterface",
    61  			test: testMacvlanSubinterface,
    62  		}, {
    63  			name: "OverlapParent",
    64  			test: testMacvlanOverlapParent,
    65  		}, {
    66  			name: "NilParent",
    67  			test: testMacvlanNilParent,
    68  		}, {
    69  			name: "InternalMode",
    70  			test: testMacvlanInternalMode,
    71  		}, {
    72  			name: "MultiSubnetWithParent",
    73  			test: testMacvlanMultiSubnetWithParent,
    74  		}, {
    75  			name: "MultiSubnetNoParent",
    76  			test: testMacvlanMultiSubnetNoParent,
    77  		}, {
    78  			name: "Addressing",
    79  			test: testMacvlanAddressing,
    80  		}, {
    81  			name: "NoIPv6",
    82  			test: testMacvlanNoIPv6,
    83  		},
    84  	} {
    85  		tc := tc
    86  		t.Run(tc.name, func(t *testing.T) {
    87  			testutil.StartSpan(ctx, t)
    88  
    89  			d := daemon.New(t)
    90  			t.Cleanup(func() { d.Stop(t) })
    91  			d.StartWithBusybox(ctx, t)
    92  			c := d.NewClientT(t)
    93  
    94  			tc.test(t, ctx, c)
    95  		})
    96  
    97  		// FIXME(vdemeester) clean network
    98  	}
    99  }
   100  
   101  func testMacvlanOverlapParent(t *testing.T, ctx context.Context, client client.APIClient) {
   102  	// verify the same parent interface cannot be used if already in use by an existing network
   103  	master := "dm-dummy0"
   104  	n.CreateMasterDummy(ctx, t, master)
   105  	defer n.DeleteInterface(ctx, t, master)
   106  
   107  	netName := "dm-subinterface"
   108  	parentName := "dm-dummy0.40"
   109  	net.CreateNoError(ctx, t, client, netName,
   110  		net.WithMacvlan(parentName),
   111  	)
   112  	assert.Check(t, n.IsNetworkAvailable(ctx, client, netName))
   113  
   114  	_, err := net.Create(ctx, client, "dm-parent-net-overlap",
   115  		net.WithMacvlan(parentName),
   116  	)
   117  	assert.Check(t, err != nil)
   118  
   119  	// delete the network while preserving the parent link
   120  	err = client.NetworkRemove(ctx, netName)
   121  	assert.NilError(t, err)
   122  
   123  	assert.Check(t, n.IsNetworkNotAvailable(ctx, client, netName))
   124  	// verify the network delete did not delete the predefined link
   125  	n.LinkExists(ctx, t, master)
   126  }
   127  
   128  func testMacvlanSubinterface(t *testing.T, ctx context.Context, client client.APIClient) {
   129  	// verify the same parent interface cannot be used if already in use by an existing network
   130  	master := "dm-dummy0"
   131  	parentName := "dm-dummy0.20"
   132  	n.CreateMasterDummy(ctx, t, master)
   133  	defer n.DeleteInterface(ctx, t, master)
   134  	n.CreateVlanInterface(ctx, t, master, parentName, "20")
   135  
   136  	netName := "dm-subinterface"
   137  	net.CreateNoError(ctx, t, client, netName,
   138  		net.WithMacvlan(parentName),
   139  	)
   140  	assert.Check(t, n.IsNetworkAvailable(ctx, client, netName))
   141  
   142  	// delete the network while preserving the parent link
   143  	err := client.NetworkRemove(ctx, netName)
   144  	assert.NilError(t, err)
   145  
   146  	assert.Check(t, n.IsNetworkNotAvailable(ctx, client, netName))
   147  	// verify the network delete did not delete the predefined link
   148  	n.LinkExists(ctx, t, parentName)
   149  }
   150  
   151  func testMacvlanNilParent(t *testing.T, ctx context.Context, client client.APIClient) {
   152  	// macvlan bridge mode - dummy parent interface is provisioned dynamically
   153  	netName := "dm-nil-parent"
   154  	net.CreateNoError(ctx, t, client, netName,
   155  		net.WithMacvlan(""),
   156  	)
   157  	assert.Check(t, n.IsNetworkAvailable(ctx, client, netName))
   158  
   159  	id1 := container.Run(ctx, t, client, container.WithNetworkMode(netName))
   160  	id2 := container.Run(ctx, t, client, container.WithNetworkMode(netName))
   161  
   162  	_, err := container.Exec(ctx, client, id2, []string{"ping", "-c", "1", id1})
   163  	assert.Check(t, err == nil)
   164  }
   165  
   166  func testMacvlanInternalMode(t *testing.T, ctx context.Context, client client.APIClient) {
   167  	// macvlan bridge mode - dummy parent interface is provisioned dynamically
   168  	netName := "dm-internal"
   169  	net.CreateNoError(ctx, t, client, netName,
   170  		net.WithMacvlan(""),
   171  		net.WithInternal(),
   172  	)
   173  	assert.Check(t, n.IsNetworkAvailable(ctx, client, netName))
   174  
   175  	id1 := container.Run(ctx, t, client, container.WithNetworkMode(netName))
   176  	id2 := container.Run(ctx, t, client, container.WithNetworkMode(netName))
   177  
   178  	result, _ := container.Exec(ctx, client, id1, []string{"ping", "-c", "1", "8.8.8.8"})
   179  	assert.Check(t, strings.Contains(result.Combined(), "Network is unreachable"))
   180  
   181  	_, err := container.Exec(ctx, client, id2, []string{"ping", "-c", "1", id1})
   182  	assert.Check(t, err == nil)
   183  }
   184  
   185  func testMacvlanMultiSubnetWithParent(t *testing.T, ctx context.Context, client client.APIClient) {
   186  	const parentIfName = "dm-dummy0"
   187  	n.CreateMasterDummy(ctx, t, parentIfName)
   188  	defer n.DeleteInterface(ctx, t, parentIfName)
   189  	testMacvlanMultiSubnet(t, ctx, client, parentIfName)
   190  }
   191  
   192  func testMacvlanMultiSubnetNoParent(t *testing.T, ctx context.Context, client client.APIClient) {
   193  	testMacvlanMultiSubnet(t, ctx, client, "")
   194  }
   195  
   196  func testMacvlanMultiSubnet(t *testing.T, ctx context.Context, client client.APIClient, parent string) {
   197  	netName := "dualstackbridge"
   198  	net.CreateNoError(ctx, t, client, netName,
   199  		net.WithMacvlan(parent),
   200  		net.WithIPv6(),
   201  		net.WithIPAM("172.28.100.0/24", ""),
   202  		net.WithIPAM("172.28.102.0/24", "172.28.102.254"),
   203  		net.WithIPAM("2001:db8:abc2::/64", ""),
   204  		net.WithIPAM("2001:db8:abc4::/64", "2001:db8:abc4::254"),
   205  	)
   206  
   207  	assert.Check(t, n.IsNetworkAvailable(ctx, client, netName))
   208  
   209  	// start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.100.0/24 and 2001:db8:abc2::/64
   210  	id1 := container.Run(ctx, t, client,
   211  		container.WithNetworkMode("dualstackbridge"),
   212  		container.WithIPv4("dualstackbridge", "172.28.100.20"),
   213  		container.WithIPv6("dualstackbridge", "2001:db8:abc2::20"),
   214  	)
   215  	id2 := container.Run(ctx, t, client,
   216  		container.WithNetworkMode("dualstackbridge"),
   217  		container.WithIPv4("dualstackbridge", "172.28.100.21"),
   218  		container.WithIPv6("dualstackbridge", "2001:db8:abc2::21"),
   219  	)
   220  	c1, err := client.ContainerInspect(ctx, id1)
   221  	assert.NilError(t, err)
   222  	if parent == "" {
   223  		// Inspect the v4 gateway to ensure no default GW was assigned
   224  		assert.Check(t, is.Equal(c1.NetworkSettings.Networks["dualstackbridge"].Gateway, ""))
   225  		// Inspect the v6 gateway to ensure no default GW was assigned
   226  		assert.Check(t, is.Equal(c1.NetworkSettings.Networks["dualstackbridge"].IPv6Gateway, ""))
   227  	} else {
   228  		// Inspect the v4 gateway to ensure the proper default GW was assigned
   229  		assert.Check(t, is.Equal(c1.NetworkSettings.Networks["dualstackbridge"].Gateway, "172.28.100.1"))
   230  		// Inspect the v6 gateway to ensure the proper default GW was assigned
   231  		assert.Check(t, is.Equal(c1.NetworkSettings.Networks["dualstackbridge"].IPv6Gateway, "2001:db8:abc2::1"))
   232  	}
   233  
   234  	// verify ipv4 connectivity to the explicit --ip address second to first
   235  	_, err = container.Exec(ctx, client, id2, []string{"ping", "-c", "1", c1.NetworkSettings.Networks["dualstackbridge"].IPAddress})
   236  	assert.NilError(t, err)
   237  	// verify ipv6 connectivity to the explicit --ip6 address second to first
   238  	_, err = container.Exec(ctx, client, id2, []string{"ping6", "-c", "1", c1.NetworkSettings.Networks["dualstackbridge"].GlobalIPv6Address})
   239  	assert.NilError(t, err)
   240  
   241  	// start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.102.0/24 and 2001:db8:abc4::/64
   242  	id3 := container.Run(ctx, t, client,
   243  		container.WithNetworkMode("dualstackbridge"),
   244  		container.WithIPv4("dualstackbridge", "172.28.102.20"),
   245  		container.WithIPv6("dualstackbridge", "2001:db8:abc4::20"),
   246  	)
   247  	id4 := container.Run(ctx, t, client,
   248  		container.WithNetworkMode("dualstackbridge"),
   249  		container.WithIPv4("dualstackbridge", "172.28.102.21"),
   250  		container.WithIPv6("dualstackbridge", "2001:db8:abc4::21"),
   251  	)
   252  	c3, err := client.ContainerInspect(ctx, id3)
   253  	assert.NilError(t, err)
   254  	if parent == "" {
   255  		// Inspect the v4 gateway to ensure no default GW was assigned
   256  		assert.Check(t, is.Equal(c3.NetworkSettings.Networks["dualstackbridge"].Gateway, ""))
   257  		// Inspect the v6 gateway to ensure no default GW was assigned
   258  		assert.Check(t, is.Equal(c3.NetworkSettings.Networks["dualstackbridge"].IPv6Gateway, ""))
   259  	} else {
   260  		// Inspect the v4 gateway to ensure the proper explicitly assigned default GW was assigned
   261  		assert.Check(t, is.Equal(c3.NetworkSettings.Networks["dualstackbridge"].Gateway, "172.28.102.254"))
   262  		// Inspect the v6 gateway to ensure the proper explicitly assigned default GW was assigned
   263  		assert.Check(t, is.Equal(c3.NetworkSettings.Networks["dualstackbridge"].IPv6Gateway, "2001:db8:abc4::254"))
   264  	}
   265  
   266  	// verify ipv4 connectivity to the explicit --ip address from third to fourth
   267  	_, err = container.Exec(ctx, client, id4, []string{"ping", "-c", "1", c3.NetworkSettings.Networks["dualstackbridge"].IPAddress})
   268  	assert.NilError(t, err)
   269  	// verify ipv6 connectivity to the explicit --ip6 address from third to fourth
   270  	_, err = container.Exec(ctx, client, id4, []string{"ping6", "-c", "1", c3.NetworkSettings.Networks["dualstackbridge"].GlobalIPv6Address})
   271  	assert.NilError(t, err)
   272  }
   273  
   274  func testMacvlanAddressing(t *testing.T, ctx context.Context, client client.APIClient) {
   275  	const parentIfName = "dm-dummy0"
   276  	n.CreateMasterDummy(ctx, t, parentIfName)
   277  	defer n.DeleteInterface(ctx, t, parentIfName)
   278  
   279  	// Ensure the default gateways, next-hops and default dev devices are properly set
   280  	netName := "dualstackbridge"
   281  	net.CreateNoError(ctx, t, client, netName,
   282  		net.WithMacvlan(parentIfName),
   283  		net.WithIPv6(),
   284  		net.WithOption("macvlan_mode", "bridge"),
   285  		net.WithIPAM("172.28.130.0/24", ""),
   286  		net.WithIPAM("2001:db8:abca::/64", "2001:db8:abca::254"),
   287  	)
   288  	assert.Check(t, n.IsNetworkAvailable(ctx, client, netName))
   289  
   290  	id1 := container.Run(ctx, t, client,
   291  		container.WithNetworkMode("dualstackbridge"),
   292  	)
   293  
   294  	// Validate macvlan bridge mode defaults gateway sets the default IPAM next-hop inferred from the subnet
   295  	result, err := container.Exec(ctx, client, id1, []string{"ip", "route"})
   296  	assert.NilError(t, err)
   297  	assert.Check(t, strings.Contains(result.Combined(), "default via 172.28.130.1 dev eth0"))
   298  	// Validate macvlan bridge mode sets the v6 gateway to the user specified default gateway/next-hop
   299  	result, err = container.Exec(ctx, client, id1, []string{"ip", "-6", "route"})
   300  	assert.NilError(t, err)
   301  	assert.Check(t, strings.Contains(result.Combined(), "default via 2001:db8:abca::254 dev eth0"))
   302  }
   303  
   304  // Check that a macvlan interface with '--ipv6=false' doesn't get kernel-assigned
   305  // IPv6 addresses, but the loopback interface does still have an IPv6 address ('::1').
   306  func testMacvlanNoIPv6(t *testing.T, ctx context.Context, client client.APIClient) {
   307  	const netName = "macvlannet"
   308  
   309  	net.CreateNoError(ctx, t, client, netName,
   310  		net.WithMacvlan(""),
   311  		net.WithOption("macvlan_mode", "bridge"),
   312  	)
   313  	assert.Check(t, n.IsNetworkAvailable(ctx, client, netName))
   314  
   315  	id := container.Run(ctx, t, client, container.WithNetworkMode(netName))
   316  
   317  	loRes := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "lo"})
   318  	assert.Check(t, is.Contains(loRes.Combined(), " inet "))
   319  	assert.Check(t, is.Contains(loRes.Combined(), " inet6 "))
   320  
   321  	eth0Res := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "eth0"})
   322  	assert.Check(t, is.Contains(eth0Res.Combined(), " inet "))
   323  	assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "),
   324  		"result.Combined(): %s", eth0Res.Combined())
   325  
   326  	sysctlRes := container.ExecT(ctx, t, client, id, []string{"sysctl", "-n", "net.ipv6.conf.eth0.disable_ipv6"})
   327  	assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), "1"))
   328  }
   329  
   330  // TestMACVlanDNS checks whether DNS is forwarded, with/without a parent
   331  // interface, and with '--internal'. Note that there's no attempt here to give
   332  // the macvlan network external connectivity - when this test supplies a parent
   333  // interface, it's a dummy. External DNS lookups only work because the daemon is
   334  // configured to see a host resolver on a loopback interface, so the external DNS
   335  // lookup happens in the host's namespace. The test is checking that an
   336  // automatically configured dummy interface causes the network to behave as if it
   337  // was '--internal'.
   338  func TestMACVlanDNS(t *testing.T) {
   339  	skip.If(t, testEnv.IsRootless, "rootless mode has different view of network")
   340  
   341  	ctx := testutil.StartSpan(baseContext, t)
   342  
   343  	net.StartDaftDNS(t, "127.0.0.1")
   344  
   345  	tmpFileName := net.WriteTempResolvConf(t, "127.0.0.1")
   346  	d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_RESOLV_CONF_PATH="+tmpFileName))
   347  	d.StartWithBusybox(ctx, t)
   348  	t.Cleanup(func() { d.Stop(t) })
   349  	c := d.NewClientT(t)
   350  
   351  	const parentIfName = "dm-dummy0"
   352  	n.CreateMasterDummy(ctx, t, parentIfName)
   353  	defer n.DeleteInterface(ctx, t, parentIfName)
   354  
   355  	const netName = "macvlan-dns-net"
   356  
   357  	testcases := []struct {
   358  		name     string
   359  		parent   string
   360  		internal bool
   361  		expDNS   bool
   362  	}{
   363  		{
   364  			name:   "with parent",
   365  			parent: parentIfName,
   366  			// External DNS should be used (even though the network has no external connectivity).
   367  			expDNS: true,
   368  		},
   369  		{
   370  			name: "no parent",
   371  			// External DNS should not be used, equivalent to '--internal'.
   372  		},
   373  		{
   374  			name:     "with parent, internal",
   375  			parent:   parentIfName,
   376  			internal: true,
   377  			expDNS:   false,
   378  		},
   379  	}
   380  
   381  	for _, tc := range testcases {
   382  		t.Run(tc.name, func(t *testing.T) {
   383  			ctx := testutil.StartSpan(ctx, t)
   384  			createOpts := []func(*types.NetworkCreate){
   385  				net.WithMacvlan(tc.parent),
   386  			}
   387  			if tc.internal {
   388  				createOpts = append(createOpts, net.WithInternal())
   389  			}
   390  			net.CreateNoError(ctx, t, c, netName, createOpts...)
   391  			defer c.NetworkRemove(ctx, netName)
   392  
   393  			ctrId := container.Run(ctx, t, c, container.WithNetworkMode(netName))
   394  			defer c.ContainerRemove(ctx, ctrId, containertypes.RemoveOptions{Force: true})
   395  			res, err := container.Exec(ctx, c, ctrId, []string{"nslookup", "test.example"})
   396  			assert.NilError(t, err)
   397  			if tc.expDNS {
   398  				assert.Check(t, is.Equal(res.ExitCode, 0))
   399  				assert.Check(t, is.Contains(res.Stdout(), net.DNSRespAddr))
   400  			} else {
   401  				assert.Check(t, is.Equal(res.ExitCode, 1))
   402  				assert.Check(t, is.Contains(res.Stdout(), "SERVFAIL"))
   403  			}
   404  		})
   405  	}
   406  }