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