Browse code

api/t/network: validate EndpointIPAMConfig in daemon

Clients should not make assumptions about the validity of an API struct
as the set of well-formed values may differ across daemon versions.
Remove it from the API module so client-application authors are not
tempted to apply it, which would restrict the forward compatibility of
the client.

Signed-off-by: Cory Snider <csnider@mirantis.com>

Cory Snider authored on 2025/09/19 04:48:05
Showing 5 changed files
... ...
@@ -1,10 +1,7 @@
1 1
 package network
2 2
 
3 3
 import (
4
-	"errors"
5
-	"fmt"
6 4
 	"maps"
7
-	"net"
8 5
 	"slices"
9 6
 )
10 7
 
... ...
@@ -71,76 +68,3 @@ func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig {
71 71
 	cfgCopy.LinkLocalIPs = slices.Clone(cfg.LinkLocalIPs)
72 72
 	return &cfgCopy
73 73
 }
74
-
75
-// NetworkSubnet describes a user-defined subnet for a specific network. It's only used to validate if an
76
-// EndpointIPAMConfig is valid for a specific network.
77
-type NetworkSubnet interface {
78
-	// Contains checks whether the NetworkSubnet contains [addr].
79
-	Contains(addr net.IP) bool
80
-	// IsStatic checks whether the subnet was statically allocated (ie. user-defined).
81
-	IsStatic() bool
82
-}
83
-
84
-// IsInRange checks whether static IP addresses are valid in a specific network.
85
-func (cfg *EndpointIPAMConfig) IsInRange(v4Subnets []NetworkSubnet, v6Subnets []NetworkSubnet) error {
86
-	var errs []error
87
-
88
-	if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil {
89
-		errs = append(errs, err)
90
-	}
91
-	if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil {
92
-		errs = append(errs, err)
93
-	}
94
-
95
-	return errJoin(errs...)
96
-}
97
-
98
-func validateEndpointIPAddress(epAddr string, ipamSubnets []NetworkSubnet) error {
99
-	if epAddr == "" {
100
-		return nil
101
-	}
102
-
103
-	var staticSubnet bool
104
-	parsedAddr := net.ParseIP(epAddr)
105
-	for _, subnet := range ipamSubnets {
106
-		if subnet.IsStatic() {
107
-			staticSubnet = true
108
-			if subnet.Contains(parsedAddr) {
109
-				return nil
110
-			}
111
-		}
112
-	}
113
-
114
-	if staticSubnet {
115
-		return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr)
116
-	}
117
-
118
-	return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets")
119
-}
120
-
121
-// Validate checks whether cfg is valid.
122
-func (cfg *EndpointIPAMConfig) Validate() error {
123
-	if cfg == nil {
124
-		return nil
125
-	}
126
-
127
-	var errs []error
128
-
129
-	if cfg.IPv4Address != "" {
130
-		if addr := net.ParseIP(cfg.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() {
131
-			errs = append(errs, fmt.Errorf("invalid IPv4 address: %s", cfg.IPv4Address))
132
-		}
133
-	}
134
-	if cfg.IPv6Address != "" {
135
-		if addr := net.ParseIP(cfg.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() {
136
-			errs = append(errs, fmt.Errorf("invalid IPv6 address: %s", cfg.IPv6Address))
137
-		}
138
-	}
139
-	for _, addr := range cfg.LinkLocalIPs {
140
-		if parsed := net.ParseIP(addr); parsed == nil || parsed.IsUnspecified() {
141
-			errs = append(errs, fmt.Errorf("invalid link-local IP address: %s", addr))
142
-		}
143
-	}
144
-
145
-	return errJoin(errs...)
146
-}
147 74
deleted file mode 100644
... ...
@@ -1,185 +0,0 @@
1
-package network
2
-
3
-import (
4
-	"net"
5
-	"testing"
6
-
7
-	"gotest.tools/v3/assert"
8
-	is "gotest.tools/v3/assert/cmp"
9
-)
10
-
11
-type subnetStub struct {
12
-	static   bool
13
-	contains map[string]bool
14
-}
15
-
16
-func (stub subnetStub) IsStatic() bool {
17
-	return stub.static
18
-}
19
-
20
-func (stub subnetStub) Contains(addr net.IP) bool {
21
-	v, ok := stub.contains[addr.String()]
22
-	return ok && v
23
-}
24
-
25
-func TestEndpointIPAMConfigWithOutOfRangeAddrs(t *testing.T) {
26
-	tests := []struct {
27
-		name           string
28
-		ipamConfig     *EndpointIPAMConfig
29
-		v4Subnets      []NetworkSubnet
30
-		v6Subnets      []NetworkSubnet
31
-		expectedErrors []string
32
-	}{
33
-		{
34
-			name: "valid config",
35
-			ipamConfig: &EndpointIPAMConfig{
36
-				IPv4Address:  "192.168.100.10",
37
-				IPv6Address:  "2a01:d2:af:420b:25c1:1816:bb33:855c",
38
-				LinkLocalIPs: []string{"169.254.169.254", "fe80::42:a8ff:fe33:6230"},
39
-			},
40
-			v4Subnets: []NetworkSubnet{
41
-				subnetStub{static: true, contains: map[string]bool{"192.168.100.10": true}},
42
-			},
43
-			v6Subnets: []NetworkSubnet{
44
-				subnetStub{static: true, contains: map[string]bool{"2a01:d2:af:420b:25c1:1816:bb33:855c": true}},
45
-			},
46
-		},
47
-		{
48
-			name: "static addresses out of range",
49
-			ipamConfig: &EndpointIPAMConfig{
50
-				IPv4Address: "192.168.100.10",
51
-				IPv6Address: "2a01:d2:af:420b:25c1:1816:bb33:855c",
52
-			},
53
-			v4Subnets: []NetworkSubnet{
54
-				subnetStub{static: true, contains: map[string]bool{"192.168.100.10": false}},
55
-			},
56
-			v6Subnets: []NetworkSubnet{
57
-				subnetStub{static: true, contains: map[string]bool{"2a01:d2:af:420b:25c1:1816:bb33:855c": false}},
58
-			},
59
-			expectedErrors: []string{
60
-				"no configured subnet or ip-range contain the IP address 192.168.100.10",
61
-				"no configured subnet or ip-range contain the IP address 2a01:d2:af:420b:25c1:1816:bb33:855c",
62
-			},
63
-		},
64
-		{
65
-			name: "static addresses with dynamic network subnets",
66
-			ipamConfig: &EndpointIPAMConfig{
67
-				IPv4Address: "192.168.100.10",
68
-				IPv6Address: "2a01:d2:af:420b:25c1:1816:bb33:855c",
69
-			},
70
-			v4Subnets: []NetworkSubnet{
71
-				subnetStub{static: false},
72
-			},
73
-			v6Subnets: []NetworkSubnet{
74
-				subnetStub{static: false},
75
-			},
76
-			expectedErrors: []string{
77
-				"user specified IP address is supported only when connecting to networks with user configured subnets",
78
-				"user specified IP address is supported only when connecting to networks with user configured subnets",
79
-			},
80
-		},
81
-	}
82
-
83
-	for _, tc := range tests {
84
-		t.Run(tc.name, func(t *testing.T) {
85
-			t.Parallel()
86
-
87
-			err := tc.ipamConfig.IsInRange(tc.v4Subnets, tc.v6Subnets)
88
-			if tc.expectedErrors == nil {
89
-				assert.NilError(t, err)
90
-				return
91
-			}
92
-
93
-			if _, ok := err.(interface{ Unwrap() []error }); !ok {
94
-				t.Fatal("returned error isn't a multierror")
95
-			}
96
-			errs := err.(interface{ Unwrap() []error }).Unwrap()
97
-			assert.Check(t, len(errs) == len(tc.expectedErrors), "errs: %+v", errs)
98
-
99
-			for _, expected := range tc.expectedErrors {
100
-				assert.Check(t, is.ErrorContains(err, expected))
101
-			}
102
-		})
103
-	}
104
-}
105
-
106
-func TestEndpointIPAMConfigWithInvalidConfig(t *testing.T) {
107
-	tests := []struct {
108
-		name           string
109
-		ipamConfig     *EndpointIPAMConfig
110
-		expectedErrors []string
111
-	}{
112
-		{
113
-			name: "valid config",
114
-			ipamConfig: &EndpointIPAMConfig{
115
-				IPv4Address:  "192.168.100.10",
116
-				IPv6Address:  "2a01:d2:af:420b:25c1:1816:bb33:855c",
117
-				LinkLocalIPs: []string{"169.254.169.254", "fe80::42:a8ff:fe33:6230"},
118
-			},
119
-		},
120
-		{
121
-			name: "invalid IP addresses",
122
-			ipamConfig: &EndpointIPAMConfig{
123
-				IPv4Address:  "foo",
124
-				IPv6Address:  "bar",
125
-				LinkLocalIPs: []string{"baz", "foobar"},
126
-			},
127
-			expectedErrors: []string{
128
-				"invalid IPv4 address: foo",
129
-				"invalid IPv6 address: bar",
130
-				"invalid link-local IP address: baz",
131
-				"invalid link-local IP address: foobar",
132
-			},
133
-		},
134
-		{
135
-			name:       "ipv6 address with a zone",
136
-			ipamConfig: &EndpointIPAMConfig{IPv6Address: "fe80::1cc0:3e8c:119f:c2e1%ens18"},
137
-			expectedErrors: []string{
138
-				"invalid IPv6 address: fe80::1cc0:3e8c:119f:c2e1%ens18",
139
-			},
140
-		},
141
-		{
142
-			name: "unspecified address is invalid",
143
-			ipamConfig: &EndpointIPAMConfig{
144
-				IPv4Address:  "0.0.0.0",
145
-				IPv6Address:  "::",
146
-				LinkLocalIPs: []string{"0.0.0.0", "::"},
147
-			},
148
-			expectedErrors: []string{
149
-				"invalid IPv4 address: 0.0.0.0",
150
-				"invalid IPv6 address: ::",
151
-				"invalid link-local IP address: 0.0.0.0",
152
-				"invalid link-local IP address: ::",
153
-			},
154
-		},
155
-		{
156
-			name: "empty link-local",
157
-			ipamConfig: &EndpointIPAMConfig{
158
-				LinkLocalIPs: []string{""},
159
-			},
160
-			expectedErrors: []string{"invalid link-local IP address:"},
161
-		},
162
-	}
163
-
164
-	for _, tc := range tests {
165
-		t.Run(tc.name, func(t *testing.T) {
166
-			t.Parallel()
167
-
168
-			err := tc.ipamConfig.Validate()
169
-			if tc.expectedErrors == nil {
170
-				assert.NilError(t, err)
171
-				return
172
-			}
173
-
174
-			if _, ok := err.(interface{ Unwrap() []error }); !ok {
175
-				t.Fatal("returned error isn't a multierror")
176
-			}
177
-			errs := err.(interface{ Unwrap() []error }).Unwrap()
178
-			assert.Check(t, len(errs) == len(tc.expectedErrors), "errs: %+v", errs)
179
-
180
-			for _, expected := range tc.expectedErrors {
181
-				assert.Check(t, is.ErrorContains(err, expected))
182
-			}
183
-		})
184
-	}
185
-}
... ...
@@ -554,26 +554,11 @@ func validateEndpointSettings(nw *libnetwork.Network, nwName string, epConfig *n
554 554
 		}
555 555
 	}
556 556
 
557
-	// TODO(aker): add a proper multierror.Append
558
-	if err := ipamConfig.Validate(); err != nil {
559
-		errs = append(errs, err.(interface{ Unwrap() []error }).Unwrap()...)
560
-	}
557
+	errs = validateEndpointIPAMConfig(errs, ipamConfig)
561 558
 
562 559
 	if nw != nil {
563 560
 		_, _, v4Configs, v6Configs := nw.IpamConfig()
564
-
565
-		var nwIPv4Subnets, nwIPv6Subnets []networktypes.NetworkSubnet
566
-		for _, nwIPAMConfig := range v4Configs {
567
-			nwIPv4Subnets = append(nwIPv4Subnets, nwIPAMConfig)
568
-		}
569
-		for _, nwIPAMConfig := range v6Configs {
570
-			nwIPv6Subnets = append(nwIPv6Subnets, nwIPAMConfig)
571
-		}
572
-
573
-		// TODO(aker): add a proper multierror.Append
574
-		if err := ipamConfig.IsInRange(nwIPv4Subnets, nwIPv6Subnets); err != nil {
575
-			errs = append(errs, err.(interface{ Unwrap() []error }).Unwrap()...)
576
-		}
561
+		errs = validateIPAMConfigIsInRange(errs, ipamConfig, v4Configs, v6Configs)
577 562
 	}
578 563
 
579 564
 	if epConfig.MacAddress != "" {
... ...
@@ -605,6 +590,65 @@ func validateEndpointSettings(nw *libnetwork.Network, nwName string, epConfig *n
605 605
 	return nil
606 606
 }
607 607
 
608
+// validateEndpointIPAMConfig checks whether cfg is valid.
609
+func validateEndpointIPAMConfig(errs []error, cfg *networktypes.EndpointIPAMConfig) []error {
610
+	if cfg == nil {
611
+		return errs
612
+	}
613
+
614
+	if cfg.IPv4Address != "" {
615
+		if addr := net.ParseIP(cfg.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() {
616
+			errs = append(errs, fmt.Errorf("invalid IPv4 address: %s", cfg.IPv4Address))
617
+		}
618
+	}
619
+	if cfg.IPv6Address != "" {
620
+		if addr := net.ParseIP(cfg.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() {
621
+			errs = append(errs, fmt.Errorf("invalid IPv6 address: %s", cfg.IPv6Address))
622
+		}
623
+	}
624
+	for _, addr := range cfg.LinkLocalIPs {
625
+		if parsed := net.ParseIP(addr); parsed == nil || parsed.IsUnspecified() {
626
+			errs = append(errs, fmt.Errorf("invalid link-local IP address: %s", addr))
627
+		}
628
+	}
629
+
630
+	return errs
631
+}
632
+
633
+// validateIPAMConfigIsInRange checks whether static IP addresses are valid in a specific network.
634
+func validateIPAMConfigIsInRange(errs []error, cfg *networktypes.EndpointIPAMConfig, v4Subnets, v6Subnets []*libnetwork.IpamConf) []error {
635
+	if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil {
636
+		errs = append(errs, err)
637
+	}
638
+	if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil {
639
+		errs = append(errs, err)
640
+	}
641
+	return errs
642
+}
643
+
644
+func validateEndpointIPAddress(epAddr string, ipamSubnets []*libnetwork.IpamConf) error {
645
+	if epAddr == "" {
646
+		return nil
647
+	}
648
+
649
+	var staticSubnet bool
650
+	parsedAddr := net.ParseIP(epAddr)
651
+	for _, subnet := range ipamSubnets {
652
+		if subnet.IsStatic() {
653
+			staticSubnet = true
654
+			if subnet.Contains(parsedAddr) {
655
+				return nil
656
+			}
657
+		}
658
+	}
659
+
660
+	if staticSubnet {
661
+		return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr)
662
+	}
663
+
664
+	return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets")
665
+}
666
+
608 667
 // cleanOperationalData resets the operational data from the passed endpoint settings
609 668
 func cleanOperationalData(es *network.EndpointSettings) {
610 669
 	es.EndpointID = ""
... ...
@@ -2,6 +2,7 @@ package daemon
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
+	"errors"
5 6
 	"testing"
6 7
 
7 8
 	containertypes "github.com/moby/moby/api/types/container"
... ...
@@ -54,3 +55,155 @@ func buildNetwork(t *testing.T, config map[string]any) *libnetwork.Network {
54 54
 
55 55
 	return nw
56 56
 }
57
+
58
+func TestEndpointIPAMConfigWithOutOfRangeAddrs(t *testing.T) {
59
+	tests := []struct {
60
+		name           string
61
+		ipamConfig     *networktypes.EndpointIPAMConfig
62
+		v4Subnets      []*libnetwork.IpamConf
63
+		v6Subnets      []*libnetwork.IpamConf
64
+		expectedErrors []string
65
+	}{
66
+		{
67
+			name: "valid config",
68
+			ipamConfig: &networktypes.EndpointIPAMConfig{
69
+				IPv4Address:  "192.168.100.10",
70
+				IPv6Address:  "2a01:d2:af:420b:25c1:1816:bb33:855c",
71
+				LinkLocalIPs: []string{"169.254.169.254", "fe80::42:a8ff:fe33:6230"},
72
+			},
73
+			v4Subnets: []*libnetwork.IpamConf{
74
+				{PreferredPool: "192.168.100.0/24"},
75
+			},
76
+			v6Subnets: []*libnetwork.IpamConf{
77
+				{PreferredPool: "2a01:d2:af:420b:25c1:1816:bb33::/112"},
78
+			},
79
+		},
80
+		{
81
+			name: "static addresses out of range",
82
+			ipamConfig: &networktypes.EndpointIPAMConfig{
83
+				IPv4Address: "192.168.100.10",
84
+				IPv6Address: "2a01:d2:af:420b:25c1:1816:bb33:855c",
85
+			},
86
+			v4Subnets: []*libnetwork.IpamConf{
87
+				{PreferredPool: "192.168.255.0/24"},
88
+			},
89
+			v6Subnets: []*libnetwork.IpamConf{
90
+				{PreferredPool: "2001:db8::/112"},
91
+			},
92
+			expectedErrors: []string{
93
+				"no configured subnet or ip-range contain the IP address 192.168.100.10",
94
+				"no configured subnet or ip-range contain the IP address 2a01:d2:af:420b:25c1:1816:bb33:855c",
95
+			},
96
+		},
97
+		{
98
+			name: "static addresses with dynamic network subnets",
99
+			ipamConfig: &networktypes.EndpointIPAMConfig{
100
+				IPv4Address: "192.168.100.10",
101
+				IPv6Address: "2a01:d2:af:420b:25c1:1816:bb33:855c",
102
+			},
103
+			v4Subnets: []*libnetwork.IpamConf{
104
+				{},
105
+			},
106
+			v6Subnets: []*libnetwork.IpamConf{
107
+				{},
108
+			},
109
+			expectedErrors: []string{
110
+				"user specified IP address is supported only when connecting to networks with user configured subnets",
111
+				"user specified IP address is supported only when connecting to networks with user configured subnets",
112
+			},
113
+		},
114
+	}
115
+
116
+	for _, tc := range tests {
117
+		t.Run(tc.name, func(t *testing.T) {
118
+			errs := validateIPAMConfigIsInRange(nil, tc.ipamConfig, tc.v4Subnets, tc.v6Subnets)
119
+			if tc.expectedErrors == nil {
120
+				assert.NilError(t, errors.Join(errs...))
121
+				return
122
+			}
123
+
124
+			assert.Check(t, len(errs) == len(tc.expectedErrors), "errs: %+v", errs)
125
+
126
+			err := errors.Join(errs...)
127
+			for _, expected := range tc.expectedErrors {
128
+				assert.Check(t, is.ErrorContains(err, expected))
129
+			}
130
+		})
131
+	}
132
+}
133
+
134
+func TestEndpointIPAMConfigWithInvalidConfig(t *testing.T) {
135
+	tests := []struct {
136
+		name           string
137
+		ipamConfig     *networktypes.EndpointIPAMConfig
138
+		expectedErrors []string
139
+	}{
140
+		{
141
+			name: "valid config",
142
+			ipamConfig: &networktypes.EndpointIPAMConfig{
143
+				IPv4Address:  "192.168.100.10",
144
+				IPv6Address:  "2a01:d2:af:420b:25c1:1816:bb33:855c",
145
+				LinkLocalIPs: []string{"169.254.169.254", "fe80::42:a8ff:fe33:6230"},
146
+			},
147
+		},
148
+		{
149
+			name: "invalid IP addresses",
150
+			ipamConfig: &networktypes.EndpointIPAMConfig{
151
+				IPv4Address:  "foo",
152
+				IPv6Address:  "bar",
153
+				LinkLocalIPs: []string{"baz", "foobar"},
154
+			},
155
+			expectedErrors: []string{
156
+				"invalid IPv4 address: foo",
157
+				"invalid IPv6 address: bar",
158
+				"invalid link-local IP address: baz",
159
+				"invalid link-local IP address: foobar",
160
+			},
161
+		},
162
+		{
163
+			name:       "ipv6 address with a zone",
164
+			ipamConfig: &networktypes.EndpointIPAMConfig{IPv6Address: "fe80::1cc0:3e8c:119f:c2e1%ens18"},
165
+			expectedErrors: []string{
166
+				"invalid IPv6 address: fe80::1cc0:3e8c:119f:c2e1%ens18",
167
+			},
168
+		},
169
+		{
170
+			name: "unspecified address is invalid",
171
+			ipamConfig: &networktypes.EndpointIPAMConfig{
172
+				IPv4Address:  "0.0.0.0",
173
+				IPv6Address:  "::",
174
+				LinkLocalIPs: []string{"0.0.0.0", "::"},
175
+			},
176
+			expectedErrors: []string{
177
+				"invalid IPv4 address: 0.0.0.0",
178
+				"invalid IPv6 address: ::",
179
+				"invalid link-local IP address: 0.0.0.0",
180
+				"invalid link-local IP address: ::",
181
+			},
182
+		},
183
+		{
184
+			name: "empty link-local",
185
+			ipamConfig: &networktypes.EndpointIPAMConfig{
186
+				LinkLocalIPs: []string{""},
187
+			},
188
+			expectedErrors: []string{"invalid link-local IP address:"},
189
+		},
190
+	}
191
+
192
+	for _, tc := range tests {
193
+		t.Run(tc.name, func(t *testing.T) {
194
+			errs := validateEndpointIPAMConfig(nil, tc.ipamConfig)
195
+			if tc.expectedErrors == nil {
196
+				assert.NilError(t, errors.Join(errs...))
197
+				return
198
+			}
199
+
200
+			assert.Check(t, len(errs) == len(tc.expectedErrors), "errs: %+v", errs)
201
+
202
+			err := errors.Join(errs...)
203
+			for _, expected := range tc.expectedErrors {
204
+				assert.Check(t, is.ErrorContains(err, expected))
205
+			}
206
+		})
207
+	}
208
+}
... ...
@@ -1,10 +1,7 @@
1 1
 package network
2 2
 
3 3
 import (
4
-	"errors"
5
-	"fmt"
6 4
 	"maps"
7
-	"net"
8 5
 	"slices"
9 6
 )
10 7
 
... ...
@@ -71,76 +68,3 @@ func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig {
71 71
 	cfgCopy.LinkLocalIPs = slices.Clone(cfg.LinkLocalIPs)
72 72
 	return &cfgCopy
73 73
 }
74
-
75
-// NetworkSubnet describes a user-defined subnet for a specific network. It's only used to validate if an
76
-// EndpointIPAMConfig is valid for a specific network.
77
-type NetworkSubnet interface {
78
-	// Contains checks whether the NetworkSubnet contains [addr].
79
-	Contains(addr net.IP) bool
80
-	// IsStatic checks whether the subnet was statically allocated (ie. user-defined).
81
-	IsStatic() bool
82
-}
83
-
84
-// IsInRange checks whether static IP addresses are valid in a specific network.
85
-func (cfg *EndpointIPAMConfig) IsInRange(v4Subnets []NetworkSubnet, v6Subnets []NetworkSubnet) error {
86
-	var errs []error
87
-
88
-	if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil {
89
-		errs = append(errs, err)
90
-	}
91
-	if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil {
92
-		errs = append(errs, err)
93
-	}
94
-
95
-	return errJoin(errs...)
96
-}
97
-
98
-func validateEndpointIPAddress(epAddr string, ipamSubnets []NetworkSubnet) error {
99
-	if epAddr == "" {
100
-		return nil
101
-	}
102
-
103
-	var staticSubnet bool
104
-	parsedAddr := net.ParseIP(epAddr)
105
-	for _, subnet := range ipamSubnets {
106
-		if subnet.IsStatic() {
107
-			staticSubnet = true
108
-			if subnet.Contains(parsedAddr) {
109
-				return nil
110
-			}
111
-		}
112
-	}
113
-
114
-	if staticSubnet {
115
-		return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr)
116
-	}
117
-
118
-	return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets")
119
-}
120
-
121
-// Validate checks whether cfg is valid.
122
-func (cfg *EndpointIPAMConfig) Validate() error {
123
-	if cfg == nil {
124
-		return nil
125
-	}
126
-
127
-	var errs []error
128
-
129
-	if cfg.IPv4Address != "" {
130
-		if addr := net.ParseIP(cfg.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() {
131
-			errs = append(errs, fmt.Errorf("invalid IPv4 address: %s", cfg.IPv4Address))
132
-		}
133
-	}
134
-	if cfg.IPv6Address != "" {
135
-		if addr := net.ParseIP(cfg.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() {
136
-			errs = append(errs, fmt.Errorf("invalid IPv6 address: %s", cfg.IPv6Address))
137
-		}
138
-	}
139
-	for _, addr := range cfg.LinkLocalIPs {
140
-		if parsed := net.ParseIP(addr); parsed == nil || parsed.IsUnspecified() {
141
-			errs = append(errs, fmt.Errorf("invalid link-local IP address: %s", addr))
142
-		}
143
-	}
144
-
145
-	return errJoin(errs...)
146
-}