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: responses:
200: 200:
description: "No error" description: "No error"
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403: 403:
description: "Operation not supported for swarm scoped networks" description: "Operation not supported for swarm scoped networks"
schema: schema:

View file

@ -562,39 +562,56 @@ func (daemon *Daemon) allocateNetwork(cfg *config.Config, container *container.C
return nil return nil
} }
// hasUserDefinedIPAddress returns whether the passed IPAM configuration contains IP address configuration // validateEndpointSettings checks whether the given epConfig is valid. The nw parameter might be nil as a container
func hasUserDefinedIPAddress(ipamConfig *networktypes.EndpointIPAMConfig) bool { // can be created with a reference to a network that don't exist yet. In that case, only partial validation will be
return ipamConfig != nil && (len(ipamConfig.IPv4Address) > 0 || len(ipamConfig.IPv6Address) > 0) // done.
} func validateEndpointSettings(nw *libnetwork.Network, nwName string, epConfig *networktypes.EndpointSettings) error {
if epConfig == nil {
// 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 {
return 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 return runconfig.ErrUnsupportedNetworkAndIP
} }
if len(epConfig.Aliases) > 0 && !serviceDiscoveryOnDefaultNetwork() { if len(epConfig.Aliases) > 0 && !serviceDiscoveryOnDefaultNetwork() {
return runconfig.ErrUnsupportedNetworkAndAlias 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 return nil
} }
_, _, nwIPv4Configs, nwIPv6Configs := n.IpamConfig() _, _, nwIPv4Configs, nwIPv6Configs := nw.IpamConfig()
for _, s := range []struct { for _, s := range []struct {
ipConfigured bool ipConfigured bool
subnetConfigs []*libnetwork.IpamConf subnetConfigs []*libnetwork.IpamConf
}{ }{
{ {
ipConfigured: len(epConfig.IPAMConfig.IPv4Address) > 0, ipConfigured: len(ipamConfig.IPv4Address) > 0,
subnetConfigs: nwIPv4Configs, subnetConfigs: nwIPv4Configs,
}, },
{ {
ipConfigured: len(epConfig.IPAMConfig.IPv6Address) > 0, ipConfigured: len(ipamConfig.IPv6Address) > 0,
subnetConfigs: nwIPv6Configs, 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 return err
} }

View file

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

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net"
"runtime" "runtime"
"strings" "strings"
"time" "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 { if err != nil {
return containertypes.CreateResponse{Warnings: warnings}, errdefs.InvalidParameter(err) return containertypes.CreateResponse{Warnings: warnings}, errdefs.InvalidParameter(err)
} }
@ -321,8 +320,8 @@ func (daemon *Daemon) mergeAndVerifyConfig(config *containertypes.Config, img *i
return nil return nil
} }
// verifyNetworkingConfig validates if the nwConfig is valid. // validateNetworkingConfig checks whether a container's NetworkingConfig is valid.
func verifyNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error { func (daemon *Daemon) validateNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error {
if nwConfig == nil { if nwConfig == nil {
return nil return nil
} }
@ -331,20 +330,14 @@ func verifyNetworkingConfig(nwConfig *networktypes.NetworkingConfig) error {
if v == nil { if v == nil {
return fmt.Errorf("no EndpointSettings for %s", k) return fmt.Errorf("no EndpointSettings for %s", k)
} }
if v.IPAMConfig != nil {
if v.IPAMConfig.IPv4Address != "" && net.ParseIP(v.IPAMConfig.IPv4Address).To4() == nil { // The referenced network k might not exist when the container is created, so just ignore the error in that case.
return fmt.Errorf("invalid IPv4 address: %s", v.IPAMConfig.IPv4Address) nw, _ := daemon.FindNetwork(k)
} if err := validateEndpointSettings(nw, k, v); err != nil {
if v.IPAMConfig.IPv6Address != "" { return err
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)
}
}
} }
} }
return nil 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. 1.44.
* `POST /containers/create` now accepts multiple `EndpointSettings` in * `POST /containers/create` now accepts multiple `EndpointSettings` in
`NetworkingConfig.EndpointSettings`. `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 ## v1.43 API changes