Skip to content

Commit 01eecb6

Browse files
committed
Validate port bindings for gateway_mode=routed
When bridge driver opt com.docker.network.bridge.gatway_mode_ipv[46] is set to "routed", there is no NAT. When there's no NAT, there's no meaning to the HostPort field in a port mapping (all the port mapping does is open the container's port), and the HostIP field is only used to determine the address family. So, check port bindings, and raise errors if fields are unexpectedly set when the mapping only applies to a gateway_mode=routed network. Zero-addresses are allowed, to say the mapping/open-port should be IPv4-only or IPv6-only, and host ports are not allowed. A mapping with no host address, so it applies to IPv4 and IPv6 when the default binding is 0.0.0.0, may include a host port if either uses NAT. The port number is ignored for the directly-routed family. Signed-off-by: Rob Murray <[email protected]>
1 parent 2a291c1 commit 01eecb6

2 files changed

Lines changed: 179 additions & 1 deletion

File tree

libnetwork/drivers/bridge/port_mapping_linux.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ func (n *bridgeNetwork) addPortMappings(
5656
containerIPv6 = epAddrV6.IP
5757
}
5858

59+
disableNAT4, disableNAT6 := n.getNATDisabled()
60+
if err := validatePortBindings(cfg,
61+
!disableNAT4 && len(containerIPv4) > 0,
62+
!disableNAT6 && len(containerIPv6) > 0); err != nil {
63+
return nil, err
64+
}
65+
5966
bindings := make([]portBinding, 0, len(cfg)*2)
6067

6168
defer func() {
@@ -70,7 +77,6 @@ func (n *bridgeNetwork) addPortMappings(
7077
sortAndNormPBs(sortedCfg)
7178

7279
proxyPath := n.userlandProxyPath()
73-
disableNAT4, disableNAT6 := n.getNATDisabled()
7480

7581
// toBind accumulates port bindings that should be allocated the same host port
7682
// (if required by NAT config). If the host address is unspecified, and defHostIP
@@ -130,6 +136,60 @@ func (n *bridgeNetwork) addPortMappings(
130136
return bindings, nil
131137
}
132138

139+
// Limit the number of errors reported, because there may be a lot of port
140+
// bindings (host port ranges are expanded by the CLI).
141+
const validationErrLimit = 6
142+
143+
// validatePortBindings checks that, if NAT is disabled for all uses of a
144+
// PortBinding, no HostPort, or non-zero HostIP, is specified - because they have
145+
// no meaning. A zero HostIP is allowed, as it's used to determine the address
146+
// family.
147+
//
148+
// The default binding IP is not considered, meaning that no error is raised if
149+
// there is a default binding address that is not used but could have been.
150+
//
151+
// For example, the default is an IPv6 interface address, no HostIP is specified,
152+
// and NAT6 is disabled; the default is ignored and no error will be raised. (Note
153+
// that this example may be valid if the container has no IPv6 address, and
154+
// docker-proxy is used to forward between the default IPv6 address and the
155+
// container's IPv4. So, simply disallowing a non-zero IPv6 default when NAT6
156+
// is disabled for the network would be incorrect.)
157+
func validatePortBindings(pbs []types.PortBinding, nat4, nat6 bool) error {
158+
var errs []error
159+
for i := range pbs {
160+
pb := &pbs[i]
161+
disallowHostPort := false
162+
if !nat4 && len(pb.HostIP) > 0 && pb.HostIP.To4() != nil && !pb.HostIP.Equal(net.IPv4zero) {
163+
// There's no NAT4, so don't allow a nonzero IPv4 host address in the mapping. The port will
164+
// accessible via any host interface.
165+
errs = append(errs,
166+
fmt.Errorf("NAT is disabled, omit host address in port mapping %s, or use 0.0.0.0::%d to open port %d for IPv4-only",
167+
pb, pb.Port, pb.Port))
168+
// The mapping is IPv4-specific but there's no NAT4, so a host port would make no sense.
169+
disallowHostPort = true
170+
} else if !nat6 && len(pb.HostIP) > 0 && pb.HostIP.To4() == nil && !pb.HostIP.Equal(net.IPv6zero) {
171+
// There's no NAT6, so don't allow an IPv6 host address in the mapping. The port will
172+
// accessible via any host interface.
173+
errs = append(errs,
174+
fmt.Errorf("NAT is disabled, omit host address in port mapping %s, or use [::]::%d to open port %d for IPv6-only",
175+
pb, pb.Port, pb.Port))
176+
// The mapping is IPv6-specific but there's no NAT6, so a host port would make no sense.
177+
disallowHostPort = true
178+
} else if !nat4 && !nat6 {
179+
// There's no NAT, so it would make no sense to specify a host port.
180+
disallowHostPort = true
181+
}
182+
if disallowHostPort && pb.HostPort != 0 {
183+
errs = append(errs,
184+
fmt.Errorf("host port must not be specified in mapping %s because NAT is disabled", pb))
185+
}
186+
if len(errs) >= validationErrLimit {
187+
break
188+
}
189+
}
190+
return errors.Join(errs...)
191+
}
192+
133193
// sortAndNormPBs normalises cfg by making HostPortEnd=HostPort (rather than 0) if the
134194
// host port isn't a range - and sorts it into the ordering defined by cmpPortBinding.
135195
func sortAndNormPBs(cfg []types.PortBinding) {

libnetwork/drivers/bridge/port_mapping_linux_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,124 @@ func loopbackUp() error {
180180
return nlHandle.LinkSetUp(iface)
181181
}
182182

183+
func TestValidatePortBindings(t *testing.T) {
184+
testcases := []struct {
185+
name string
186+
nat4 bool
187+
nat6 bool
188+
pbs []types.PortBinding
189+
expErrs []string
190+
}{
191+
{
192+
name: "no nat or addrs or ports",
193+
pbs: []types.PortBinding{
194+
{Proto: types.TCP, Port: 80},
195+
},
196+
},
197+
{
198+
name: "no nat with addrs",
199+
pbs: []types.PortBinding{
200+
{Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, Port: 80},
201+
{Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, Port: 80},
202+
},
203+
expErrs: []string{
204+
"NAT is disabled, omit host address in port mapping 233.252.0.2::80/tcp, or use 0.0.0.0::80 to open port 80 for IPv4-only",
205+
"NAT is disabled, omit host address in port mapping [2001:db8::2]::80/tcp, or use [::]::80 to open port 80 for IPv6-only",
206+
},
207+
},
208+
{
209+
name: "no nat with zero addrs",
210+
pbs: []types.PortBinding{
211+
{Proto: types.TCP, HostIP: newIPNet(t, "0.0.0.0/0").IP, Port: 80},
212+
{Proto: types.TCP, HostIP: newIPNet(t, "::/0").IP, Port: 80},
213+
},
214+
},
215+
{
216+
name: "no nat with host port",
217+
pbs: []types.PortBinding{
218+
{Proto: types.TCP, HostPort: 8080, Port: 80},
219+
},
220+
expErrs: []string{
221+
"host port must not be specified in mapping 8080:80/tcp because NAT is disabled",
222+
},
223+
},
224+
{
225+
name: "nat4 any addr with host port",
226+
nat4: true,
227+
pbs: []types.PortBinding{
228+
{Proto: types.TCP, HostPort: 8080, Port: 80},
229+
},
230+
},
231+
{
232+
name: "nat6 any addr with host port",
233+
nat6: true,
234+
pbs: []types.PortBinding{
235+
{Proto: types.TCP, HostPort: 8080, Port: 80},
236+
},
237+
},
238+
{
239+
name: "nat and addrs and ports",
240+
nat4: true,
241+
nat6: true,
242+
pbs: []types.PortBinding{
243+
{Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80},
244+
{Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80},
245+
},
246+
},
247+
{
248+
name: "no nat and addrs and ports",
249+
pbs: []types.PortBinding{
250+
{Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80},
251+
{Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80},
252+
},
253+
expErrs: []string{
254+
"NAT is disabled, omit host address in port mapping 233.252.0.2:8080:80/tcp, or use 0.0.0.0::80 to open port 80 for IPv4-only",
255+
"NAT is disabled, omit host address in port mapping [2001:db8::2]:8080:80/tcp, or use [::]::80 to open port 80 for IPv6-only",
256+
"host port must not be specified in mapping 233.252.0.2:8080:80/tcp because NAT is disabled",
257+
"host port must not be specified in mapping [2001:db8::2]:8080:80/tcp because NAT is disabled",
258+
},
259+
},
260+
{
261+
name: "max errs reached",
262+
pbs: []types.PortBinding{
263+
{Proto: types.TCP, HostPort: 8080, Port: 80},
264+
{Proto: types.TCP, HostPort: 8081, Port: 80},
265+
{Proto: types.TCP, HostPort: 8082, Port: 80},
266+
{Proto: types.TCP, HostPort: 8083, Port: 80},
267+
{Proto: types.TCP, HostPort: 8084, Port: 80},
268+
{Proto: types.TCP, HostPort: 8085, Port: 80},
269+
{Proto: types.TCP, HostPort: 8086, Port: 80},
270+
},
271+
expErrs: []string{
272+
"host port must not be specified in mapping 8080:80/tcp because NAT is disabled",
273+
"host port must not be specified in mapping 8081:80/tcp because NAT is disabled",
274+
"host port must not be specified in mapping 8082:80/tcp because NAT is disabled",
275+
"host port must not be specified in mapping 8083:80/tcp because NAT is disabled",
276+
"host port must not be specified in mapping 8084:80/tcp because NAT is disabled",
277+
"host port must not be specified in mapping 8085:80/tcp because NAT is disabled",
278+
},
279+
},
280+
}
281+
282+
for _, tc := range testcases {
283+
tc := tc
284+
t.Run(tc.name, func(t *testing.T) {
285+
err := validatePortBindings(tc.pbs, tc.nat4, tc.nat6)
286+
if tc.expErrs == nil {
287+
assert.Check(t, err)
288+
} else {
289+
assert.Assert(t, err != nil)
290+
for _, e := range tc.expErrs {
291+
assert.Check(t, is.ErrorContains(err, e))
292+
}
293+
numErrs := len(err.(interface{ Unwrap() []error }).Unwrap())
294+
assert.Check(t, is.Equal(numErrs, len(tc.expErrs)),
295+
fmt.Sprintf("expected %d errors, got %d in %s", len(tc.expErrs), numErrs, err.Error()))
296+
}
297+
})
298+
}
299+
}
300+
183301
func TestCmpPortBindings(t *testing.T) {
184302
pb := types.PortBinding{
185303
Proto: types.TCP,

0 commit comments

Comments
 (0)