daemon: Improve NetworkingConfig & EndpointSettings validation

So far, only a subset of NetworkingConfig was validated when calling
ContainerCreate. Other parameters would be validated when the container
was started. And the same goes for EndpointSettings on NetworkConnect.

This commit adds two validation steps:

1. Check if the IP addresses set in endpoint's IPAMConfig are valid,
   when ContainerCreate and ConnectToNetwork is called ;
2. Check if the network allows static IP addresses, only on
   ConnectToNetwork as we need the libnetwork's Network for that and it
   might not exist until NetworkAttachment requests are sent to the
   Swarm leader (which happens only when starting the container) ;

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
This commit is contained in:
Albin Kerouanton 2023-08-09 22:18:12 +02:00
parent a33043f0e0
commit ff503882f7
No known key found for this signature in database
GPG key ID: 630B8E1DCBDB1864
6 changed files with 50 additions and 52 deletions

View file

@ -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:

View file

@ -562,39 +562,56 @@ 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 might be nil as a container
// can be created with a reference to a network that don't exist yet. In that case, only partial validation will be
// done.
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() {
ipamConfig := &networktypes.EndpointIPAMConfig{}
if epConfig.IPAMConfig != nil {
ipamConfig = epConfig.IPAMConfig
}
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() {
return runconfig.ErrUnsupportedNetworkAndIP
}
if len(epConfig.Aliases) > 0 && !serviceDiscoveryOnDefaultNetwork() {
return runconfig.ErrUnsupportedNetworkAndAlias
}
}
if !hasUserDefinedIPAddress(epConfig.IPAMConfig) {
if ipamConfig.IPv4Address != "" {
if addr := net.ParseIP(ipamConfig.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() {
return fmt.Errorf("invalid IPv4 address: %s", ipamConfig.IPv4Address)
}
}
if ipamConfig.IPv6Address != "" {
if addr := net.ParseIP(ipamConfig.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() {
return fmt.Errorf("invalid IPv6 address: %s", ipamConfig.IPv6Address)
}
}
if nw == nil {
return nil
}
_, _, nwIPv4Configs, nwIPv6Configs := n.IpamConfig()
_, _, nwIPv4Configs, nwIPv6Configs := nw.IpamConfig()
for _, s := range []struct {
ipConfigured bool
subnetConfigs []*libnetwork.IpamConf
}{
{
ipConfigured: len(epConfig.IPAMConfig.IPv4Address) > 0,
ipConfigured: len(ipamConfig.IPv4Address) > 0,
subnetConfigs: nwIPv4Configs,
},
{
ipConfigured: len(epConfig.IPAMConfig.IPv6Address) > 0,
ipConfigured: len(ipamConfig.IPv6Address) > 0,
subnetConfigs: nwIPv6Configs,
},
} {
@ -657,7 +674,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
}

View file

@ -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
}

View file

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"net"
"runtime"
"strings"
"time"
@ -98,7 +97,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,8 +320,8 @@ 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
}
@ -331,20 +330,14 @@ func verifyNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error {
if v == nil {
return fmt.Errorf("no EndpointSettings for %s", k)
}
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 {
return err
}
}
return nil
}

View file

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

View file

@ -47,6 +47,9 @@ 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`.
Note that this change is _unversioned_ and applies to all API versions.
## v1.43 API changes