Files
moby/integration/network/network_linux_test.go
2025-10-30 23:45:18 -05:00

545 lines
23 KiB
Go

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")
})
}
}