Skip to content

Commit 4f47013

Browse files
akerouantonthaJeztah
authored andcommitted
api: Validate IPAM config before creating a network
Currently, IPAM config is never validated by the API. Some checks are done by the CLI, but they're not exhaustive. And some of these misconfigurations might be caught early by libnetwork (ie. when the network is created), and others only surface when connecting a container to a misconfigured network. In both cases, the API would return a 500. Although the `NetworkCreate` endpoint might already return warnings, these are never displayed by the CLI. As such, it was decided during a maintainer's call to return validation errors _for all API versions_. Signed-off-by: Albin Kerouanton <[email protected]> Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent d146e59 commit 4f47013

25 files changed

Lines changed: 349 additions & 1 deletion

api/swagger.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9929,6 +9929,10 @@ paths:
99299929
example:
99309930
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
99319931
Warning: ""
9932+
400:
9933+
description: "bad parameter"
9934+
schema:
9935+
$ref: "#/definitions/ErrorResponse"
99329936
403:
99339937
description: |
99349938
Forbidden operation. This happens when trying to create a network named after a pre-defined network,

api/types/network/ipam.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
package network
22

3+
import (
4+
"errors"
5+
"fmt"
6+
"net/netip"
7+
8+
"github.com/docker/docker/internal/multierror"
9+
)
10+
311
// IPAM represents IP Address Management
412
type IPAM struct {
513
Driver string
@@ -14,3 +22,111 @@ type IPAMConfig struct {
1422
Gateway string `json:",omitempty"`
1523
AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"`
1624
}
25+
26+
type ipFamily string
27+
28+
const (
29+
ip4 ipFamily = "IPv4"
30+
ip6 ipFamily = "IPv6"
31+
)
32+
33+
func ValidateIPAM(ipam *IPAM, enableIPv6 bool) error {
34+
if ipam == nil {
35+
return nil
36+
}
37+
38+
var errs []error
39+
for _, cfg := range ipam.Config {
40+
subnet, err := netip.ParsePrefix(cfg.Subnet)
41+
if err != nil {
42+
errs = append(errs, fmt.Errorf("invalid subnet %s: invalid CIDR block notation", cfg.Subnet))
43+
continue
44+
}
45+
subnetFamily := ip4
46+
if subnet.Addr().Is6() {
47+
subnetFamily = ip6
48+
}
49+
50+
if subnet != subnet.Masked() {
51+
errs = append(errs, fmt.Errorf("invalid subnet %s: it should be %s", subnet, subnet.Masked()))
52+
}
53+
54+
if !enableIPv6 && subnetFamily == ip6 {
55+
errs = append(errs, fmt.Errorf("invalid subnet %s: IPv6 has not been enabled for this network", subnet))
56+
}
57+
58+
if ipRangeErrs := validateIPRange(cfg.IPRange, subnet, subnetFamily); len(ipRangeErrs) > 0 {
59+
errs = append(errs, ipRangeErrs...)
60+
}
61+
62+
if err := validateAddress(cfg.Gateway, subnet, subnetFamily); err != nil {
63+
errs = append(errs, fmt.Errorf("invalid gateway %s: %w", cfg.Gateway, err))
64+
}
65+
66+
for auxName, aux := range cfg.AuxAddress {
67+
if err := validateAddress(aux, subnet, subnetFamily); err != nil {
68+
errs = append(errs, fmt.Errorf("invalid auxiliary address %s: %w", auxName, err))
69+
}
70+
}
71+
}
72+
73+
if err := multierror.Join(errs...); err != nil {
74+
return fmt.Errorf("invalid network config:\n%w", err)
75+
}
76+
77+
return nil
78+
}
79+
80+
func validateIPRange(ipRange string, subnet netip.Prefix, subnetFamily ipFamily) []error {
81+
if ipRange == "" {
82+
return nil
83+
}
84+
prefix, err := netip.ParsePrefix(ipRange)
85+
if err != nil {
86+
return []error{fmt.Errorf("invalid ip-range %s: invalid CIDR block notation", ipRange)}
87+
}
88+
family := ip4
89+
if prefix.Addr().Is6() {
90+
family = ip6
91+
}
92+
93+
if family != subnetFamily {
94+
return []error{fmt.Errorf("invalid ip-range %s: parent subnet is an %s block", ipRange, subnetFamily)}
95+
}
96+
97+
var errs []error
98+
if prefix.Bits() < subnet.Bits() {
99+
errs = append(errs, fmt.Errorf("invalid ip-range %s: CIDR block is bigger than its parent subnet %s", ipRange, subnet))
100+
}
101+
if prefix != prefix.Masked() {
102+
errs = append(errs, fmt.Errorf("invalid ip-range %s: it should be %s", prefix, prefix.Masked()))
103+
}
104+
if !subnet.Overlaps(prefix) {
105+
errs = append(errs, fmt.Errorf("invalid ip-range %s: parent subnet %s doesn't contain ip-range", ipRange, subnet))
106+
}
107+
108+
return errs
109+
}
110+
111+
func validateAddress(address string, subnet netip.Prefix, subnetFamily ipFamily) error {
112+
if address == "" {
113+
return nil
114+
}
115+
addr, err := netip.ParseAddr(address)
116+
if err != nil {
117+
return errors.New("invalid address")
118+
}
119+
family := ip4
120+
if addr.Is6() {
121+
family = ip6
122+
}
123+
124+
if family != subnetFamily {
125+
return fmt.Errorf("parent subnet is an %s block", subnetFamily)
126+
}
127+
if !subnet.Contains(addr) {
128+
return fmt.Errorf("parent subnet %s doesn't contain this address", subnet)
129+
}
130+
131+
return nil
132+
}

api/types/network/ipam_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package network
2+
3+
import (
4+
"testing"
5+
6+
"gotest.tools/v3/assert"
7+
is "gotest.tools/v3/assert/cmp"
8+
)
9+
10+
func TestNetworkWithInvalidIPAM(t *testing.T) {
11+
testcases := []struct {
12+
name string
13+
ipam IPAM
14+
ipv6 bool
15+
expectedErrors []string
16+
}{
17+
{
18+
name: "IP version mismatch",
19+
ipam: IPAM{
20+
Config: []IPAMConfig{{
21+
Subnet: "10.10.10.0/24",
22+
IPRange: "2001:db8::/32",
23+
Gateway: "2001:db8::1",
24+
AuxAddress: map[string]string{"DefaultGatewayIPv4": "2001:db8::1"},
25+
}},
26+
},
27+
expectedErrors: []string{
28+
"invalid ip-range 2001:db8::/32: parent subnet is an IPv4 block",
29+
"invalid gateway 2001:db8::1: parent subnet is an IPv4 block",
30+
"invalid auxiliary address DefaultGatewayIPv4: parent subnet is an IPv4 block",
31+
},
32+
},
33+
{
34+
name: "IPv6 subnet is discarded when IPv6 is disabled",
35+
ipam: IPAM{Config: []IPAMConfig{{Subnet: "2001:db8::/32"}}},
36+
ipv6: false,
37+
expectedErrors: []string{"invalid subnet 2001:db8::/32: IPv6 has not been enabled for this network"},
38+
},
39+
{
40+
name: "Invalid data - Subnet",
41+
ipam: IPAM{Config: []IPAMConfig{{Subnet: "foobar"}}},
42+
expectedErrors: []string{
43+
`invalid subnet foobar: invalid CIDR block notation`,
44+
},
45+
},
46+
{
47+
name: "Invalid data",
48+
ipam: IPAM{
49+
Config: []IPAMConfig{{
50+
Subnet: "10.10.10.0/24",
51+
IPRange: "foobar",
52+
Gateway: "1001.10.5.3",
53+
AuxAddress: map[string]string{"DefaultGatewayIPv4": "dummy"},
54+
}},
55+
},
56+
expectedErrors: []string{
57+
"invalid ip-range foobar: invalid CIDR block notation",
58+
"invalid gateway 1001.10.5.3: invalid address",
59+
"invalid auxiliary address DefaultGatewayIPv4: invalid address",
60+
},
61+
},
62+
{
63+
name: "IPRange bigger than its subnet",
64+
ipam: IPAM{
65+
Config: []IPAMConfig{
66+
{Subnet: "10.10.10.0/24", IPRange: "10.0.0.0/8"},
67+
},
68+
},
69+
expectedErrors: []string{
70+
"invalid ip-range 10.0.0.0/8: CIDR block is bigger than its parent subnet 10.10.10.0/24",
71+
},
72+
},
73+
{
74+
name: "Out of range prefix & addresses",
75+
ipam: IPAM{
76+
Config: []IPAMConfig{{
77+
Subnet: "10.0.0.0/8",
78+
IPRange: "192.168.0.1/24",
79+
Gateway: "192.168.0.1",
80+
AuxAddress: map[string]string{"DefaultGatewayIPv4": "192.168.0.1"},
81+
}},
82+
},
83+
expectedErrors: []string{
84+
"invalid ip-range 192.168.0.1/24: it should be 192.168.0.0/24",
85+
"invalid ip-range 192.168.0.1/24: parent subnet 10.0.0.0/8 doesn't contain ip-range",
86+
"invalid gateway 192.168.0.1: parent subnet 10.0.0.0/8 doesn't contain this address",
87+
"invalid auxiliary address DefaultGatewayIPv4: parent subnet 10.0.0.0/8 doesn't contain this address",
88+
},
89+
},
90+
{
91+
name: "Subnet with host fragment set",
92+
ipam: IPAM{
93+
Config: []IPAMConfig{{
94+
Subnet: "10.10.10.0/8",
95+
}},
96+
},
97+
expectedErrors: []string{"invalid subnet 10.10.10.0/8: it should be 10.0.0.0/8"},
98+
},
99+
{
100+
name: "IPRange with host fragment set",
101+
ipam: IPAM{
102+
Config: []IPAMConfig{{
103+
Subnet: "10.0.0.0/8",
104+
IPRange: "10.10.10.0/16",
105+
}},
106+
},
107+
expectedErrors: []string{"invalid ip-range 10.10.10.0/16: it should be 10.10.0.0/16"},
108+
},
109+
{
110+
name: "Empty IPAM is valid",
111+
ipam: IPAM{},
112+
},
113+
{
114+
name: "Valid IPAM",
115+
ipam: IPAM{
116+
Config: []IPAMConfig{{
117+
Subnet: "10.0.0.0/8",
118+
IPRange: "10.10.0.0/16",
119+
Gateway: "10.10.0.1",
120+
AuxAddress: map[string]string{"DefaultGatewayIPv4": "10.10.0.1"},
121+
}},
122+
},
123+
},
124+
}
125+
126+
for _, tc := range testcases {
127+
tc := tc
128+
t.Run(tc.name, func(t *testing.T) {
129+
t.Parallel()
130+
131+
errs := ValidateIPAM(&tc.ipam, tc.ipv6)
132+
if tc.expectedErrors == nil {
133+
assert.NilError(t, errs)
134+
return
135+
}
136+
137+
assert.Check(t, is.ErrorContains(errs, "invalid network config"))
138+
for _, expected := range tc.expectedErrors {
139+
assert.Check(t, is.ErrorContains(errs, expected))
140+
}
141+
})
142+
}
143+
}

daemon/network.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,9 @@ func (daemon *Daemon) createNetwork(cfg *config.Config, create types.NetworkCrea
346346
nwOptions = append(nwOptions, libnetwork.NetworkOptionConfigOnly())
347347
}
348348

349+
if err := network.ValidateIPAM(create.IPAM, create.EnableIPv6); err != nil {
350+
return nil, errdefs.InvalidParameter(err)
351+
}
349352
if create.IPAM != nil {
350353
ipam := create.IPAM
351354
v4Conf, v6Conf, err := getIpamConfig(ipam.Config)

docs/api/v1.25.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6149,6 +6149,10 @@ paths:
61496149
example:
61506150
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
61516151
Warning: ""
6152+
400:
6153+
description: "bad parameter"
6154+
schema:
6155+
$ref: "#/definitions/ErrorResponse"
61526156
403:
61536157
description: "operation not supported for pre-defined networks"
61546158
schema:

docs/api/v1.26.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6158,6 +6158,10 @@ paths:
61586158
example:
61596159
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
61606160
Warning: ""
6161+
400:
6162+
description: "bad parameter"
6163+
schema:
6164+
$ref: "#/definitions/ErrorResponse"
61616165
403:
61626166
description: "operation not supported for pre-defined networks"
61636167
schema:

docs/api/v1.27.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6229,6 +6229,10 @@ paths:
62296229
example:
62306230
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
62316231
Warning: ""
6232+
400:
6233+
description: "bad parameter"
6234+
schema:
6235+
$ref: "#/definitions/ErrorResponse"
62326236
403:
62336237
description: "operation not supported for pre-defined networks"
62346238
schema:

docs/api/v1.28.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6358,6 +6358,10 @@ paths:
63586358
example:
63596359
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
63606360
Warning: ""
6361+
400:
6362+
description: "bad parameter"
6363+
schema:
6364+
$ref: "#/definitions/ErrorResponse"
63616365
403:
63626366
description: "operation not supported for pre-defined networks"
63636367
schema:

docs/api/v1.29.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6396,6 +6396,10 @@ paths:
63966396
example:
63976397
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
63986398
Warning: ""
6399+
400:
6400+
description: "bad parameter"
6401+
schema:
6402+
$ref: "#/definitions/ErrorResponse"
63996403
403:
64006404
description: "operation not supported for pre-defined networks"
64016405
schema:

docs/api/v1.30.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6659,6 +6659,10 @@ paths:
66596659
example:
66606660
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
66616661
Warning: ""
6662+
400:
6663+
description: "bad parameter"
6664+
schema:
6665+
$ref: "#/definitions/ErrorResponse"
66626666
403:
66636667
description: "operation not supported for pre-defined networks"
66646668
schema:

0 commit comments

Comments
 (0)