Repository: moby/moby (language: go)

Issue:
order in default-address-pools in daemon.json is relevant
### Description

using the list of `default-address-pools` in daemon.json in different orders leads to the configuration error:

```
failed to start daemon: Error initializing network controller: 
Error creating default "bridge" network: 
failed to parse pool request for address space "LocalDefault" pool "fc00:1337:1337::/80" subpool "127.16.0.0/12": 
failed to compute range's lowest ip address: 
cannot compute host portion ip address because ip and mask are not compatible: 
(net.IP{0x7f, 0x10, 0x0, 0x0}, net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0})
```

### Reproduce

Working order (ipv4 first):
```
  "default-address-pools": [
    {
      "base": "172.16.0.0/12",
      "size": 24
    },
    {
      "base": "fc00:1337:1337::0/64",
      "size": 80
    }
```

Broken order (ipv6 first):
```
  "default-address-pools": [
    {
      "base": "fc00:1337:1337::0/64",
      "size": 80      
    },
    {
      "base": "172.16.0.0/12",
      "size": 24
    }
```

### Expected behavior

a list in json configuration should not be order dependent

### docker version

```bash
Client: Docker Engine - Community
 Version:           20.10.18
 API version:       1.41
 Go version:        go1.18.6
 Git commit:        b40c2f6
 Built:             Thu Sep  8 23:11:45 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.18
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.18.6
  Git commit:       e42327a
  Built:            Thu Sep  8 23:09:37 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.8
  GitCommit:        9cd3357b7fd7218e4aec3eae239db1f68a5a6ec6
 runc:
  Version:          1.1.4
  GitCommit:        v1.1.4-0-g5fd4c4d
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```


### docker info

```bash
Client:
 Context:    default
 Debug Mode: false
 Plugins:
  app: Docker App (Docker Inc., v0.9.1-beta3)
  buildx: Docker Buildx (Docker Inc., v0.9.1-docker)

Server:
 Containers: 5
  Running: 5
  Paused: 0
  Stopped: 0
 Images: 12
 Server Version: 20.10.18
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 1
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runtime.v1.linux runc io.containerd.runc.v2
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 9cd3357b7fd7218e4aec3eae239db1f68a5a6ec6
 runc version: v1.1.4-0-g5fd4c4d
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: default
 Kernel Version: 5.4.0-126-generic
 Operating System: Ubuntu 20.04.5 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 2
 Total Memory: 3.84GiB
 Name: <hostname>
 ID: AVF6:M5H3:LGQI:JWEX:SWVQ:VQTV:S6PE:3ZLA:T42S:E2SM:JVRQ:TVHP
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
 Default Address Pools:
   Base: 172.16.0.0/12, Size: 24
   Base: fc00:1337:1337::0/64, Size: 80

WARNING: No swap limit support
```


### Additional Info

This should either be fixed or its behavior should be well-documented for the option (which is currently quite underdocumented anyways...)


Discussion / hints:
I tried to reproduce this issue, and although I don't see the same error messages I can confirm the daemon fails to start when an IPv6 subnet is put before IPv4 subnet(s):

> failed to start daemon: Error initializing network controller: Error creating default "bridge" network: failed to add IPv4 address fc00:1337:1337::1/8

Didn't went down the rabbit hole yet, but I tested #43033 and I confirm this PR is fixing the issue.

EDIT: Will publish a fix for that tomorrow.

Fix patch:
diff --git a/daemon/daemon_linux.go b/daemon/daemon_linux.go
index 7aed4395a780c..5448c0afa94e1 100644
--- a/daemon/daemon_linux.go
+++ b/daemon/daemon_linux.go
@@ -4,16 +4,19 @@ import (
 	"bufio"
 	"fmt"
 	"io"
+	"net"
 	"os"
 	"regexp"
 	"strings"
 
 	"github.com/docker/docker/daemon/config"
+	"github.com/docker/docker/libnetwork/ns"
 	"github.com/docker/docker/libnetwork/resolvconf"
 	"github.com/moby/sys/mount"
 	"github.com/moby/sys/mountinfo"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
+	"github.com/vishvananda/netlink"
 )
 
 // On Linux, plugins use a static path for storing execution state,
@@ -141,3 +144,41 @@ func setupResolvConf(config *config.Config) {
 	}
 	config.ResolvConf = resolvconf.Path()
 }
+
+// ifaceAddrs returns the IPv4 and IPv6 addresses assigned to the network
+// interface with name linkName.
+//
+// No error is returned if the named interface does not exist.
+func ifaceAddrs(linkName string) (v4, v6 []*net.IPNet, err error) {
+	nl := ns.NlHandle()
+	link, err := nl.LinkByName(linkName)
+	if err != nil {
+		if !errors.As(err, new(netlink.LinkNotFoundError)) {
+			return nil, nil, err
+		}
+		return nil, nil, nil
+	}
+
+	get := func(family int) ([]*net.IPNet, error) {
+		addrs, err := nl.AddrList(link, family)
+		if err != nil {
+			return nil, err
+		}
+
+		ipnets := make([]*net.IPNet, len(addrs))
+		for i := range addrs {
+			ipnets[i] = addrs[i].IPNet
+		}
+		return ipnets, nil
+	}
+
+	v4, err = get(netlink.FAMILY_V4)
+	if err != nil {
+		return nil, nil, err
+	}
+	v6, err = get(netlink.FAMILY_V6)
+	if err != nil {
+		return nil, nil, err
+	}
+	return v4, v6, nil
+}
diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go
index cd602b3434a1f..10ad5f9794895 100644
--- a/daemon/daemon_unix.go
+++ b/daemon/daemon_unix.go
@@ -34,7 +34,6 @@ import (
 	nwconfig "github.com/docker/docker/libnetwork/config"
 	"github.com/docker/docker/libnetwork/drivers/bridge"
 	"github.com/docker/docker/libnetwork/netlabel"
-	"github.com/docker/docker/libnetwork/netutils"
 	"github.com/docker/docker/libnetwork/options"
 	lntypes "github.com/docker/docker/libnetwork/types"
 	"github.com/docker/docker/opts"
@@ -950,30 +949,37 @@ func initBridgeDriver(controller libnetwork.NetworkController, config *config.Co
 
 	ipamV4Conf := &libnetwork.IpamConf{AuxAddresses: make(map[string]string)}
 
-	nwList, nw6List, err := netutils.ElectInterfaceAddresses(bridgeName)
+	// By default, libnetwork will request an arbitrary available address
+	// pool for the network from the configured IPAM allocator.
+	// Configure it to use the IPv4 network ranges of the existing bridge
+	// interface if one exists with IPv4 addresses assigned to it.
+
+	nwList, nw6List, err := ifaceAddrs(bridgeName)
 	if err != nil {
 		return errors.Wrap(err, "list bridge addresses failed")
 	}
 
-	nw := nwList[0]
-	if len(nwList) > 1 && config.BridgeConfig.FixedCIDR != "" {
-		_, fCIDR, err := net.ParseCIDR(config.BridgeConfig.FixedCIDR)
-		if err != nil {
-			return errors.Wrap(err, "parse CIDR failed")
-		}
-		// Iterate through in case there are multiple addresses for the bridge
-		for _, entry := range nwList {
-			if fCIDR.Contains(entry.IP) {
-				nw = entry
-				break
+	if len(nwList) > 0 {
+		nw := nwList[0]
+		if len(nwList) > 1 && config.BridgeConfig.FixedCIDR != "" {
+			_, fCIDR, err := net.ParseCIDR(config.BridgeConfig.FixedCIDR)
+			if err != nil {
+				return errors.Wrap(err, "parse CIDR failed")
+			}
+			// Iterate through in case there are multiple addresses for the bridge
+			for _, entry := range nwList {
+				if fCIDR.Contains(entry.IP) {
+					nw = entry
+					break
+				}
 			}
 		}
-	}
 
-	ipamV4Conf.PreferredPool = lntypes.GetIPNetCanonical(nw).String()
-	hip, _ := lntypes.GetHostPartIP(nw.IP, nw.Mask)
-	if hip.IsGlobalUnicast() {
-		ipamV4Conf.Gateway = nw.IP.String()
+		ipamV4Conf.PreferredPool = lntypes.GetIPNetCanonical(nw).String()
+		hip, _ := lntypes.GetHostPartIP(nw.IP, nw.Mask)
+		if hip.IsGlobalUnicast() {
+			ipamV4Conf.Gateway = nw.IP.String()
+		}
 	}
 
 	if config.BridgeConfig.IP != "" {


Test patch:
diff --git a/daemon/daemon_linux_test.go b/daemon/daemon_linux_test.go
index b8c89b9e82866..af51a49ee2345 100644
--- a/daemon/daemon_linux_test.go
+++ b/daemon/daemon_linux_test.go
@@ -4,6 +4,7 @@
 package daemon // import "github.com/docker/docker/daemon"
 
 import (
+	"net"
 	"os"
 	"path/filepath"
 	"strings"
@@ -11,8 +12,12 @@ import (
 
 	containertypes "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/daemon/config"
+	"github.com/docker/docker/libnetwork/testutils"
+	"github.com/docker/docker/libnetwork/types"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/moby/sys/mount"
 	"github.com/moby/sys/mountinfo"
+	"github.com/vishvananda/netlink"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
 )
@@ -343,3 +348,66 @@ func TestRootMountCleanup(t *testing.T) {
 		assert.Assert(t, d.cleanupMounts())
 	})
 }
+
+func TestIfaceAddrs(t *testing.T) {
+	CIDR := func(cidr string) *net.IPNet {
+		t.Helper()
+		nw, err := types.ParseCIDR(cidr)
+		assert.NilError(t, err)
+		return nw
+	}
+
+	for _, tt := range []struct {
+		name string
+		nws  []*net.IPNet
+	}{
+		{
+			name: "Single",
+			nws:  []*net.IPNet{CIDR("172.101.202.254/16")},
+		},
+		{
+			name: "Multiple",
+			nws: []*net.IPNet{
+				CIDR("172.101.202.254/16"),
+				CIDR("172.102.202.254/16"),
+			},
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			defer testutils.SetupTestOSContext(t)()
+
+			createBridge(t, "test", tt.nws...)
+
+			ipv4Nw, ipv6Nw, err := ifaceAddrs("test")
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			assert.Check(t, is.DeepEqual(tt.nws, ipv4Nw,
+				cmpopts.SortSlices(func(a, b *net.IPNet) bool { return a.String() < b.String() })))
+			// IPv6 link-local address
+			assert.Check(t, is.Len(ipv6Nw, 1))
+		})
+	}
+}
+
+func createBridge(t *testing.T, name string, bips ...*net.IPNet) {
+	t.Helper()
+
+	link := &netlink.Bridge{
+		LinkAttrs: netlink.LinkAttrs{
+			Name: name,
+		},
+	}
+	if err := netlink.LinkAdd(link); err != nil {
+		t.Fatalf("Failed to create interface via netlink: %v", err)
+	}
+	for _, bip := range bips {
+		if err := netlink.AddrAdd(link, &netlink.Addr{IPNet: bip}); err != nil {
+			t.Fatal(err)
+		}
+	}
+	if err := netlink.LinkSetUp(link); err != nil {
+		t.Fatal(err)
+	}
+}