package network import ( "bytes" "context" "fmt" "os/exec" "slices" "strings" "syscall" "testing" "time" networktypes "github.com/moby/moby/api/types/network" "github.com/moby/moby/client" "github.com/moby/moby/client/pkg/versions" "github.com/moby/moby/v2/daemon/libnetwork/netlabel" "github.com/moby/moby/v2/integration/internal/container" "github.com/moby/moby/v2/integration/internal/network" "github.com/moby/moby/v2/integration/internal/testutils/networking" "github.com/moby/moby/v2/internal/testutil" "github.com/moby/moby/v2/internal/testutil/daemon" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/icmd" "gotest.tools/v3/skip" ) func TestRunContainerWithBridgeNone(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon, "cannot start daemon on remote test run") skip.If(t, testEnv.IsUserNamespace) skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") ctx := testutil.StartSpan(baseContext, t) d := daemon.New(t) d.StartWithBusybox(ctx, t, "-b", "none") defer d.Stop(t) c := d.NewClientT(t) id1 := container.Run(ctx, t, c) defer c.ContainerRemove(ctx, id1, client.ContainerRemoveOptions{Force: true}) result, err := container.Exec(ctx, c, id1, []string{"ip", "l"}) assert.NilError(t, err) assert.Check(t, is.Equal(false, strings.Contains(result.Combined(), "eth0")), "There shouldn't be eth0 in container in default(bridge) mode when bridge network is disabled") id2 := container.Run(ctx, t, c, container.WithNetworkMode("bridge")) defer c.ContainerRemove(ctx, id2, client.ContainerRemoveOptions{Force: true}) result, err = container.Exec(ctx, c, id2, []string{"ip", "l"}) assert.NilError(t, err) assert.Check(t, is.Equal(false, strings.Contains(result.Combined(), "eth0")), "There shouldn't be eth0 in container in bridge mode when bridge network is disabled") nsCommand := "ls -l /proc/self/ns/net | awk -F '->' '{print $2}'" cmd := exec.Command("sh", "-c", nsCommand) stdout := bytes.NewBuffer(nil) cmd.Stdout = stdout err = cmd.Run() assert.NilError(t, err, "Failed to get current process network namespace: %+v", err) id3 := container.Run(ctx, t, c, container.WithNetworkMode("host")) defer c.ContainerRemove(ctx, id3, client.ContainerRemoveOptions{Force: true}) result, err = container.Exec(ctx, c, id3, []string{"sh", "-c", nsCommand}) assert.NilError(t, err) assert.Check(t, is.Equal(stdout.String(), result.Combined()), "The network namespace of container should be the same with host when --net=host and bridge network is disabled") } func TestHostIPv4BridgeLabel(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") ctx := testutil.StartSpan(baseContext, t) d := daemon.New(t) d.Start(t) defer d.Stop(t) c := d.NewClientT(t) defer c.Close() ipv4SNATAddr := "172.0.0.172" // Create a bridge network with --opt com.docker.network.host_ipv4=172.0.0.172 bridgeName := "hostIPv4Bridge" network.CreateNoError(ctx, t, c, bridgeName, network.WithDriver("bridge"), network.WithOption("com.docker.network.host_ipv4", ipv4SNATAddr), network.WithOption("com.docker.network.bridge.name", bridgeName), ) defer network.RemoveNoError(ctx, t, c, bridgeName) res, err := c.NetworkInspect(ctx, bridgeName, client.NetworkInspectOptions{Verbose: true}) assert.NilError(t, err) assert.Assert(t, len(res.Network.IPAM.Config) > 0) // Make sure the SNAT rule exists if strings.HasPrefix(testEnv.FirewallBackendDriver(), "nftables") { chain := testutil.RunCommand(ctx, "nft", "--stateless", "list", "chain", "ip", "docker-bridges", "nat-postrouting-out__hostIPv4Bridge").Combined() exp := fmt.Sprintf(`oifname != "hostIPv4Bridge" ip saddr %s counter snat to %s comment "SNAT"`, res.Network.IPAM.Config[0].Subnet, ipv4SNATAddr) assert.Check(t, is.Contains(chain, exp)) } else { testutil.RunCommand(ctx, "iptables", "-t", "nat", "-C", "POSTROUTING", "-s", res.Network.IPAM.Config[0].Subnet.String(), "!", "-o", bridgeName, "-j", "SNAT", "--to-source", ipv4SNATAddr).Assert(t, icmd.Success) } } func TestDefaultNetworkOpts(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") ctx := testutil.StartSpan(baseContext, t) tests := []struct { name string mtu int configFrom bool args []string }{ { name: "default value", mtu: 1500, args: []string{}, }, { name: "cmdline value", mtu: 1234, args: []string{"--default-network-opt", "bridge=com.docker.network.driver.mtu=1234"}, }, { name: "config-from value", configFrom: true, mtu: 1233, args: []string{"--default-network-opt", "bridge=com.docker.network.driver.mtu=1234"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { ctx := testutil.StartSpan(ctx, t) d := daemon.New(t) d.StartWithBusybox(ctx, t, tc.args...) defer d.Stop(t) c := d.NewClientT(t) defer c.Close() if tc.configFrom { // Create a new network config network.CreateNoError(ctx, t, c, "from-net", func(create *client.NetworkCreateOptions) { create.ConfigOnly = true create.Options = map[string]string{ "com.docker.network.driver.mtu": fmt.Sprint(tc.mtu), } }) defer c.NetworkRemove(ctx, "from-net", client.NetworkRemoveOptions{}) } // Create a new network networkName := "testnet" networkId := network.CreateNoError(ctx, t, c, networkName, func(create *client.NetworkCreateOptions) { if tc.configFrom { create.ConfigFrom = "from-net" } }) defer c.NetworkRemove(ctx, networkName, client.NetworkRemoveOptions{}) // Check the MTU of the bridge itself, before any devices are connected. (The // bridge's MTU will be set to the minimum MTU of anything connected to it, but // it's set explicitly on the bridge anyway - so it doesn't look like the option // was ignored.) cmd := exec.Command("ip", "link", "show", "br-"+networkId[:12]) output, err := cmd.CombinedOutput() assert.NilError(t, err) assert.Check(t, is.Contains(string(output), fmt.Sprintf(" mtu %d ", tc.mtu)), "Bridge MTU should have been set to %d", tc.mtu) // Start a container to inspect the MTU of its network interface id1 := container.Run(ctx, t, c, container.WithNetworkMode(networkName)) defer c.ContainerRemove(ctx, id1, client.ContainerRemoveOptions{Force: true}) result, err := container.Exec(ctx, c, id1, []string{"ip", "l", "show", "eth0"}) assert.NilError(t, err) assert.Check(t, is.Contains(result.Combined(), fmt.Sprintf(" mtu %d ", tc.mtu)), "Network MTU should have been set to %d", tc.mtu) }) } } func TestForbidDuplicateNetworkNames(t *testing.T) { ctx := testutil.StartSpan(baseContext, t) d := daemon.New(t) d.StartWithBusybox(ctx, t) defer d.Stop(t) c := d.NewClientT(t) defer c.Close() network.CreateNoError(ctx, t, c, "testnet") defer network.RemoveNoError(ctx, t, c, "testnet") _, err := c.NetworkCreate(ctx, "testnet", client.NetworkCreateOptions{}) assert.Error(t, err, "Error response from daemon: network with name testnet already exists", "2nd NetworkCreate call should have failed") } // TestHostGatewayFromDocker0 checks that, when docker0 has IPv6, host-gateway maps to both IPv4 and IPv6. func TestHostGatewayFromDocker0(t *testing.T) { ctx := testutil.StartSpan(baseContext, t) // Run the daemon in its own n/w namespace, to avoid interfering with // the docker0 bridge belonging to the daemon started by CI. const name = "host-gw-ips" l3 := networking.NewL3Segment(t, "test-"+name) defer l3.Destroy(t) l3.AddHost(t, "host-gw-ips", "host-gw-ips", "eth0") // 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=")) l3.Hosts[name].Do(t, func() { d.StartWithBusybox(ctx, t, "--ipv6", "--fixed-cidr", "192.168.50.0/24", "--fixed-cidr-v6", "fddd:6ff4:6e08::/64", ) }) defer d.Stop(t) c := d.NewClientT(t) defer c.Close() res := container.RunAttach(ctx, t, c, container.WithExtraHost("hg:host-gateway"), container.WithCmd("grep", "hg$", "/etc/hosts"), ) assert.Check(t, is.Equal(res.ExitCode, 0)) assert.Check(t, is.Contains(res.Stdout.String(), "192.168.50.1\thg")) assert.Check(t, is.Contains(res.Stdout.String(), "fddd:6ff4:6e08::1\thg")) } func TestCreateWithPriority(t *testing.T) { // This feature should work on Windows, but the test is skipped because: // 1. Linux-specific tools are used here; 2. 'windows' IPAM driver doesn't // support static allocations. skip.If(t, testEnv.DaemonInfo.OSType == "windows") skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.48"), "requires API v1.48") ctx := setupTest(t) apiClient := testEnv.APIClient() network.CreateNoError(ctx, t, apiClient, "testnet1", network.WithIPv6(), network.WithIPAM("10.100.20.0/24", "10.100.20.1"), network.WithIPAM("fd54:7a1b:8269::/64", "fd54:7a1b:8269::1")) defer network.RemoveNoError(ctx, t, apiClient, "testnet1") network.CreateNoError(ctx, t, apiClient, "testnet2", network.WithIPv6(), network.WithIPAM("10.100.30.0/24", "10.100.30.1"), network.WithIPAM("fdff:6dfe:37d2::/64", "fdff:6dfe:37d2::1")) defer network.RemoveNoError(ctx, t, apiClient, "testnet2") ctrID := container.Run(ctx, t, apiClient, container.WithCmd("sleep", "infinity"), container.WithNetworkMode("testnet1"), container.WithEndpointSettings("testnet1", &networktypes.EndpointSettings{GwPriority: 10}), container.WithEndpointSettings("testnet2", &networktypes.EndpointSettings{GwPriority: 100})) defer container.Remove(ctx, t, apiClient, ctrID, client.ContainerRemoveOptions{Force: true}) checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 3, "default via 10.100.30.1 dev") // IPv6 routing table will contain for each interface, one route for the LL // address, one for the ULA, and one multicast. checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 7, "default via fdff:6dfe:37d2::1 dev") } func TestConnectWithPriority(t *testing.T) { // This feature should work on Windows, but the test is skipped because: // 1. Linux-specific tools are used here; 2. 'windows' IPAM driver doesn't // support static allocations. skip.If(t, testEnv.DaemonInfo.OSType == "windows") skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.48"), "requires API v1.48") ctx := setupTest(t) apiClient := testEnv.APIClient() network.CreateNoError(ctx, t, apiClient, "testnet1", network.WithIPv6(), network.WithIPAM("10.100.10.0/24", "10.100.10.1"), network.WithIPAM("fddd:4901:f594::/64", "fddd:4901:f594::1")) defer network.RemoveNoError(ctx, t, apiClient, "testnet1") network.CreateNoError(ctx, t, apiClient, "testnet2", network.WithIPv6(), network.WithIPAM("10.100.20.0/24", "10.100.20.1"), network.WithIPAM("fd83:7683:7008::/64", "fd83:7683:7008::1")) defer network.RemoveNoError(ctx, t, apiClient, "testnet2") network.CreateNoError(ctx, t, apiClient, "testnet3", network.WithDriver("bridge"), network.WithIPv6(), network.WithIPAM("10.100.30.0/24", "10.100.30.1"), network.WithIPAM("fd72:de0:adad::/64", "fd72:de0:adad::1")) defer network.RemoveNoError(ctx, t, apiClient, "testnet3") network.CreateNoError(ctx, t, apiClient, "testnet4", network.WithIPv6(), network.WithIPAM("10.100.40.0/24", "10.100.40.1"), network.WithIPAM("fd4c:c927:7d90::/64", "fd4c:c927:7d90::1")) defer network.RemoveNoError(ctx, t, apiClient, "testnet4") network.CreateNoError(ctx, t, apiClient, "testnet5", network.WithIPv6(), network.WithIPAM("10.100.50.0/24", "10.100.50.1"), network.WithIPAM("fd4c:364b:1110::/64", "fd4c:364b:1110::1")) defer network.RemoveNoError(ctx, t, apiClient, "testnet5") ctrID := container.Run(ctx, t, apiClient, container.WithCmd("sleep", "infinity"), container.WithNetworkMode("testnet1"), container.WithEndpointSettings("testnet1", &networktypes.EndpointSettings{})) defer container.Remove(ctx, t, apiClient, ctrID, client.ContainerRemoveOptions{Force: true}) checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 2, "default via 10.100.10.1 dev eth0") checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 4, "default via fddd:4901:f594::1 dev eth0") // testnet5 has a negative priority -- the default gateway should not change. _, err := apiClient.NetworkConnect(ctx, "testnet5", client.NetworkConnectOptions{ Container: ctrID, EndpointConfig: &networktypes.EndpointSettings{GwPriority: -100}, }) assert.NilError(t, err) checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 3, "default via 10.100.10.1 dev eth0") checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 7, "default via fddd:4901:f594::1 dev eth0") // testnet2 has a higher priority. It should now provide the default gateway. _, err = apiClient.NetworkConnect(ctx, "testnet2", client.NetworkConnectOptions{ Container: ctrID, EndpointConfig: &networktypes.EndpointSettings{GwPriority: 100}, }) assert.NilError(t, err) checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 4, "default via 10.100.20.1 dev eth2") checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 10, "default via fd83:7683:7008::1 dev eth2") // testnet3 has a lower priority, so testnet2 should still provide the default gateway. _, err = apiClient.NetworkConnect(ctx, "testnet3", client.NetworkConnectOptions{ Container: ctrID, EndpointConfig: &networktypes.EndpointSettings{GwPriority: 10}, }) assert.NilError(t, err) checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 5, "default via 10.100.20.1 dev eth2") checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 13, "default via fd83:7683:7008::1 dev eth2") // testnet4 has the same priority as testnet3, but it sorts after in // lexicographic order. For now, testnet2 stays the default gateway. _, err = apiClient.NetworkConnect(ctx, "testnet4", client.NetworkConnectOptions{ Container: ctrID, EndpointConfig: &networktypes.EndpointSettings{GwPriority: 10}, }) assert.NilError(t, err) checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 6, "default via 10.100.20.1 dev eth2") checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 16, "default via fd83:7683:7008::1 dev eth2") inspect := container.Inspect(ctx, t, apiClient, ctrID) assert.Equal(t, inspect.NetworkSettings.Networks["testnet1"].GwPriority, 0) assert.Equal(t, inspect.NetworkSettings.Networks["testnet2"].GwPriority, 100) assert.Equal(t, inspect.NetworkSettings.Networks["testnet3"].GwPriority, 10) assert.Equal(t, inspect.NetworkSettings.Networks["testnet4"].GwPriority, 10) assert.Equal(t, inspect.NetworkSettings.Networks["testnet5"].GwPriority, -100) // Disconnect testnet2, so testnet3 should now provide the default gateway. // When two endpoints have the same priority (eg. testnet3 vs testnet4), // the one that sorts first in lexicographic order is picked. _, err = apiClient.NetworkDisconnect(ctx, "testnet2", client.NetworkDisconnectOptions{Container: ctrID, Force: true}) assert.NilError(t, err) checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 5, "default via 10.100.30.1 dev eth3") checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 13, "default via fd72:de0:adad::1 dev eth3") // Disconnect testnet3, so testnet4 should now provide the default gateway. _, err = apiClient.NetworkDisconnect(ctx, "testnet3", client.NetworkDisconnectOptions{Container: ctrID, Force: true}) assert.NilError(t, err) checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 4, "default via 10.100.40.1 dev eth4") checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 10, "default via fd4c:c927:7d90::1 dev eth4") // Disconnect testnet4, so testnet1 should now provide the default gateway. _, err = apiClient.NetworkDisconnect(ctx, "testnet4", client.NetworkDisconnectOptions{Container: ctrID, Force: true}) assert.NilError(t, err) checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET, 3, "default via 10.100.10.1 dev eth0") checkCtrRoutes(t, ctx, apiClient, ctrID, syscall.AF_INET6, 7, "default via fddd:4901:f594::1 dev eth0") } // checkCtrRoutes execute 'ip route show' in a container, and check that the // number of routes matches expRoutes. It also checks that the default route // matches expDefRoute. A substring match is used to avoid issues with // non-stable interface names. func checkCtrRoutes(t *testing.T, ctx context.Context, apiClient client.APIClient, ctrID string, af, expRoutes int, expDefRoute string) { t.Helper() fam := "-4" if af == syscall.AF_INET6 { fam = "-6" } ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() res, err := container.Exec(ctx, apiClient, ctrID, []string{"ip", "-o", fam, "route", "show"}) assert.NilError(t, err) assert.Equal(t, res.ExitCode, 0) assert.Equal(t, res.Stderr(), "") routes := slices.DeleteFunc(strings.Split(res.Stdout(), "\n"), func(s string) bool { return s == "" }) assert.Check(t, is.Equal(len(routes), expRoutes), "expected %d routes, got %d:\n%s", expRoutes, len(routes), strings.Join(routes, "\n")) if expDefRoute == "" { defFound := slices.ContainsFunc(routes, func(s string) bool { return strings.HasPrefix(s, "default") }) assert.Check(t, !defFound, "unexpected default route\n%s", strings.Join(routes, "\n")) } else { defFound := slices.ContainsFunc(routes, func(s string) bool { return strings.Contains(s, expDefRoute) }) assert.Check(t, defFound, "default route %q not found:\n%s", expDefRoute, strings.Join(routes, "\n")) } } // TestMixL3IPVlanAndBridge checks that a container can be connected to a layer-3 // ipvlan network as well as a bridge ... the bridge network will set up a // default gateway, if selected as the gateway endpoint. The ipvlan driver sets // up a connected route to 0.0.0.0 or [::], a route via a specific interface with // no next-hop address (because the next-hop address can't be ARP'd to determine // the interface). These two types of route cannot be set up at the same time. // So, the ipvlan's route must be treated like the default gateway and only get // set up when the ipvlan is selected as the gateway endpoint. // Regression test for https://github.com/moby/moby/issues/48576 func TestMixL3IPVlanAndBridge(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows", "no ipvlan on Windows") skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.48"), "gw-priority requires API v1.48") skip.If(t, testEnv.IsRootless, "can't see the dummy parent interface from the rootless namespace") ctx := testutil.StartSpan(baseContext, t) tests := []struct { name string liveRestore bool }{ { name: "no live restore", }, { // If the daemon is restarted with a running container, the osSbox structure // must be repopulated correctly in order for gateways to be removed then // re-added when network connections change. name: "live restore", liveRestore: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { ctx := testutil.StartSpan(ctx, t) d := daemon.New(t) var daemonArgs []string if tc.liveRestore { daemonArgs = append(daemonArgs, "--live-restore") } d.StartWithBusybox(ctx, t, daemonArgs...) defer d.Stop(t) c := d.NewClientT(t) defer c.Close() const br46NetName = "br46net" network.CreateNoError(ctx, t, c, br46NetName, network.WithOption(netlabel.ContainerIfacePrefix, "bds"), network.WithIPv6(), network.WithIPAM("192.168.123.0/24", "192.168.123.1"), network.WithIPAM("fd6f:36f8:3005::/64", "fd6f:36f8:3005::1"), ) defer network.RemoveNoError(ctx, t, c, br46NetName) const br6NetName = "br6net" network.CreateNoError(ctx, t, c, br6NetName, network.WithOption(netlabel.ContainerIfacePrefix, "bss"), network.WithIPv4(false), network.WithIPv6(), network.WithIPAM("fdc9:adaf:b5da::/64", "fdc9:adaf:b5da::1"), ) defer network.RemoveNoError(ctx, t, c, br6NetName) // Create a dummy parent interface rather than letting the driver do it because, // when the driver creates its own, it becomes a '--internal' network and no // default route is configured. const parentIfName = "di-dummy0" CreateMasterDummy(ctx, t, parentIfName) defer DeleteInterface(ctx, t, parentIfName) const ipvNetName = "ipvnet" network.CreateNoError(ctx, t, c, ipvNetName, network.WithDriver("ipvlan"), network.WithOption("ipvlan_mode", "l3"), network.WithOption("parent", parentIfName), network.WithIPv6(), network.WithIPAM("192.168.124.0/24", ""), network.WithIPAM("fd7d:8755:51ba::/64", ""), ) defer network.RemoveNoError(ctx, t, c, ipvNetName) // Create a container connected to all three networks, bridge network acting as gateway. ctrId := container.Run(ctx, t, c, container.WithNetworkMode(br46NetName), container.WithEndpointSettings(br46NetName, &networktypes.EndpointSettings{GwPriority: 1}, ), container.WithEndpointSettings(br6NetName, &networktypes.EndpointSettings{}), container.WithEndpointSettings(ipvNetName, &networktypes.EndpointSettings{}), ) defer container.Remove(ctx, t, c, ctrId, client.ContainerRemoveOptions{Force: true}) if tc.liveRestore { d.Restart(t, daemonArgs...) } // Expect three IPv4 routes: the default, plus one per network. checkCtrRoutes(t, ctx, c, ctrId, syscall.AF_INET, 3, "default via 192.168.123.1 dev bds") // Expect ten IPv6 routes: the default, plus UL, LL, and multicast routes per network. checkCtrRoutes(t, ctx, c, ctrId, syscall.AF_INET6, 10, "default via fd6f:36f8:3005::1 dev bds") // Disconnect the dual-stack bridge network, expect the ipvlan's default route to be set up. c.NetworkDisconnect(ctx, br46NetName, client.NetworkDisconnectOptions{Container: ctrId, Force: false}) checkCtrRoutes(t, ctx, c, ctrId, syscall.AF_INET, 2, "default dev eth") checkCtrRoutes(t, ctx, c, ctrId, syscall.AF_INET6, 7, "default dev eth") // Disconnect the ipvlan, expect the IPv6-only network to be the gateway, with no IPv4 gateway. // (For this to work in the live-restore case the "dstName" of the interface must have been // restored in the osSbox, based on matching the running interface's IPv6 address.) c.NetworkDisconnect(ctx, ipvNetName, client.NetworkDisconnectOptions{Container: ctrId, Force: false}) checkCtrRoutes(t, ctx, c, ctrId, syscall.AF_INET, 0, "") checkCtrRoutes(t, ctx, c, ctrId, syscall.AF_INET6, 4, "default via fdc9:adaf:b5da::1 dev bss") // Reconnect the dual-stack bridge, expect it to be the gateway for both addr families. c.NetworkConnect(ctx, br46NetName, client.NetworkConnectOptions{ Container: ctrId, EndpointConfig: &networktypes.EndpointSettings{GwPriority: 1}, }) checkCtrRoutes(t, ctx, c, ctrId, syscall.AF_INET, 2, "default via 192.168.123.1 dev bds") checkCtrRoutes(t, ctx, c, ctrId, syscall.AF_INET6, 7, "default via fd6f:36f8:3005::1 dev bds") }) } }