Procházet zdrojové kódy

Merge pull request #46183 from akerouanton/validate-NetworkingConfig

daemon: Improve NetworkingConfig & EndpointSettings validation
Sebastiaan van Stijn před 1 rokem
rodič
revize
f13065b1f7

+ 4 - 0
api/swagger.yaml

@@ -10038,6 +10038,10 @@ paths:
       responses:
         200:
           description: "No error"
+        400:
+          description: "bad parameter"
+          schema:
+            $ref: "#/definitions/ErrorResponse"
         403:
           description: "Operation not supported for swarm scoped networks"
           schema:

+ 135 - 0
api/types/network/endpoint.go

@@ -0,0 +1,135 @@
+package network
+
+import (
+	"errors"
+	"fmt"
+	"net"
+
+	"github.com/docker/docker/internal/multierror"
+)
+
+// EndpointSettings stores the network endpoint details
+type EndpointSettings struct {
+	// Configurations
+	IPAMConfig *EndpointIPAMConfig
+	Links      []string
+	Aliases    []string
+	// Operational data
+	NetworkID           string
+	EndpointID          string
+	Gateway             string
+	IPAddress           string
+	IPPrefixLen         int
+	IPv6Gateway         string
+	GlobalIPv6Address   string
+	GlobalIPv6PrefixLen int
+	MacAddress          string
+	DriverOpts          map[string]string
+}
+
+// Copy makes a deep copy of `EndpointSettings`
+func (es *EndpointSettings) Copy() *EndpointSettings {
+	epCopy := *es
+	if es.IPAMConfig != nil {
+		epCopy.IPAMConfig = es.IPAMConfig.Copy()
+	}
+
+	if es.Links != nil {
+		links := make([]string, 0, len(es.Links))
+		epCopy.Links = append(links, es.Links...)
+	}
+
+	if es.Aliases != nil {
+		aliases := make([]string, 0, len(es.Aliases))
+		epCopy.Aliases = append(aliases, es.Aliases...)
+	}
+	return &epCopy
+}
+
+// EndpointIPAMConfig represents IPAM configurations for the endpoint
+type EndpointIPAMConfig struct {
+	IPv4Address  string   `json:",omitempty"`
+	IPv6Address  string   `json:",omitempty"`
+	LinkLocalIPs []string `json:",omitempty"`
+}
+
+// Copy makes a copy of the endpoint ipam config
+func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig {
+	cfgCopy := *cfg
+	cfgCopy.LinkLocalIPs = make([]string, 0, len(cfg.LinkLocalIPs))
+	cfgCopy.LinkLocalIPs = append(cfgCopy.LinkLocalIPs, cfg.LinkLocalIPs...)
+	return &cfgCopy
+}
+
+// NetworkSubnet describes a user-defined subnet for a specific network. It's only used to validate if an
+// EndpointIPAMConfig is valid for a specific network.
+type NetworkSubnet interface {
+	// Contains checks whether the NetworkSubnet contains [addr].
+	Contains(addr net.IP) bool
+	// IsStatic checks whether the subnet was statically allocated (ie. user-defined).
+	IsStatic() bool
+}
+
+// IsInRange checks whether static IP addresses are valid in a specific network.
+func (cfg *EndpointIPAMConfig) IsInRange(v4Subnets []NetworkSubnet, v6Subnets []NetworkSubnet) error {
+	var errs []error
+
+	if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil {
+		errs = append(errs, err)
+	}
+	if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil {
+		errs = append(errs, err)
+	}
+
+	return multierror.Join(errs...)
+}
+
+func validateEndpointIPAddress(epAddr string, ipamSubnets []NetworkSubnet) error {
+	if epAddr == "" {
+		return nil
+	}
+
+	var staticSubnet bool
+	parsedAddr := net.ParseIP(epAddr)
+	for _, subnet := range ipamSubnets {
+		if subnet.IsStatic() {
+			staticSubnet = true
+			if subnet.Contains(parsedAddr) {
+				return nil
+			}
+		}
+	}
+
+	if staticSubnet {
+		return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr)
+	}
+
+	return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets")
+}
+
+// Validate checks whether cfg is valid.
+func (cfg *EndpointIPAMConfig) Validate() error {
+	if cfg == nil {
+		return nil
+	}
+
+	var errs []error
+
+	if cfg.IPv4Address != "" {
+		if addr := net.ParseIP(cfg.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() {
+			errs = append(errs, fmt.Errorf("invalid IPv4 address: %s", cfg.IPv4Address))
+		}
+	}
+	if cfg.IPv6Address != "" {
+		if addr := net.ParseIP(cfg.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() {
+			errs = append(errs, fmt.Errorf("invalid IPv6 address: %s", cfg.IPv6Address))
+		}
+	}
+	for _, addr := range cfg.LinkLocalIPs {
+		if parsed := net.ParseIP(addr); parsed == nil || parsed.IsUnspecified() {
+			errs = append(errs, fmt.Errorf("invalid link-local IP address: %s", addr))
+		}
+	}
+
+	return multierror.Join(errs...)
+}

+ 188 - 0
api/types/network/endpoint_test.go

@@ -0,0 +1,188 @@
+package network
+
+import (
+	"net"
+	"testing"
+
+	"gotest.tools/v3/assert"
+	is "gotest.tools/v3/assert/cmp"
+)
+
+type subnetStub struct {
+	static   bool
+	contains map[string]bool
+}
+
+func (stub subnetStub) IsStatic() bool {
+	return stub.static
+}
+
+func (stub subnetStub) Contains(addr net.IP) bool {
+	v, ok := stub.contains[addr.String()]
+	return ok && v
+}
+
+func TestEndpointIPAMConfigWithOutOfRangeAddrs(t *testing.T) {
+	testcases := []struct {
+		name           string
+		ipamConfig     *EndpointIPAMConfig
+		v4Subnets      []NetworkSubnet
+		v6Subnets      []NetworkSubnet
+		expectedErrors []string
+	}{
+		{
+			name: "valid config",
+			ipamConfig: &EndpointIPAMConfig{
+				IPv4Address:  "192.168.100.10",
+				IPv6Address:  "2a01:d2:af:420b:25c1:1816:bb33:855c",
+				LinkLocalIPs: []string{"169.254.169.254", "fe80::42:a8ff:fe33:6230"},
+			},
+			v4Subnets: []NetworkSubnet{
+				subnetStub{static: true, contains: map[string]bool{"192.168.100.10": true}},
+			},
+			v6Subnets: []NetworkSubnet{
+				subnetStub{static: true, contains: map[string]bool{"2a01:d2:af:420b:25c1:1816:bb33:855c": true}},
+			},
+		},
+		{
+			name: "static addresses out of range",
+			ipamConfig: &EndpointIPAMConfig{
+				IPv4Address: "192.168.100.10",
+				IPv6Address: "2a01:d2:af:420b:25c1:1816:bb33:855c",
+			},
+			v4Subnets: []NetworkSubnet{
+				subnetStub{static: true, contains: map[string]bool{"192.168.100.10": false}},
+			},
+			v6Subnets: []NetworkSubnet{
+				subnetStub{static: true, contains: map[string]bool{"2a01:d2:af:420b:25c1:1816:bb33:855c": false}},
+			},
+			expectedErrors: []string{
+				"no configured subnet or ip-range contain the IP address 192.168.100.10",
+				"no configured subnet or ip-range contain the IP address 2a01:d2:af:420b:25c1:1816:bb33:855c",
+			},
+		},
+		{
+			name: "static addresses with dynamic network subnets",
+			ipamConfig: &EndpointIPAMConfig{
+				IPv4Address: "192.168.100.10",
+				IPv6Address: "2a01:d2:af:420b:25c1:1816:bb33:855c",
+			},
+			v4Subnets: []NetworkSubnet{
+				subnetStub{static: false},
+			},
+			v6Subnets: []NetworkSubnet{
+				subnetStub{static: false},
+			},
+			expectedErrors: []string{
+				"user specified IP address is supported only when connecting to networks with user configured subnets",
+				"user specified IP address is supported only when connecting to networks with user configured subnets",
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			err := tc.ipamConfig.IsInRange(tc.v4Subnets, tc.v6Subnets)
+			if tc.expectedErrors == nil {
+				assert.NilError(t, err)
+				return
+			}
+
+			if _, ok := err.(interface{ Unwrap() []error }); !ok {
+				t.Fatal("returned error isn't a multierror")
+			}
+			errs := err.(interface{ Unwrap() []error }).Unwrap()
+			assert.Check(t, len(errs) == len(tc.expectedErrors), "errs: %+v", errs)
+
+			for _, expected := range tc.expectedErrors {
+				assert.Check(t, is.ErrorContains(err, expected))
+			}
+		})
+	}
+
+}
+
+func TestEndpointIPAMConfigWithInvalidConfig(t *testing.T) {
+	testcases := []struct {
+		name           string
+		ipamConfig     *EndpointIPAMConfig
+		expectedErrors []string
+	}{
+		{
+			name: "valid config",
+			ipamConfig: &EndpointIPAMConfig{
+				IPv4Address:  "192.168.100.10",
+				IPv6Address:  "2a01:d2:af:420b:25c1:1816:bb33:855c",
+				LinkLocalIPs: []string{"169.254.169.254", "fe80::42:a8ff:fe33:6230"},
+			},
+		},
+		{
+			name: "invalid IP addresses",
+			ipamConfig: &EndpointIPAMConfig{
+				IPv4Address:  "foo",
+				IPv6Address:  "bar",
+				LinkLocalIPs: []string{"baz", "foobar"},
+			},
+			expectedErrors: []string{
+				"invalid IPv4 address: foo",
+				"invalid IPv6 address: bar",
+				"invalid link-local IP address: baz",
+				"invalid link-local IP address: foobar",
+			},
+		},
+		{
+			name:       "ipv6 address with a zone",
+			ipamConfig: &EndpointIPAMConfig{IPv6Address: "fe80::1cc0:3e8c:119f:c2e1%ens18"},
+			expectedErrors: []string{
+				"invalid IPv6 address: fe80::1cc0:3e8c:119f:c2e1%ens18",
+			},
+		},
+		{
+			name: "unspecified address is invalid",
+			ipamConfig: &EndpointIPAMConfig{
+				IPv4Address:  "0.0.0.0",
+				IPv6Address:  "::",
+				LinkLocalIPs: []string{"0.0.0.0", "::"},
+			},
+			expectedErrors: []string{
+				"invalid IPv4 address: 0.0.0.0",
+				"invalid IPv6 address: ::",
+				"invalid link-local IP address: 0.0.0.0",
+				"invalid link-local IP address: ::",
+			},
+		},
+		{
+			name: "empty link-local",
+			ipamConfig: &EndpointIPAMConfig{
+				LinkLocalIPs: []string{""},
+			},
+			expectedErrors: []string{"invalid link-local IP address:"},
+		},
+	}
+
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			err := tc.ipamConfig.Validate()
+			if tc.expectedErrors == nil {
+				assert.NilError(t, err)
+				return
+			}
+
+			if _, ok := err.(interface{ Unwrap() []error }); !ok {
+				t.Fatal("returned error isn't a multierror")
+			}
+			errs := err.(interface{ Unwrap() []error }).Unwrap()
+			assert.Check(t, len(errs) == len(tc.expectedErrors), "errs: %+v", errs)
+
+			for _, expected := range tc.expectedErrors {
+				assert.Check(t, is.ErrorContains(err, expected))
+			}
+		})
+	}
+}

+ 2 - 0
api/types/network/ipam.go

@@ -51,6 +51,8 @@ func HasIPv6Subnets(ipam *IPAM) bool {
 	return false
 }
 
+// ValidateIPAM checks whether the network's IPAM passed as argument is valid. It returns a joinError of the list of
+// errors found.
 func ValidateIPAM(ipam *IPAM) error {
 	if ipam == nil {
 		return nil

+ 0 - 53
api/types/network/network.go

@@ -9,46 +9,12 @@ type Address struct {
 	PrefixLen int
 }
 
-// EndpointIPAMConfig represents IPAM configurations for the endpoint
-type EndpointIPAMConfig struct {
-	IPv4Address  string   `json:",omitempty"`
-	IPv6Address  string   `json:",omitempty"`
-	LinkLocalIPs []string `json:",omitempty"`
-}
-
-// Copy makes a copy of the endpoint ipam config
-func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig {
-	cfgCopy := *cfg
-	cfgCopy.LinkLocalIPs = make([]string, 0, len(cfg.LinkLocalIPs))
-	cfgCopy.LinkLocalIPs = append(cfgCopy.LinkLocalIPs, cfg.LinkLocalIPs...)
-	return &cfgCopy
-}
-
 // PeerInfo represents one peer of an overlay network
 type PeerInfo struct {
 	Name string
 	IP   string
 }
 
-// EndpointSettings stores the network endpoint details
-type EndpointSettings struct {
-	// Configurations
-	IPAMConfig *EndpointIPAMConfig
-	Links      []string
-	Aliases    []string
-	// Operational data
-	NetworkID           string
-	EndpointID          string
-	Gateway             string
-	IPAddress           string
-	IPPrefixLen         int
-	IPv6Gateway         string
-	GlobalIPv6Address   string
-	GlobalIPv6PrefixLen int
-	MacAddress          string
-	DriverOpts          map[string]string
-}
-
 // Task carries the information about one backend task
 type Task struct {
 	Name       string
@@ -65,25 +31,6 @@ type ServiceInfo struct {
 	Tasks        []Task
 }
 
-// Copy makes a deep copy of `EndpointSettings`
-func (es *EndpointSettings) Copy() *EndpointSettings {
-	epCopy := *es
-	if es.IPAMConfig != nil {
-		epCopy.IPAMConfig = es.IPAMConfig.Copy()
-	}
-
-	if es.Links != nil {
-		links := make([]string, 0, len(es.Links))
-		epCopy.Links = append(links, es.Links...)
-	}
-
-	if es.Aliases != nil {
-		aliases := make([]string, 0, len(es.Aliases))
-		epCopy.Aliases = append(aliases, es.Aliases...)
-	}
-	return &epCopy
-}
-
 // NetworkingConfig represents the container's networking configuration for each of its interfaces
 // Carries the networking configs specified in the `docker run` and `docker network connect` commands
 type NetworkingConfig struct {

+ 44 - 40
daemon/container_operations.go

@@ -18,6 +18,7 @@ import (
 	"github.com/docker/docker/daemon/config"
 	"github.com/docker/docker/daemon/network"
 	"github.com/docker/docker/errdefs"
+	"github.com/docker/docker/internal/multierror"
 	"github.com/docker/docker/libnetwork"
 	"github.com/docker/docker/libnetwork/netlabel"
 	"github.com/docker/docker/libnetwork/options"
@@ -562,54 +563,57 @@ func (daemon *Daemon) allocateNetwork(cfg *config.Config, container *container.C
 	return nil
 }
 
-// hasUserDefinedIPAddress returns whether the passed IPAM configuration contains IP address configuration
-func hasUserDefinedIPAddress(ipamConfig *networktypes.EndpointIPAMConfig) bool {
-	return ipamConfig != nil && (len(ipamConfig.IPv4Address) > 0 || len(ipamConfig.IPv6Address) > 0)
-}
-
-// User specified ip address is acceptable only for networks with user specified subnets.
-func validateNetworkingConfig(n *libnetwork.Network, epConfig *networktypes.EndpointSettings) error {
-	if n == nil || epConfig == nil {
+// validateEndpointSettings checks whether the given epConfig is valid. The nw parameter can be nil, in which case it
+// won't try to check if the endpoint IP addresses are within network's subnets.
+func validateEndpointSettings(nw *libnetwork.Network, nwName string, epConfig *networktypes.EndpointSettings) error {
+	if epConfig == nil {
 		return nil
 	}
-	if !containertypes.NetworkMode(n.Name()).IsUserDefined() {
-		if hasUserDefinedIPAddress(epConfig.IPAMConfig) && !enableIPOnPredefinedNetwork() {
-			return runconfig.ErrUnsupportedNetworkAndIP
+
+	ipamConfig := &networktypes.EndpointIPAMConfig{}
+	if epConfig.IPAMConfig != nil {
+		ipamConfig = epConfig.IPAMConfig
+	}
+
+	var errs []error
+
+	// TODO(aker): move this into api/types/network/endpoint.go once enableIPOnPredefinedNetwork and
+	//  serviceDiscoveryOnDefaultNetwork are removed.
+	if !containertypes.NetworkMode(nwName).IsUserDefined() {
+		hasStaticAddresses := ipamConfig.IPv4Address != "" || ipamConfig.IPv6Address != ""
+		// On Linux, user specified IP address is accepted only by networks with user specified subnets.
+		if hasStaticAddresses && !enableIPOnPredefinedNetwork() {
+			errs = append(errs, runconfig.ErrUnsupportedNetworkAndIP)
 		}
 		if len(epConfig.Aliases) > 0 && !serviceDiscoveryOnDefaultNetwork() {
-			return runconfig.ErrUnsupportedNetworkAndAlias
+			errs = append(errs, runconfig.ErrUnsupportedNetworkAndAlias)
 		}
 	}
-	if !hasUserDefinedIPAddress(epConfig.IPAMConfig) {
-		return nil
+
+	// TODO(aker): add a proper multierror.Append
+	if err := ipamConfig.Validate(); err != nil {
+		errs = append(errs, err.(interface{ Unwrap() []error }).Unwrap()...)
 	}
 
-	_, _, nwIPv4Configs, nwIPv6Configs := n.IpamConfig()
-	for _, s := range []struct {
-		ipConfigured  bool
-		subnetConfigs []*libnetwork.IpamConf
-	}{
-		{
-			ipConfigured:  len(epConfig.IPAMConfig.IPv4Address) > 0,
-			subnetConfigs: nwIPv4Configs,
-		},
-		{
-			ipConfigured:  len(epConfig.IPAMConfig.IPv6Address) > 0,
-			subnetConfigs: nwIPv6Configs,
-		},
-	} {
-		if s.ipConfigured {
-			foundSubnet := false
-			for _, cfg := range s.subnetConfigs {
-				if len(cfg.PreferredPool) > 0 {
-					foundSubnet = true
-					break
-				}
-			}
-			if !foundSubnet {
-				return runconfig.ErrUnsupportedNetworkNoSubnetAndIP
-			}
+	if nw != nil {
+		_, _, v4Configs, v6Configs := nw.IpamConfig()
+
+		var nwIPv4Subnets, nwIPv6Subnets []networktypes.NetworkSubnet
+		for _, nwIPAMConfig := range v4Configs {
+			nwIPv4Subnets = append(nwIPv4Subnets, nwIPAMConfig)
 		}
+		for _, nwIPAMConfig := range v6Configs {
+			nwIPv6Subnets = append(nwIPv6Subnets, nwIPAMConfig)
+		}
+
+		// TODO(aker): add a proper multierror.Append
+		if err := ipamConfig.IsInRange(nwIPv4Subnets, nwIPv6Subnets); err != nil {
+			errs = append(errs, err.(interface{ Unwrap() []error }).Unwrap()...)
+		}
+	}
+
+	if err := multierror.Join(errs...); err != nil {
+		return fmt.Errorf("invalid endpoint settings:\n%w", err)
 	}
 
 	return nil
@@ -657,7 +661,7 @@ func (daemon *Daemon) updateNetworkConfig(container *container.Container, n *lib
 		}
 	}
 
-	if err := validateNetworkingConfig(n, endpointConfig); err != nil {
+	if err := validateEndpointSettings(n, n.Name(), endpointConfig); err != nil {
 		return err
 	}
 

+ 2 - 0
daemon/container_operations_unix.go

@@ -367,11 +367,13 @@ func isLinkable(child *container.Container) bool {
 	return ok
 }
 
+// TODO(aker): remove when we make the default bridge network behave like any other network
 func enableIPOnPredefinedNetwork() bool {
 	return false
 }
 
 // serviceDiscoveryOnDefaultNetwork indicates if service discovery is supported on the default network
+// TODO(aker): remove when we make the default bridge network behave like any other network
 func serviceDiscoveryOnDefaultNetwork() bool {
 	return false
 }

+ 17 - 17
daemon/create.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net"
 	"runtime"
 	"strings"
 	"time"
@@ -21,6 +20,7 @@ import (
 	"github.com/docker/docker/daemon/images"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/image"
+	"github.com/docker/docker/internal/multierror"
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/docker/docker/runconfig"
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -98,7 +98,7 @@ func (daemon *Daemon) containerCreate(ctx context.Context, daemonCfg *configStor
 		}
 	}
 
-	err = verifyNetworkingConfig(opts.params.NetworkingConfig)
+	err = daemon.validateNetworkingConfig(opts.params.NetworkingConfig)
 	if err != nil {
 		return containertypes.CreateResponse{Warnings: warnings}, errdefs.InvalidParameter(err)
 	}
@@ -321,30 +321,30 @@ func (daemon *Daemon) mergeAndVerifyConfig(config *containertypes.Config, img *i
 	return nil
 }
 
-// verifyNetworkingConfig validates if the nwConfig is valid.
-func verifyNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error {
+// validateNetworkingConfig checks whether a container's NetworkingConfig is valid.
+func (daemon *Daemon) validateNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error {
 	if nwConfig == nil {
 		return nil
 	}
 
+	var errs []error
 	for k, v := range nwConfig.EndpointsConfig {
 		if v == nil {
-			return fmt.Errorf("no EndpointSettings for %s", k)
+			errs = append(errs, fmt.Errorf("invalid config for network %s: EndpointsConfig is nil", k))
+			continue
 		}
-		if v.IPAMConfig != nil {
-			if v.IPAMConfig.IPv4Address != "" && net.ParseIP(v.IPAMConfig.IPv4Address).To4() == nil {
-				return fmt.Errorf("invalid IPv4 address: %s", v.IPAMConfig.IPv4Address)
-			}
-			if v.IPAMConfig.IPv6Address != "" {
-				n := net.ParseIP(v.IPAMConfig.IPv6Address)
-				// if the address is an invalid network address (ParseIP == nil) or if it is
-				// an IPv4 address (To4() != nil), then it is an invalid IPv6 address
-				if n == nil || n.To4() != nil {
-					return fmt.Errorf("invalid IPv6 address: %s", v.IPAMConfig.IPv6Address)
-				}
-			}
+
+		// The referenced network k might not exist when the container is created, so just ignore the error in that case.
+		nw, _ := daemon.FindNetwork(k)
+		if err := validateEndpointSettings(nw, k, v); err != nil {
+			errs = append(errs, fmt.Errorf("invalid config for network %s: %w", k, err))
 		}
 	}
+
+	if len(errs) > 0 {
+		return errdefs.InvalidParameter(multierror.Join(errs...))
+	}
+
 	return nil
 }
 

+ 0 - 21
daemon/create_test.go

@@ -1,21 +0,0 @@
-package daemon // import "github.com/docker/docker/daemon"
-
-import (
-	"testing"
-
-	"github.com/docker/docker/api/types/network"
-	"gotest.tools/v3/assert"
-	is "gotest.tools/v3/assert/cmp"
-)
-
-// Test case for 35752
-func TestVerifyNetworkingConfig(t *testing.T) {
-	name := "mynet"
-	endpoints := make(map[string]*network.EndpointSettings, 1)
-	endpoints[name] = nil
-	nwConfig := &network.NetworkingConfig{
-		EndpointsConfig: endpoints,
-	}
-	err := verifyNetworkingConfig(nwConfig)
-	assert.Check(t, is.Error(err, "no EndpointSettings for mynet"), "should produce an error because no EndpointSettings were passed")
-}

+ 5 - 0
docs/api/version-history.md

@@ -47,6 +47,11 @@ keywords: "API, Docker, rcli, REST, documentation"
   1.44.
 * `POST /containers/create` now accepts multiple `EndpointSettings` in
   `NetworkingConfig.EndpointSettings`.
+* `POST /containers/create` and `POST /networks/{id}/connect` will now catch
+  validation errors that were previously only returned during `POST /containers/{id}/start`.
+  These endpoints will also return the full set of validation errors they find,
+  instead of returning only the first one.
+  Note that this change is _unversioned_ and applies to all API versions.
 
 ## v1.43 API changes
 

+ 5 - 5
integration-cli/docker_cli_network_unix_test.go

@@ -1289,9 +1289,9 @@ func (s *DockerNetworkSuite) TestDockerNetworkConnectPreferredIP(c *testing.T) {
 	verifyIPAddresses(c, "c0", "n0", "172.28.99.88", "2001:db8:1234::9988")
 
 	// connect the container to the second network specifying an ip addresses
-	dockerCmd(c, "network", "connect", "--ip", "172.30.55.44", "--ip6", "2001:db8:abcd::5544", "n1", "c0")
-	verifyIPAddressConfig(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544")
-	verifyIPAddresses(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544")
+	dockerCmd(c, "network", "connect", "--ip", "172.30.5.44", "--ip6", "2001:db8:abcd::5544", "n1", "c0")
+	verifyIPAddressConfig(c, "c0", "n1", "172.30.5.44", "2001:db8:abcd::5544")
+	verifyIPAddresses(c, "c0", "n1", "172.30.5.44", "2001:db8:abcd::5544")
 
 	// Stop and restart the container
 	dockerCmd(c, "stop", "c0")
@@ -1300,8 +1300,8 @@ func (s *DockerNetworkSuite) TestDockerNetworkConnectPreferredIP(c *testing.T) {
 	// verify requested addresses are applied and configs are still there
 	verifyIPAddressConfig(c, "c0", "n0", "172.28.99.88", "2001:db8:1234::9988")
 	verifyIPAddresses(c, "c0", "n0", "172.28.99.88", "2001:db8:1234::9988")
-	verifyIPAddressConfig(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544")
-	verifyIPAddresses(c, "c0", "n1", "172.30.55.44", "2001:db8:abcd::5544")
+	verifyIPAddressConfig(c, "c0", "n1", "172.30.5.44", "2001:db8:abcd::5544")
+	verifyIPAddresses(c, "c0", "n1", "172.30.5.44", "2001:db8:abcd::5544")
 
 	// Still it should fail to connect to the default network with a specified IP (whatever ip)
 	out, _, err := dockerCmdWithError("network", "connect", "--ip", "172.21.55.44", "bridge", "c0")

+ 25 - 1
libnetwork/network.go

@@ -71,6 +71,8 @@ type networkDBTable struct {
 }
 
 // IpamConf contains all the ipam related configurations for a network
+//
+// TODO(aker): use proper net/* structs instead of string literals.
 type IpamConf struct {
 	// PreferredPool is the master address pool for containers and network interfaces.
 	PreferredPool string
@@ -92,6 +94,28 @@ func (c *IpamConf) Validate() error {
 	return nil
 }
 
+// Contains checks whether the ipamSubnet contains [addr].
+func (c *IpamConf) Contains(addr net.IP) bool {
+	if c == nil {
+		return false
+	}
+	if c.PreferredPool == "" {
+		return false
+	}
+
+	_, allowedRange, _ := net.ParseCIDR(c.PreferredPool)
+	if c.SubPool != "" {
+		_, allowedRange, _ = net.ParseCIDR(c.SubPool)
+	}
+
+	return allowedRange.Contains(addr)
+}
+
+// IsStatic checks whether the subnet was statically allocated (ie. user-defined).
+func (c *IpamConf) IsStatic() bool {
+	return c != nil && c.PreferredPool != ""
+}
+
 // IpamInfo contains all the ipam related operational info for a network
 type IpamInfo struct {
 	PoolID string
@@ -150,7 +174,7 @@ type Network struct {
 	created          time.Time
 	scope            string // network data scope
 	labels           map[string]string
-	ipamType         string
+	ipamType         string // ipamType is the name of the IPAM driver
 	ipamOptions      map[string]string
 	addrSpace        string
 	ipamV4Config     []*IpamConf