diff --git a/libnetwork/drivers/bridge/port_mapping_linux.go b/libnetwork/drivers/bridge/port_mapping_linux.go index 969a277caf..072f94be56 100644 --- a/libnetwork/drivers/bridge/port_mapping_linux.go +++ b/libnetwork/drivers/bridge/port_mapping_linux.go @@ -56,6 +56,13 @@ func (n *bridgeNetwork) addPortMappings( containerIPv6 = epAddrV6.IP } + disableNAT4, disableNAT6 := n.getNATDisabled() + if err := validatePortBindings(cfg, + !disableNAT4 && len(containerIPv4) > 0, + !disableNAT6 && len(containerIPv6) > 0); err != nil { + return nil, err + } + bindings := make([]portBinding, 0, len(cfg)*2) defer func() { @@ -70,7 +77,6 @@ func (n *bridgeNetwork) addPortMappings( sortAndNormPBs(sortedCfg) proxyPath := n.userlandProxyPath() - disableNAT4, disableNAT6 := n.getNATDisabled() // toBind accumulates port bindings that should be allocated the same host port // (if required by NAT config). If the host address is unspecified, and defHostIP @@ -130,6 +136,60 @@ func (n *bridgeNetwork) addPortMappings( return bindings, nil } +// Limit the number of errors reported, because there may be a lot of port +// bindings (host port ranges are expanded by the CLI). +const validationErrLimit = 6 + +// validatePortBindings checks that, if NAT is disabled for all uses of a +// PortBinding, no HostPort, or non-zero HostIP, is specified - because they have +// no meaning. A zero HostIP is allowed, as it's used to determine the address +// family. +// +// The default binding IP is not considered, meaning that no error is raised if +// there is a default binding address that is not used but could have been. +// +// For example, the default is an IPv6 interface address, no HostIP is specified, +// and NAT6 is disabled; the default is ignored and no error will be raised. (Note +// that this example may be valid if the container has no IPv6 address, and +// docker-proxy is used to forward between the default IPv6 address and the +// container's IPv4. So, simply disallowing a non-zero IPv6 default when NAT6 +// is disabled for the network would be incorrect.) +func validatePortBindings(pbs []types.PortBinding, nat4, nat6 bool) error { + var errs []error + for i := range pbs { + pb := &pbs[i] + disallowHostPort := false + if !nat4 && len(pb.HostIP) > 0 && pb.HostIP.To4() != nil && !pb.HostIP.Equal(net.IPv4zero) { + // There's no NAT4, so don't allow a nonzero IPv4 host address in the mapping. The port will + // accessible via any host interface. + errs = append(errs, + 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", + pb, pb.Port, pb.Port)) + // The mapping is IPv4-specific but there's no NAT4, so a host port would make no sense. + disallowHostPort = true + } else if !nat6 && len(pb.HostIP) > 0 && pb.HostIP.To4() == nil && !pb.HostIP.Equal(net.IPv6zero) { + // There's no NAT6, so don't allow an IPv6 host address in the mapping. The port will + // accessible via any host interface. + errs = append(errs, + fmt.Errorf("NAT is disabled, omit host address in port mapping %s, or use [::]::%d to open port %d for IPv6-only", + pb, pb.Port, pb.Port)) + // The mapping is IPv6-specific but there's no NAT6, so a host port would make no sense. + disallowHostPort = true + } else if !nat4 && !nat6 { + // There's no NAT, so it would make no sense to specify a host port. + disallowHostPort = true + } + if disallowHostPort && pb.HostPort != 0 { + errs = append(errs, + fmt.Errorf("host port must not be specified in mapping %s because NAT is disabled", pb)) + } + if len(errs) >= validationErrLimit { + break + } + } + return errors.Join(errs...) +} + // sortAndNormPBs normalises cfg by making HostPortEnd=HostPort (rather than 0) if the // host port isn't a range - and sorts it into the ordering defined by cmpPortBinding. func sortAndNormPBs(cfg []types.PortBinding) { diff --git a/libnetwork/drivers/bridge/port_mapping_linux_test.go b/libnetwork/drivers/bridge/port_mapping_linux_test.go index b93e1de10c..a9dced8f53 100644 --- a/libnetwork/drivers/bridge/port_mapping_linux_test.go +++ b/libnetwork/drivers/bridge/port_mapping_linux_test.go @@ -180,6 +180,124 @@ func loopbackUp() error { return nlHandle.LinkSetUp(iface) } +func TestValidatePortBindings(t *testing.T) { + testcases := []struct { + name string + nat4 bool + nat6 bool + pbs []types.PortBinding + expErrs []string + }{ + { + name: "no nat or addrs or ports", + pbs: []types.PortBinding{ + {Proto: types.TCP, Port: 80}, + }, + }, + { + name: "no nat with addrs", + pbs: []types.PortBinding{ + {Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, Port: 80}, + {Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, Port: 80}, + }, + expErrs: []string{ + "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", + "NAT is disabled, omit host address in port mapping [2001:db8::2]::80/tcp, or use [::]::80 to open port 80 for IPv6-only", + }, + }, + { + name: "no nat with zero addrs", + pbs: []types.PortBinding{ + {Proto: types.TCP, HostIP: newIPNet(t, "0.0.0.0/0").IP, Port: 80}, + {Proto: types.TCP, HostIP: newIPNet(t, "::/0").IP, Port: 80}, + }, + }, + { + name: "no nat with host port", + pbs: []types.PortBinding{ + {Proto: types.TCP, HostPort: 8080, Port: 80}, + }, + expErrs: []string{ + "host port must not be specified in mapping 8080:80/tcp because NAT is disabled", + }, + }, + { + name: "nat4 any addr with host port", + nat4: true, + pbs: []types.PortBinding{ + {Proto: types.TCP, HostPort: 8080, Port: 80}, + }, + }, + { + name: "nat6 any addr with host port", + nat6: true, + pbs: []types.PortBinding{ + {Proto: types.TCP, HostPort: 8080, Port: 80}, + }, + }, + { + name: "nat and addrs and ports", + nat4: true, + nat6: true, + pbs: []types.PortBinding{ + {Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80}, + {Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80}, + }, + }, + { + name: "no nat and addrs and ports", + pbs: []types.PortBinding{ + {Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80}, + {Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80}, + }, + expErrs: []string{ + "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", + "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", + "host port must not be specified in mapping 233.252.0.2:8080:80/tcp because NAT is disabled", + "host port must not be specified in mapping [2001:db8::2]:8080:80/tcp because NAT is disabled", + }, + }, + { + name: "max errs reached", + pbs: []types.PortBinding{ + {Proto: types.TCP, HostPort: 8080, Port: 80}, + {Proto: types.TCP, HostPort: 8081, Port: 80}, + {Proto: types.TCP, HostPort: 8082, Port: 80}, + {Proto: types.TCP, HostPort: 8083, Port: 80}, + {Proto: types.TCP, HostPort: 8084, Port: 80}, + {Proto: types.TCP, HostPort: 8085, Port: 80}, + {Proto: types.TCP, HostPort: 8086, Port: 80}, + }, + expErrs: []string{ + "host port must not be specified in mapping 8080:80/tcp because NAT is disabled", + "host port must not be specified in mapping 8081:80/tcp because NAT is disabled", + "host port must not be specified in mapping 8082:80/tcp because NAT is disabled", + "host port must not be specified in mapping 8083:80/tcp because NAT is disabled", + "host port must not be specified in mapping 8084:80/tcp because NAT is disabled", + "host port must not be specified in mapping 8085:80/tcp because NAT is disabled", + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + err := validatePortBindings(tc.pbs, tc.nat4, tc.nat6) + if tc.expErrs == nil { + assert.Check(t, err) + } else { + assert.Assert(t, err != nil) + for _, e := range tc.expErrs { + assert.Check(t, is.ErrorContains(err, e)) + } + numErrs := len(err.(interface{ Unwrap() []error }).Unwrap()) + assert.Check(t, is.Equal(numErrs, len(tc.expErrs)), + fmt.Sprintf("expected %d errors, got %d in %s", len(tc.expErrs), numErrs, err.Error())) + } + }) + } +} + func TestCmpPortBindings(t *testing.T) { pb := types.PortBinding{ Proto: types.TCP,