Files
moby/integration/daemon/daemon_linux_test.go
Albin Kerouanton d087d3c057 daemon: close EventsService on shutdown
On daemon shutdown, the HTTP server tries to gracefully shutdown for 5
seconds. If there's an open API connection to the '/events' endpoint, it
fails to do so as nothing interrupts that connection, thus forcing the
daemon to wait until that timeout is reached.

Add a Close method to the EventsService, and call it during daemon
shutdown. It'll close any events channel, signaling to the '/events'
handler to return and close the connection.

It now takes ~1s (or less) to shutdown the daemon when there's an active
'/events' connection, instead of 5.

Signed-off-by: Albin Kerouanton <albin.kerouanton@docker.com>
2025-11-09 22:38:59 +01:00

561 lines
23 KiB
Go

package daemon
import (
"context"
"net"
"net/netip"
"slices"
"testing"
"time"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/network"
swarmtypes "github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/moby/moby/v2/daemon/libnetwork/nlwrap"
"github.com/moby/moby/v2/integration/internal/testutils/networking"
"github.com/moby/moby/v2/internal/testutil"
"github.com/moby/moby/v2/internal/testutil/daemon"
"github.com/vishvananda/netlink"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/poll"
"gotest.tools/v3/skip"
)
// Check that the daemon will start with fixed-cidr set, but no bip.
// Regression test for https://github.com/moby/moby/issues/45356
func TestDaemonDefaultBridgeWithFixedCidrButNoBip(t *testing.T) {
skip.If(t, testEnv.IsRootless, "can't create L3 segment in rootless namespace")
ctx := testutil.StartSpan(baseContext, t)
host, cleanup := newHostInL3Seg(t, "fcnobip", "192.168.130.1", "fd69:d2cd:f7df::1")
defer cleanup()
host.Do(t, func() {
// 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="))
d.StartWithBusybox(ctx, t, "--fixed-cidr", "192.168.130.0/24")
defer func() {
d.Stop(t)
d.Cleanup(t)
}()
})
}
// Test fixed-cidr and bip options, with various addresses on the bridge
// before the daemon starts.
func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) {
skip.If(t, testEnv.IsRootless, "can't create L3 segment in rootless namespace")
ctx := testutil.StartSpan(baseContext, t)
testcases := []defaultBridgeIPAMTestCase{
{
name: "no config",
// No config for the bridge, but override default-address-pools to
// get a predictable result for IPv6 (rather than the daemon's ULA).
daemonArgs: []string{
"--default-address-pool", `base=192.168.176.0/20,size=24`,
"--default-address-pool", `base=fdd1:8161:2d2c::/56,size=64`,
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::1")},
},
},
{
name: "fixed-cidr only",
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/24",
"--fixed-cidr-v6", "fdd1:8161:2d2c::/64",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::1")},
},
},
{
name: "bip only",
daemonArgs: []string{
"--bip", "192.168.176.88/24",
"--bip6", "fdd1:8161:2d2c::8888/64",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::8888")},
},
},
{
name: "existing bridge address only",
initialBridgeAddrs: []string{"192.168.176.88/24", "fdd1:8161:2d2c::8888/64"},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::8888")},
},
},
{
name: "fixed-cidr within old bridge subnet",
initialBridgeAddrs: []string{"192.168.176.88/20", "fdd1:8161:2d2c::8888/56"},
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/24",
"--fixed-cidr-v6", "fdd1:8161:2d2c::/64",
},
// There's no --bip to dictate the subnet, so it's derived from an
// existing bridge address. If fixed-cidr's subnet is made smaller
// following a daemon restart, a user might reasonably expect the
// default bridge network's subnet to shrink to match. However,
// that has not been the behaviour - instead, only the allocatable
// range is reduced (as would happen with a user-managed bridge).
// In this case, if the user wants a smaller subnet, their options
// are to delete docker0, or supply a --bip. A change in this subtle
// behaviour might be best. But, it's probably not causing problems,
// and it'd be a breaking change for anyone relying on the existing
// behaviour.
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/20"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/56"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::8888")},
},
},
{
name: "link-local fixed-cidr-v6",
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/24",
"--fixed-cidr-v6", "fe80::/64",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")},
{Subnet: netip.MustParsePrefix("fe80::/64"), IPRange: netip.MustParsePrefix("fe80::/64"), Gateway: llGwPlaceholder},
},
},
{
name: "nonstandard link-local fixed-cidr-v6",
initialBridgeAddrs: []string{"192.168.176.88/20", "fe80:1234::88/56"},
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/24",
"--fixed-cidr-v6", "fe80:1234::/64",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/20"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fe80:1234::/56"), IPRange: netip.MustParsePrefix("fe80:1234::/64"), Gateway: netip.MustParseAddr("fe80:1234::88")},
},
},
{
name: "fixed-cidr within old bridge subnet with new bip",
initialBridgeAddrs: []string{"192.168.176.88/20", "fdd1:8161:2d2c::/56"},
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/24", "--bip", "192.168.176.99/24",
"--fixed-cidr-v6", "fdd1:8161:2d2c::/64", "--bip6", "fdd1:8161:2d2c::9999/64",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.99")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::9999")},
},
},
{
name: "old bridge subnet within fixed-cidr",
initialBridgeAddrs: []string{"192.168.176.88/24", "fdd1:8161:2d2c::8888/64"},
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/20",
"--fixed-cidr-v6", "fdd1:8161:2d2c::/56",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/20"), IPRange: netip.MustParsePrefix("192.168.176.0/20"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/56"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/56"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::8888")},
},
},
{
name: "old bridge subnet outside fixed-cidr",
initialBridgeAddrs: []string{"192.168.176.88/24", "fdd1:8161:2d2c::8888/64"},
daemonArgs: []string{
"--fixed-cidr", "192.168.177.0/24",
"--fixed-cidr-v6", "fdd1:8161:2d2c:1::/64",
},
// The bridge's address/subnet should be ignored, this is a change
// of fixed-cidr.
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.177.0/24"), IPRange: netip.MustParsePrefix("192.168.177.0/24"), Gateway: netip.MustParseAddr("192.168.177.1")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c:1::1")},
},
},
{
name: "old bridge subnet outside fixed-cidr with bip",
initialBridgeAddrs: []string{"192.168.176.88/24", "fdd1:8161:2d2c::8888/64"},
daemonArgs: []string{
"--fixed-cidr", "192.168.177.0/24", "--bip", "192.168.177.99/24",
"--fixed-cidr-v6", "fdd1:8161:2d2c:1::/64", "--bip6", "fdd1:8161:2d2c:1::9999/64",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.177.0/24"), IPRange: netip.MustParsePrefix("192.168.177.0/24"), Gateway: netip.MustParseAddr("192.168.177.99")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c:1::9999")},
},
},
}
for _, tc := range testcases {
tc.bridgeName = "docker0"
testDefaultBridgeIPAM(ctx, t, tc)
}
}
// Like TestDaemonUserDefaultBridgeIPAMDocker0, but with a user-defined/supplied
// bridge, instead of docker0.
func TestDaemonDefaultBridgeIPAM_UserBr(t *testing.T) {
skip.If(t, testEnv.IsRootless, "can't create L3 segment in rootless namespace")
ctx := testutil.StartSpan(baseContext, t)
testcases := []defaultBridgeIPAMTestCase{
{
name: "bridge only",
initialBridgeAddrs: []string{"192.168.176.88/20", "fdd1:8161:2d2c::8888/64"},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/20"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::8888")},
},
},
{
name: "fixed-cidr only",
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/24",
"--fixed-cidr-v6", "fdd1:8161:2d2c::/64",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::1")},
},
},
{
name: "fcidr in bridge subnet and bridge ip in fcidr",
initialBridgeAddrs: []string{
"192.168.160.88/20", "192.168.176.88/20", "192.168.192.88/20",
"fdd1:8161:2d2c::8888/60", "fdd1:8161:2d2c:10::8888/60", "fdd1:8161:2d2c:20::8888/60",
},
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/24",
"--fixed-cidr-v6", "fdd1:8161:2d2c:10::/64",
},
// Selected bip should be the one within fixed-cidr
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/20"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c:10::/60"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c:10::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c:10::8888")},
},
},
{
name: "fcidr in bridge subnet and bridge ip not in fcidr",
initialBridgeAddrs: []string{
"192.168.160.88/20", "192.168.176.88/20", "192.168.192.88/20",
"fdd1:8161:2d2c::8888/60", "fdd1:8161:2d2c:10::8888/60", "fdd1:8161:2d2c:20::8888/60",
},
daemonArgs: []string{
"--fixed-cidr", "192.168.177.0/24",
"--fixed-cidr-v6", "fdd1:8161:2d2c:11::8888/64",
},
// Selected bridge subnet should be the one that encompasses fixed-cidr.
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/20"), IPRange: netip.MustParsePrefix("192.168.177.0/24"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c:10::/60"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c:11::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c:10::8888")},
},
},
{
name: "link-local fixed-cidr-v6",
initialBridgeAddrs: []string{"192.168.176.88/20"},
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/24",
"--fixed-cidr-v6", "fe80::/64",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/20"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fe80::/64"), IPRange: netip.MustParsePrefix("fe80::/64"), Gateway: llGwPlaceholder},
},
},
{
name: "nonstandard link-local fixed-cidr-v6",
initialBridgeAddrs: []string{"192.168.176.88/20", "fe80:1234::88/56"},
daemonArgs: []string{
"--fixed-cidr", "192.168.176.0/24",
"--fixed-cidr-v6", "fe80:1234::/64",
},
expIPAMConfig: []network.IPAMConfig{
{Subnet: netip.MustParsePrefix("192.168.176.0/20"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.88")},
{Subnet: netip.MustParsePrefix("fe80:1234::/56"), IPRange: netip.MustParsePrefix("fe80:1234::/64"), Gateway: netip.MustParseAddr("fe80:1234::88")},
},
},
{
name: "fixed-cidr bigger than bridge subnet",
initialBridgeAddrs: []string{"192.168.176.88/24"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/20"},
ipv4Only: true,
// fixed-cidr (the range of allocatable addresses) is bigger than the
// bridge subnet - this is a configuration error, but has historically
// been allowed. Because IPRange is treated as an offset into Subnet, it
// would normally result in a docker network that allocated addresses
// within the selected subnet. So, fixed-cidr is dropped, making the
// whole subnet allocatable.
expIPAMConfig: []network.IPAMConfig{{Subnet: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.88")}},
},
{
name: "no bridge ip within fixed-cidr",
initialBridgeAddrs: []string{"192.168.160.88/20"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/24"},
ipv4Only: true,
// fixed-cidr (the range of allocatable addresses) is outside the bridge
// subnet - this is a configuration error, but has historically been
// allowed. Because IPRange is treated as an offset into Subnet, it
// would normally result in a docker network that allocated addresses
// within the selected subnet. So, fixed-cidr is dropped, making the
// whole subnet allocatable.
expIPAMConfig: []network.IPAMConfig{{Subnet: netip.MustParsePrefix("192.168.160.0/20"), Gateway: netip.MustParseAddr("192.168.160.88")}},
},
{
name: "fixed-cidr contains bridge subnet",
initialBridgeAddrs: []string{"192.168.177.1/24"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/20"},
// fixed-cidr (the range of allocatable addresses) is bigger than the
// bridge subnet, and the bridge's address is not within fixed-cidr.
// This is a configuration error, but has historically been allowed.
// Because IPRange is treated as an offset into Subnet, it would
// normally result in a docker network that allocated addresses
// within the selected subnet. So, fixed-cidr is dropped, making the
// whole subnet allocatable.
ipv4Only: true,
expIPAMConfig: []network.IPAMConfig{{Subnet: netip.MustParsePrefix("192.168.177.0/24"), Gateway: netip.MustParseAddr("192.168.177.1")}},
},
{
name: "fixed-cidr-v6 bigger than bridge subnet",
initialBridgeAddrs: []string{"fdd1:8161:2d2c::8888/64"},
daemonArgs: []string{"--fixed-cidr-v6", "fdd1:8161:2d2c::/60"},
// fixed-cidr-v6 (the range of allocatable addresses) is bigger than the bridge
// subnet - this is a configuration error. Unlike IPv4, it has not historically
// been allowed, so it will prevent daemon startup.
expStartErr: true,
},
{
name: "no bridge ip within fixed-cidr-v6",
initialBridgeAddrs: []string{"fdd1:8161:2d2c::8888/60"},
daemonArgs: []string{"--fixed-cidr-v6", "fdd1:8161:2d2c:10::/64"},
// fixed-cidr-v6 (the range of allocatable addresses) is outside the bridge subnet -
// this is a configuration error. Unlike IPv4, it has not historically been
// allowed, so it will prevent daemon startup.
expStartErr: true,
},
{
name: "fixed-cidr-v6 contains bridge subnet",
initialBridgeAddrs: []string{"fdd1:8161:2d2c:10::1/64"},
daemonArgs: []string{"--fixed-cidr-v6", "fdd1:8161:2d2c:10::/60"},
// fixed-cidr-v6 (the range of allocatable addresses) is bigger than the
// bridge subnet, and the bridge's address is not within fixed-cidr.
// This is a configuration error, Unlike IPv4, it has not historically been
// allowed, so it will prevent daemon startup.
expStartErr: true,
},
}
for _, tc := range testcases {
tc.bridgeName = "br-dbi"
tc.userDefinedBridge = true
testDefaultBridgeIPAM(ctx, t, tc)
}
}
// llGwPlaceholder can be used as a value for "Gateway" in expected IPAM config,
// before comparison with actual results it'll be replaced by the kernel assigned
// link local IPv6 address for the bridge.
var llGwPlaceholder = netip.MustParseAddr("fe80::1").WithZone("ll-gateway-placeholder")
type defaultBridgeIPAMTestCase struct {
name string
bridgeName string
userDefinedBridge bool
initialBridgeAddrs []string
daemonArgs []string
ipv4Only bool
expStartErr bool
expIPAMConfig []network.IPAMConfig
}
func testDefaultBridgeIPAM(ctx context.Context, t *testing.T, tc defaultBridgeIPAMTestCase) {
t.Run(tc.name, func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
// Run this test in its own network namespace because it messes with the default
// bridge and, for example, iptables rules for the default bridge aren't deleted
// because the network can't be deleted. Then, rules with old addresses may break
// unrelated tests.
host, cleanup := newHostInL3Seg(t, "defbripam", "192.168.131.1", "fdf9:2eb5:ba8c::1")
defer cleanup()
host.Do(t, func() {
llAddr, _ := netip.AddrFromSlice(createBridge(t, tc.bridgeName, tc.initialBridgeAddrs))
var dArgs []string
if !tc.ipv4Only {
dArgs = append(tc.daemonArgs, "--ipv6")
}
if tc.userDefinedBridge {
// If a bridge is supplied by the user, the daemon should use its addresses
// to infer --bip (which cannot be specified).
dArgs = append(dArgs, "--bridge", tc.bridgeName)
}
// 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="))
defer func() {
d.Stop(t)
d.Cleanup(t)
}()
if tc.expStartErr {
err := d.StartWithError(dArgs...)
assert.Check(t, is.ErrorContains(err, "daemon exited during startup"))
return
}
d.Start(t, dArgs...)
c := d.NewClientT(t)
defer c.Close()
res, err := c.NetworkInspect(ctx, network.NetworkBridge, client.NetworkInspectOptions{})
assert.NilError(t, err)
expIPAMConfig := slices.Clone(tc.expIPAMConfig)
for i := range expIPAMConfig {
if expIPAMConfig[i].Gateway == llGwPlaceholder {
expIPAMConfig[i].Gateway = llAddr
}
}
assert.Check(t, is.DeepEqual(res.Network.IPAM.Config, expIPAMConfig, cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{})), "unexpected IPAM config '%s'", tc.name)
})
})
}
func newHostInL3Seg(t *testing.T, name, ip4, ip6 string) (networking.Host, func()) {
// Set up a netns for each test to avoid sysctl and iptables pollution.
addr4 := netip.MustParseAddr(ip4)
addr6 := netip.MustParseAddr(ip6)
l3 := networking.NewL3Segment(t, "test-"+name,
netip.PrefixFrom(addr4, 24),
netip.PrefixFrom(addr6, 64),
)
hostname := "host-" + name
l3.AddHost(t, hostname, "ns-"+name, "eth0",
netip.PrefixFrom(addr4.Next(), 24),
netip.PrefixFrom(addr6.Next(), 64),
)
return l3.Hosts[hostname], func() { l3.Destroy(t) }
}
// createBridge creates a bridge device named ifName, brings it up, waits for
// the kernel to assign a link-local IPv6 address, assigns addrs, and returns
// the kernel-assigned LL address.
func createBridge(t *testing.T, ifName string, addrs []string) net.IP {
t.Helper()
// Get a netlink handle in this netns.
nlh, err := nlwrap.NewHandle()
assert.NilError(t, err)
defer nlh.Close()
link := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: ifName,
},
}
err = nlh.LinkAdd(link)
assert.NilError(t, err)
// Bring the interface up, and wait for the kernel to assign its link-local
// address (to cause maximum confusion - the LL address shouldn't be selected
// as "bip6").
brLink, err := nlh.LinkByName(ifName)
assert.NilError(t, err)
err = nlh.LinkSetUp(brLink)
assert.NilError(t, err)
var llAddr net.IP
poll.WaitOn(t, func(t poll.LogT) poll.Result {
addrs, err := nlh.AddrList(brLink, netlink.FAMILY_V6)
if err != nil {
return poll.Error(err)
}
if len(addrs) == 0 {
return poll.Continue("no IPv6 addresses")
}
llAddr = addrs[0].IP
return poll.Success()
})
for _, addr := range addrs {
ip, ipNet, err := net.ParseCIDR(addr)
assert.NilError(t, err)
ipNet.IP = ip
err = nlh.AddrAdd(link, &netlink.Addr{IPNet: ipNet})
assert.NilError(t, err)
}
return llAddr
}
// TestSwarmNoNftables checks that, because swarm does not yet have nftables support,
// it's not possible to:
// - enable Swarm when nftables is enabled, or to
// - restart an already Swarm enabled daemon with nftables enabled as well.
func TestSwarmNoNftables(t *testing.T) {
ctx := testutil.StartSpan(baseContext, t)
skip.If(t, testEnv.IsRemoteDaemon)
skip.If(t, testEnv.IsRootless, "rootless mode doesn't support Swarm-mode")
t.Run("start", func(t *testing.T) {
d := daemon.New(t)
d.Start(t, "--firewall-backend=nftables")
defer d.Stop(t)
err := d.SwarmInitWithError(ctx, t, swarmtypes.InitRequest{AdvertiseAddr: "127.0.0.1:2377"})
assert.Check(t, is.ErrorContains(err, "--firewall-backend=nftables is incompatible with swarm mode"))
})
t.Run("restart", func(t *testing.T) {
d := daemon.New(t)
d.Start(t, "--firewall-backend=iptables")
defer d.Stop(t)
d.SwarmInit(ctx, t, swarmtypes.InitRequest{AdvertiseAddr: "127.0.0.1:2377"})
err := d.RestartWithError("--firewall-backend=nftables")
assert.Check(t, is.ErrorContains(err, "daemon exited during startup"))
})
}
// TestDaemonShutsDownQuicklyDespiteEventsConnection checks whether the daemon
// shuts down in less than 5 secs when there's an active API connection to the
// '/events' endpoint.
//
// As this test verifies timing behavior, it may become flaky. Feel free to
// delete it if it's too annoying.
//
// Regression test for https://github.com/moby/moby/issues/32357#issuecomment-3496848492
func TestDaemonShutsDownQuicklyDespiteEventsConnection(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon, "cannot start daemon on remote test run")
// The Engine is presumably working the same way on Windows and Linux.
// Avoid running on Windows as it may be more flaky there.
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
t.Parallel()
ctx := testutil.StartSpan(baseContext, t)
d := daemon.New(t)
defer d.Cleanup(t)
// This is a parallel test, disable iptables integration.
d.StartWithBusybox(ctx, t, "--iptables=false", "--ip6tables=false")
defer d.Stop(t)
apiClient := d.NewClientT(t)
// Open a connection to the '/events' endpoint.
apiClient.Events(ctx, client.EventsListOptions{})
// Kill the daemon
t0 := time.Now()
d.Stop(t)
dt := time.Since(t0)
if dt.Seconds() > 5 {
t.Error("the daemon took more than 5 secs to shutdown")
}
}