Merge pull request #45759 from akerouanton/validate-ipam-config

Validate IPAM config before handing it over to libnetwork
This commit is contained in:
Bjorn Neergaard 2023-08-22 13:58:28 -06:00 committed by GitHub
commit 8383430946
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 373 additions and 16 deletions

View file

@ -9929,6 +9929,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: |
Forbidden operation. This happens when trying to create a network named after a pre-defined network,

132
api/types/network/ipam.go Normal file
View file

@ -0,0 +1,132 @@
package network
import (
"errors"
"fmt"
"net/netip"
"github.com/docker/docker/internal/multierror"
)
// IPAM represents IP Address Management
type IPAM struct {
Driver string
Options map[string]string // Per network IPAM driver options
Config []IPAMConfig
}
// IPAMConfig represents IPAM configurations
type IPAMConfig struct {
Subnet string `json:",omitempty"`
IPRange string `json:",omitempty"`
Gateway string `json:",omitempty"`
AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"`
}
type ipFamily string
const (
ip4 ipFamily = "IPv4"
ip6 ipFamily = "IPv6"
)
func ValidateIPAM(ipam *IPAM, enableIPv6 bool) error {
if ipam == nil {
return nil
}
var errs []error
for _, cfg := range ipam.Config {
subnet, err := netip.ParsePrefix(cfg.Subnet)
if err != nil {
errs = append(errs, fmt.Errorf("invalid subnet %s: invalid CIDR block notation", cfg.Subnet))
continue
}
subnetFamily := ip4
if subnet.Addr().Is6() {
subnetFamily = ip6
}
if subnet != subnet.Masked() {
errs = append(errs, fmt.Errorf("invalid subnet %s: it should be %s", subnet, subnet.Masked()))
}
if !enableIPv6 && subnetFamily == ip6 {
errs = append(errs, fmt.Errorf("invalid subnet %s: IPv6 has not been enabled for this network", subnet))
}
if ipRangeErrs := validateIPRange(cfg.IPRange, subnet, subnetFamily); len(ipRangeErrs) > 0 {
errs = append(errs, ipRangeErrs...)
}
if err := validateAddress(cfg.Gateway, subnet, subnetFamily); err != nil {
errs = append(errs, fmt.Errorf("invalid gateway %s: %w", cfg.Gateway, err))
}
for auxName, aux := range cfg.AuxAddress {
if err := validateAddress(aux, subnet, subnetFamily); err != nil {
errs = append(errs, fmt.Errorf("invalid auxiliary address %s: %w", auxName, err))
}
}
}
if err := multierror.Join(errs...); err != nil {
return fmt.Errorf("invalid network config:\n%w", err)
}
return nil
}
func validateIPRange(ipRange string, subnet netip.Prefix, subnetFamily ipFamily) []error {
if ipRange == "" {
return nil
}
prefix, err := netip.ParsePrefix(ipRange)
if err != nil {
return []error{fmt.Errorf("invalid ip-range %s: invalid CIDR block notation", ipRange)}
}
family := ip4
if prefix.Addr().Is6() {
family = ip6
}
if family != subnetFamily {
return []error{fmt.Errorf("invalid ip-range %s: parent subnet is an %s block", ipRange, subnetFamily)}
}
var errs []error
if prefix.Bits() < subnet.Bits() {
errs = append(errs, fmt.Errorf("invalid ip-range %s: CIDR block is bigger than its parent subnet %s", ipRange, subnet))
}
if prefix != prefix.Masked() {
errs = append(errs, fmt.Errorf("invalid ip-range %s: it should be %s", prefix, prefix.Masked()))
}
if !subnet.Overlaps(prefix) {
errs = append(errs, fmt.Errorf("invalid ip-range %s: parent subnet %s doesn't contain ip-range", ipRange, subnet))
}
return errs
}
func validateAddress(address string, subnet netip.Prefix, subnetFamily ipFamily) error {
if address == "" {
return nil
}
addr, err := netip.ParseAddr(address)
if err != nil {
return errors.New("invalid address")
}
family := ip4
if addr.Is6() {
family = ip6
}
if family != subnetFamily {
return fmt.Errorf("parent subnet is an %s block", subnetFamily)
}
if !subnet.Contains(addr) {
return fmt.Errorf("parent subnet %s doesn't contain this address", subnet)
}
return nil
}

View file

@ -0,0 +1,143 @@
package network
import (
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestNetworkWithInvalidIPAM(t *testing.T) {
testcases := []struct {
name string
ipam IPAM
ipv6 bool
expectedErrors []string
}{
{
name: "IP version mismatch",
ipam: IPAM{
Config: []IPAMConfig{{
Subnet: "10.10.10.0/24",
IPRange: "2001:db8::/32",
Gateway: "2001:db8::1",
AuxAddress: map[string]string{"DefaultGatewayIPv4": "2001:db8::1"},
}},
},
expectedErrors: []string{
"invalid ip-range 2001:db8::/32: parent subnet is an IPv4 block",
"invalid gateway 2001:db8::1: parent subnet is an IPv4 block",
"invalid auxiliary address DefaultGatewayIPv4: parent subnet is an IPv4 block",
},
},
{
name: "IPv6 subnet is discarded when IPv6 is disabled",
ipam: IPAM{Config: []IPAMConfig{{Subnet: "2001:db8::/32"}}},
ipv6: false,
expectedErrors: []string{"invalid subnet 2001:db8::/32: IPv6 has not been enabled for this network"},
},
{
name: "Invalid data - Subnet",
ipam: IPAM{Config: []IPAMConfig{{Subnet: "foobar"}}},
expectedErrors: []string{
`invalid subnet foobar: invalid CIDR block notation`,
},
},
{
name: "Invalid data",
ipam: IPAM{
Config: []IPAMConfig{{
Subnet: "10.10.10.0/24",
IPRange: "foobar",
Gateway: "1001.10.5.3",
AuxAddress: map[string]string{"DefaultGatewayIPv4": "dummy"},
}},
},
expectedErrors: []string{
"invalid ip-range foobar: invalid CIDR block notation",
"invalid gateway 1001.10.5.3: invalid address",
"invalid auxiliary address DefaultGatewayIPv4: invalid address",
},
},
{
name: "IPRange bigger than its subnet",
ipam: IPAM{
Config: []IPAMConfig{
{Subnet: "10.10.10.0/24", IPRange: "10.0.0.0/8"},
},
},
expectedErrors: []string{
"invalid ip-range 10.0.0.0/8: CIDR block is bigger than its parent subnet 10.10.10.0/24",
},
},
{
name: "Out of range prefix & addresses",
ipam: IPAM{
Config: []IPAMConfig{{
Subnet: "10.0.0.0/8",
IPRange: "192.168.0.1/24",
Gateway: "192.168.0.1",
AuxAddress: map[string]string{"DefaultGatewayIPv4": "192.168.0.1"},
}},
},
expectedErrors: []string{
"invalid ip-range 192.168.0.1/24: it should be 192.168.0.0/24",
"invalid ip-range 192.168.0.1/24: parent subnet 10.0.0.0/8 doesn't contain ip-range",
"invalid gateway 192.168.0.1: parent subnet 10.0.0.0/8 doesn't contain this address",
"invalid auxiliary address DefaultGatewayIPv4: parent subnet 10.0.0.0/8 doesn't contain this address",
},
},
{
name: "Subnet with host fragment set",
ipam: IPAM{
Config: []IPAMConfig{{
Subnet: "10.10.10.0/8",
}},
},
expectedErrors: []string{"invalid subnet 10.10.10.0/8: it should be 10.0.0.0/8"},
},
{
name: "IPRange with host fragment set",
ipam: IPAM{
Config: []IPAMConfig{{
Subnet: "10.0.0.0/8",
IPRange: "10.10.10.0/16",
}},
},
expectedErrors: []string{"invalid ip-range 10.10.10.0/16: it should be 10.10.0.0/16"},
},
{
name: "Empty IPAM is valid",
ipam: IPAM{},
},
{
name: "Valid IPAM",
ipam: IPAM{
Config: []IPAMConfig{{
Subnet: "10.0.0.0/8",
IPRange: "10.10.0.0/16",
Gateway: "10.10.0.1",
AuxAddress: map[string]string{"DefaultGatewayIPv4": "10.10.0.1"},
}},
},
},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
errs := ValidateIPAM(&tc.ipam, tc.ipv6)
if tc.expectedErrors == nil {
assert.NilError(t, errs)
return
}
assert.Check(t, is.ErrorContains(errs, "invalid network config"))
for _, expected := range tc.expectedErrors {
assert.Check(t, is.ErrorContains(errs, expected))
}
})
}
}

View file

@ -9,21 +9,6 @@ type Address struct {
PrefixLen int
}
// IPAM represents IP Address Management
type IPAM struct {
Driver string
Options map[string]string // Per network IPAM driver options
Config []IPAMConfig
}
// IPAMConfig represents IPAM configurations
type IPAMConfig struct {
Subnet string `json:",omitempty"`
IPRange string `json:",omitempty"`
Gateway string `json:",omitempty"`
AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"`
}
// EndpointIPAMConfig represents IPAM configurations for the endpoint
type EndpointIPAMConfig struct {
IPv4Address string `json:",omitempty"`

View file

@ -346,6 +346,9 @@ func (daemon *Daemon) createNetwork(cfg *config.Config, create types.NetworkCrea
nwOptions = append(nwOptions, libnetwork.NetworkOptionConfigOnly())
}
if err := network.ValidateIPAM(create.IPAM, create.EnableIPv6); err != nil {
return nil, errdefs.InvalidParameter(err)
}
if create.IPAM != nil {
ipam := create.IPAM
v4Conf, v6Conf, err := getIpamConfig(ipam.Config)

View file

@ -6149,6 +6149,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -6158,6 +6158,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -6229,6 +6229,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -6358,6 +6358,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -6396,6 +6396,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -6659,6 +6659,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -6757,6 +6757,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -7798,6 +7798,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -7807,6 +7807,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -7848,6 +7848,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -7860,6 +7860,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -7902,6 +7902,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -7945,6 +7945,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -8006,6 +8006,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -8955,6 +8955,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -9292,6 +9292,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -9499,6 +9499,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -9877,6 +9877,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -9895,6 +9895,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "operation not supported for pre-defined networks"
schema:

View file

@ -38,6 +38,9 @@ keywords: "API, Docker, rcli, REST, documentation"
specifications directories. The use of the applied setting requires the daemon
to have expermental enabled, and for non-experimental daemons an empty list is
always returned.
* `POST /networks/create` now returns a 400 if the `IPAMConfig` has invalid
values. Note that this change is _unversioned_ and applied to all API
versions on daemon that support version 1.44.
## v1.43 API changes

View file

@ -23,7 +23,10 @@ source hack/make/.integration-test-helpers
# TODO re-enable test_attach_no_stream after https://github.com/docker/docker-py/issues/2513 is resolved
# TODO re-enable test_create_with_device_cgroup_rules after https://github.com/docker/docker-py/issues/2939 is resolved
# TODO re-enable test_prune_volumes after https://github.com/docker/docker-py/pull/3051 is resolved
: "${PY_TEST_OPTIONS:=--junitxml=${DEST}/junit-report.xml --deselect=tests/integration/api_container_test.py::AttachContainerTest::test_attach_no_stream --deselect=tests/integration/api_container_test.py::CreateContainerTest::test_create_with_device_cgroup_rules --deselect=tests/integration/api_volume_test.py::TestVolumes::test_prune_volumes}"
# TODO re-enable test_connect_with_ipv6_address after we updated to a version of docker-py with https://github.com/docker/docker-py/pull/3169
# TODO re-enable test_create_network_ipv6_enabled after we updated to a version of docker-py with https://github.com/docker/docker-py/pull/3169
# TODO re-enable test_create_with_ipv6_address after we updated to a version of docker-py with https://github.com/docker/docker-py/pull/3169
: "${PY_TEST_OPTIONS:=--junitxml=${DEST}/junit-report.xml --deselect=tests/integration/api_container_test.py::AttachContainerTest::test_attach_no_stream --deselect=tests/integration/api_container_test.py::CreateContainerTest::test_create_with_device_cgroup_rules --deselect=tests/integration/api_volume_test.py::TestVolumes::test_prune_volumes --deselect=tests/integration/api_network_test.py::TestNetworks::test_connect_with_ipv6_address --deselect=tests/integration/api_network_test.py::TestNetworks::test_create_network_ipv6_enabled --deselect=tests/integration/api_network_test.py::TestNetworks::test_create_with_ipv6_address}"
(
bundle .integration-daemon-start

View file

@ -106,6 +106,14 @@ func (a *Allocator) RequestPool(addressSpace, requestedPool, requestedSubPool st
}
k.Subnet, k.ChildSubnet = k.Subnet.Masked(), k.ChildSubnet.Masked()
// Prior to https://github.com/moby/moby/pull/44968, libnetwork would happily accept a ChildSubnet with a bigger
// mask than its parent subnet. In such case, it was producing IP addresses based on the parent subnet, and the
// child subnet was not allocated from the address pool. Following condition take care of restoring this behavior
// for networks created before upgrading to v24.0.
if k.ChildSubnet.IsValid() && k.ChildSubnet.Bits() < k.Subnet.Bits() {
k.ChildSubnet = k.Subnet
}
err = aSpace.allocateSubnet(k.Subnet, k.ChildSubnet)
if err != nil {
return "", nil, nil, err