Browse code

Run tests that change docker0 in their own netns

These tests create iptables rules for different addresses on
docker0 but, unlike tests that do that for user-defined bridges,
those rules aren't removed when the test deletes the network,
because the default bridge network can't be deleted.

So, use (abuse) the L3Segment code to run the tests in their
own network namespace.

Signed-off-by: Rob Murray <rob.murray@docker.com>

Rob Murray authored on 2024/12/04 18:26:53
Showing 1 changed files
... ...
@@ -3,43 +3,45 @@ package daemon // import "github.com/docker/docker/integration/daemon"
3 3
 import (
4 4
 	"context"
5 5
 	"net"
6
+	"net/netip"
6 7
 	"testing"
7 8
 
8 9
 	"github.com/docker/docker/api/types/network"
10
+	"github.com/docker/docker/internal/nlwrap"
11
+	"github.com/docker/docker/internal/testutils/networking"
9 12
 	"github.com/docker/docker/testutil"
10 13
 	"github.com/docker/docker/testutil/daemon"
11 14
 	"github.com/vishvananda/netlink"
12 15
 	"gotest.tools/v3/assert"
13 16
 	is "gotest.tools/v3/assert/cmp"
14
-	"gotest.tools/v3/icmd"
15 17
 	"gotest.tools/v3/skip"
16 18
 )
17 19
 
20
+// Check that the daemon will start with fixed-cidr set, but no bip.
21
+// Regression test for https://github.com/moby/moby/issues/45356
18 22
 func TestDaemonDefaultBridgeWithFixedCidrButNoBip(t *testing.T) {
23
+	skip.If(t, testEnv.IsRootless, "can't create L3 segment in rootless namespace")
19 24
 	ctx := testutil.StartSpan(baseContext, t)
20 25
 
21
-	bridgeName := "ext-bridge1"
22
-	d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_CREATE_DEFAULT_BRIDGE="+bridgeName))
23
-	defer func() {
24
-		d.Stop(t)
25
-		d.Cleanup(t)
26
-	}()
26
+	host, cleanup := newHostInL3Seg(t, "fcnobip", "192.168.130.1", "fd69:d2cd:f7df::1")
27
+	defer cleanup()
27 28
 
28
-	defer func() {
29
-		// No need to clean up when running this test in rootless mode, as the
30
-		// interface is deleted when the daemon is stopped and the netns
31
-		// reclaimed by the kernel.
32
-		if !testEnv.IsRootless() {
33
-			deleteInterface(t, bridgeName)
34
-		}
35
-	}()
36
-	d.StartWithBusybox(ctx, t, "--bridge", bridgeName, "--fixed-cidr", "192.168.130.0/24")
29
+	host.Do(t, func() {
30
+		// Run without OTel because there's no routing from this netns for it - which
31
+		// means the daemon doesn't shut down cleanly, causing the test to fail.
32
+		d := daemon.New(t, daemon.WithEnvVars("OTEL_EXPORTER_OTLP_ENDPOINT="))
33
+		d.StartWithBusybox(ctx, t, "--fixed-cidr", "192.168.130.0/24")
34
+		defer func() {
35
+			d.Stop(t)
36
+			d.Cleanup(t)
37
+		}()
38
+	})
37 39
 }
38 40
 
39 41
 // Test fixed-cidr and bip options, with various addresses on the bridge
40 42
 // before the daemon starts.
41 43
 func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) {
42
-	skip.If(t, testEnv.IsRootless, "can't create test bridge in rootless namespace")
44
+	skip.If(t, testEnv.IsRootless, "can't create L3 segment in rootless namespace")
43 45
 	ctx := testutil.StartSpan(baseContext, t)
44 46
 
45 47
 	testcases := []defaultBridgeIPAMTestCase{
... ...
@@ -168,6 +170,7 @@ func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) {
168 168
 		},
169 169
 	}
170 170
 	for _, tc := range testcases {
171
+		tc.bridgeName = "docker0"
171 172
 		testDefaultBridgeIPAM(ctx, t, tc)
172 173
 	}
173 174
 }
... ...
@@ -175,7 +178,7 @@ func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) {
175 175
 // Like TestDaemonUserDefaultBridgeIPAMDocker0, but with a user-defined/supplied
176 176
 // bridge, instead of docker0.
177 177
 func TestDaemonDefaultBridgeIPAM_UserBr(t *testing.T) {
178
-	skip.If(t, testEnv.IsRootless, "can't create test bridge in rootless namespace")
178
+	skip.If(t, testEnv.IsRootless, "can't create L3 segment in rootless namespace")
179 179
 	ctx := testutil.StartSpan(baseContext, t)
180 180
 
181 181
 	testcases := []defaultBridgeIPAMTestCase{
... ...
@@ -301,6 +304,7 @@ func TestDaemonDefaultBridgeIPAM_UserBr(t *testing.T) {
301 301
 		},
302 302
 	}
303 303
 	for _, tc := range testcases {
304
+		tc.bridgeName = "br-dbi"
304 305
 		tc.userDefinedBridge = true
305 306
 		testDefaultBridgeIPAM(ctx, t, tc)
306 307
 	}
... ...
@@ -308,6 +312,7 @@ func TestDaemonDefaultBridgeIPAM_UserBr(t *testing.T) {
308 308
 
309 309
 type defaultBridgeIPAMTestCase struct {
310 310
 	name               string
311
+	bridgeName         string
311 312
 	userDefinedBridge  bool
312 313
 	initialBridgeAddrs []string
313 314
 	daemonArgs         []string
... ...
@@ -319,71 +324,89 @@ type defaultBridgeIPAMTestCase struct {
319 319
 func testDefaultBridgeIPAM(ctx context.Context, t *testing.T, tc defaultBridgeIPAMTestCase) {
320 320
 	t.Run(tc.name, func(t *testing.T) {
321 321
 		ctx := testutil.StartSpan(ctx, t)
322
-		const bridgeName = "br-dbi"
323 322
 
324
-		createBridge(t, bridgeName, tc.initialBridgeAddrs)
325
-		defer deleteInterface(t, bridgeName)
323
+		// Run this test in its own network namespace because it messes with the default
324
+		// bridge and, for example, iptables rules for the default bridge aren't deleted
325
+		// because the network can't be deleted. Then, rules with old addresses may break
326
+		// unrelated tests.
327
+		host, cleanup := newHostInL3Seg(t, "defbripam", "192.168.131.1", "fdf9:2eb5:ba8c::1")
328
+		defer cleanup()
326 329
 
327
-		var dOpts []daemon.Option
328
-		var dArgs []string
329
-		if !tc.ipv4Only {
330
-			dArgs = append(tc.daemonArgs, "--ipv6")
331
-		}
332
-		if tc.userDefinedBridge {
333
-			// If a bridge is supplied by the user, the daemon should use its addresses
334
-			// to infer --bip (which cannot be specified).
335
-			dArgs = append(dArgs, "--bridge", bridgeName)
336
-		} else {
337
-			// The bridge is created and managed by docker, it's always called "docker0",
338
-			// unless this test-only env var is set - to avoid conflict with the docker0
339
-			// belonging to the daemon started in CI runs.
340
-			dOpts = append(dOpts, daemon.WithEnvVars("DOCKER_TEST_CREATE_DEFAULT_BRIDGE="+bridgeName))
341
-		}
330
+		host.Do(t, func() {
331
+			createBridge(t, tc.bridgeName, tc.initialBridgeAddrs)
342 332
 
343
-		d := daemon.New(t, dOpts...)
344
-		defer func() {
345
-			d.Stop(t)
346
-			d.Cleanup(t)
347
-		}()
333
+			var dArgs []string
334
+			if !tc.ipv4Only {
335
+				dArgs = append(tc.daemonArgs, "--ipv6")
336
+			}
337
+			if tc.userDefinedBridge {
338
+				// If a bridge is supplied by the user, the daemon should use its addresses
339
+				// to infer --bip (which cannot be specified).
340
+				dArgs = append(dArgs, "--bridge", tc.bridgeName)
341
+			}
348 342
 
349
-		if tc.expStartErr {
350
-			err := d.StartWithError(dArgs...)
351
-			assert.Check(t, is.ErrorContains(err, "daemon exited during startup"))
352
-			return
353
-		}
343
+			// Run without OTel because there's no routing from this netns for it - which
344
+			// means the daemon doesn't shut down cleanly, causing the test to fail.
345
+			d := daemon.New(t, daemon.WithEnvVars("OTEL_EXPORTER_OTLP_ENDPOINT="))
346
+			defer func() {
347
+				d.Stop(t)
348
+				d.Cleanup(t)
349
+			}()
354 350
 
355
-		d.Start(t, dArgs...)
356
-		c := d.NewClientT(t)
357
-		defer c.Close()
351
+			if tc.expStartErr {
352
+				err := d.StartWithError(dArgs...)
353
+				assert.Check(t, is.ErrorContains(err, "daemon exited during startup"))
354
+				return
355
+			}
358 356
 
359
-		insp, err := c.NetworkInspect(ctx, network.NetworkBridge, network.InspectOptions{})
360
-		assert.NilError(t, err)
361
-		assert.Check(t, is.DeepEqual(insp.IPAM.Config, tc.expIPAMConfig))
357
+			d.Start(t, dArgs...)
358
+			c := d.NewClientT(t)
359
+			defer c.Close()
360
+
361
+			insp, err := c.NetworkInspect(ctx, network.NetworkBridge, network.InspectOptions{})
362
+			assert.NilError(t, err)
363
+			assert.Check(t, is.DeepEqual(insp.IPAM.Config, tc.expIPAMConfig))
364
+		})
362 365
 	})
363 366
 }
364 367
 
368
+func newHostInL3Seg(t *testing.T, name, ip4, ip6 string) (networking.Host, func()) {
369
+	// Set up a netns for each test to avoid sysctl and iptables pollution.
370
+	addr4 := netip.MustParseAddr(ip4)
371
+	addr6 := netip.MustParseAddr(ip6)
372
+	l3 := networking.NewL3Segment(t, "test-"+name,
373
+		netip.PrefixFrom(addr4, 24),
374
+		netip.PrefixFrom(addr6, 64),
375
+	)
376
+	hostname := "host-" + name
377
+	l3.AddHost(t, hostname, "ns-"+name, "eth0",
378
+		netip.PrefixFrom(addr4.Next(), 24),
379
+		netip.PrefixFrom(addr6.Next(), 64),
380
+	)
381
+	return l3.Hosts[hostname], func() { l3.Destroy(t) }
382
+}
383
+
365 384
 func createBridge(t *testing.T, ifName string, addrs []string) {
366 385
 	t.Helper()
367 386
 
387
+	// Get a netlink handle in this netns.
388
+	nlh, err := nlwrap.NewHandle()
389
+	assert.NilError(t, err)
390
+	defer nlh.Close()
391
+
368 392
 	link := &netlink.Bridge{
369 393
 		LinkAttrs: netlink.LinkAttrs{
370 394
 			Name: ifName,
371 395
 		},
372 396
 	}
373 397
 
374
-	err := netlink.LinkAdd(link)
398
+	err = nlh.LinkAdd(link)
375 399
 	assert.NilError(t, err)
376 400
 	for _, addr := range addrs {
377 401
 		ip, ipNet, err := net.ParseCIDR(addr)
378 402
 		assert.NilError(t, err)
379 403
 		ipNet.IP = ip
380
-		err = netlink.AddrAdd(link, &netlink.Addr{IPNet: ipNet})
404
+		err = nlh.AddrAdd(link, &netlink.Addr{IPNet: ipNet})
381 405
 		assert.NilError(t, err)
382 406
 	}
383 407
 }
384
-
385
-func deleteInterface(t *testing.T, ifName string) {
386
-	icmd.RunCommand("ip", "link", "delete", ifName).Assert(t, icmd.Success)
387
-	icmd.RunCommand("iptables", "-t", "nat", "--flush").Assert(t, icmd.Success)
388
-	icmd.RunCommand("iptables", "--flush").Assert(t, icmd.Success)
389
-}