Allocate same port for IPv4/IPv6 for 'any interface' mappings.

The bridge driver now does its own port-mapping, rather than using the
portmapper module (which ran as two completely separate instances, for
IPv4 and IPv6).

When asked for a mapping from any host address (0.0.0.0/0) with a range
of host ports, the same port will be allocated for IPv4 and IPv6, or the
mapping will fail with an error if that's not possible.

The bridge driver now manages its own port mappings. So, remove
linux-specific PortMapper code and make what's left Windows-only.

Also, replace the portmapper.userlandProxy interface with StartProxy().

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2024-05-24 19:00:45 +01:00
parent 03577884d9
commit 4f09af6267
17 changed files with 809 additions and 642 deletions

View File

@@ -21,7 +21,6 @@ import (
"github.com/docker/docker/libnetwork/ns"
"github.com/docker/docker/libnetwork/options"
"github.com/docker/docker/libnetwork/portallocator"
"github.com/docker/docker/libnetwork/portmapper"
"github.com/docker/docker/libnetwork/scope"
"github.com/docker/docker/libnetwork/types"
"github.com/pkg/errors"
@@ -113,7 +112,7 @@ type bridgeEndpoint struct {
macAddress net.HardwareAddr
containerConfig *containerConfiguration
extConnConfig *connectivityConfiguration
portMapping []types.PortBinding // Operation port bindings
portMapping []portBinding // Operational port bindings
dbIndex uint64
dbExists bool
}
@@ -123,9 +122,7 @@ type bridgeNetwork struct {
bridge *bridgeInterface // The bridge's L3 interface
config *networkConfiguration
endpoints map[string]*bridgeEndpoint // key: endpoint id
portMapper *portmapper.PortMapper
portMapperV6 *portmapper.PortMapper
driver *driver // The network's driver
driver *driver // The network's driver
iptCleanFuncs iptablesCleanFuncs
sync.Mutex
}
@@ -355,6 +352,15 @@ func (n *bridgeNetwork) getNetworkBridgeName() string {
return config.BridgeName
}
func (n *bridgeNetwork) userlandProxyPath() string {
n.Lock()
defer n.Unlock()
if n.driver == nil {
return ""
}
return n.driver.userlandProxyPath()
}
func (n *bridgeNetwork) getEndpoint(eid string) (*bridgeEndpoint, error) {
if eid == "" {
return nil, InvalidEndpointIDError(eid)
@@ -508,6 +514,16 @@ func (d *driver) getNetwork(id string) (*bridgeNetwork, error) {
return nil, types.NotFoundErrorf("network not found: %s", id)
}
func (d *driver) userlandProxyPath() string {
d.Lock()
defer d.Unlock()
if d.config.EnableUserlandProxy {
return d.config.UserlandProxyPath
}
return ""
}
func parseNetworkGenericOptions(data interface{}) (*networkConfiguration, error) {
var (
err error
@@ -714,13 +730,11 @@ func (d *driver) createNetwork(config *networkConfiguration) (err error) {
// Create and set network handler in driver
network := &bridgeNetwork{
id: config.ID,
endpoints: make(map[string]*bridgeEndpoint),
config: config,
portMapper: portmapper.NewWithPortAllocator(d.portAllocator, d.config.UserlandProxyPath),
portMapperV6: portmapper.NewWithPortAllocator(d.portAllocator, d.config.UserlandProxyPath),
bridge: bridgeIface,
driver: d,
id: config.ID,
endpoints: make(map[string]*bridgeEndpoint),
config: config,
bridge: bridgeIface,
driver: d,
}
d.Lock()
@@ -1221,7 +1235,7 @@ func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, erro
// Return a copy of the operational data
pmc := make([]types.PortBinding, 0, len(ep.portMapping))
for _, pm := range ep.portMapping {
pmc = append(pmc, pm.GetCopy())
pmc = append(pmc, pm.PortBinding.GetCopy())
}
m[netlabel.PortMap] = pmc
}
@@ -1321,9 +1335,16 @@ func (d *driver) ProgramExternalConnectivity(nid, eid string, options map[string
}
// Program any required port mapping and store them in the endpoint
endpoint.portMapping, err = network.allocatePorts(endpoint, network.config.DefaultBindingIP, d.config.EnableUserlandProxy)
if err != nil {
return err
if endpoint.extConnConfig != nil && endpoint.extConnConfig.PortBindings != nil {
endpoint.portMapping, err = network.addPortMappings(
endpoint.addr,
endpoint.addrv6,
endpoint.extConnConfig.PortBindings,
network.config.DefaultBindingIP,
)
if err != nil {
return err
}
}
defer func() {

View File

@@ -59,22 +59,26 @@ func TestEndpointMarshalling(t *testing.T) {
},
},
},
portMapping: []types.PortBinding{
portMapping: []portBinding{
{
Proto: 17,
IP: net.ParseIP("172.33.9.56"),
Port: uint16(99),
HostIP: net.ParseIP("10.10.100.2"),
HostPort: uint16(9900),
HostPortEnd: uint16(10000),
PortBinding: types.PortBinding{
Proto: 17,
IP: net.ParseIP("172.33.9.56"),
Port: uint16(99),
HostIP: net.ParseIP("10.10.100.2"),
HostPort: uint16(9900),
HostPortEnd: uint16(10000),
},
},
{
Proto: 6,
IP: net.ParseIP("171.33.9.56"),
Port: uint16(55),
HostIP: net.ParseIP("10.11.100.2"),
HostPort: uint16(5500),
HostPortEnd: uint16(55000),
PortBinding: types.PortBinding{
Proto: 6,
IP: net.ParseIP("171.33.9.56"),
Port: uint16(55),
HostIP: net.ParseIP("10.11.100.2"),
HostPort: uint16(5500),
HostPortEnd: uint16(55000),
},
},
},
}
@@ -101,9 +105,9 @@ func TestEndpointMarshalling(t *testing.T) {
// a different port cannot be selected on live-restore if the original is
// already in-use). So, fix up portMapping in the original before running
// the comparison.
epms := make([]types.PortBinding, len(e.portMapping))
for i, pb := range e.portMapping {
epms[i] = pb
epms := make([]portBinding, len(e.portMapping))
for i, p := range e.portMapping {
epms[i] = p
epms[i].HostPortEnd = epms[i].HostPort
}
if !compareBindings(epms, ee.portMapping) {
@@ -197,12 +201,12 @@ func comparePortBinding(p *types.PortBinding, o *types.PortBinding) bool {
return true
}
func compareBindings(a, b []types.PortBinding) bool {
func compareBindings(a, b []portBinding) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if !comparePortBinding(&a[i], &b[i]) {
if !comparePortBinding(&a[i].PortBinding, &b[i].PortBinding) {
return false
}
}
@@ -732,7 +736,7 @@ func testQueryEndpointInfo(t *testing.T, ulPxyEnabled bool) {
t.Fatal("Incomplete data for port mapping in endpoint operational data")
}
for i, pb := range ep.portMapping {
if !comparePortBinding(&pb, &pm[i]) {
if !comparePortBinding(&pb.PortBinding, &pm[i]) {
t.Fatal("Unexpected data for port mapping in endpoint operational data")
}
}

View File

@@ -374,17 +374,30 @@ func (ep *bridgeEndpoint) CopyTo(o datastore.KVObject) error {
return nil
}
// restorePortAllocations is used during live-restore. It re-creates iptables
// forwarding/NAT rules, and restarts docker-proxy, as needed.
//
// TODO(robmry) - if any previously-mapped host ports are no longer available, all
// iptables forwarding/NAT rules get removed and there will be no docker-proxy
// processes. So, the container will be left running, but inaccessible.
func (n *bridgeNetwork) restorePortAllocations(ep *bridgeEndpoint) {
if ep.extConnConfig == nil ||
ep.extConnConfig.ExposedPorts == nil ||
ep.extConnConfig.PortBindings == nil {
return
}
tmp := ep.extConnConfig.PortBindings
ep.extConnConfig.PortBindings = ep.portMapping
_, err := n.allocatePorts(ep, n.config.DefaultBindingIP, n.driver.config.EnableUserlandProxy)
// ep.portMapping has HostPort=HostPortEnd, the host port allocated last
// time around ... use that in place of ep.extConnConfig.PortBindings, which
// may specify host port ranges.
cfg := make([]types.PortBinding, len(ep.portMapping))
for i, b := range ep.portMapping {
cfg[i] = b.PortBinding
}
var err error
ep.portMapping, err = n.addPortMappings(ep.addr, ep.addrv6, cfg, n.config.DefaultBindingIP)
if err != nil {
log.G(context.TODO()).Warnf("Failed to reserve existing port mapping for endpoint %.7s:%v", ep.id, err)
}
ep.extConnConfig.PortBindings = tmp
}

View File

@@ -1,221 +1,335 @@
package bridge
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"github.com/containerd/log"
"github.com/docker/docker/libnetwork/iptables"
"github.com/docker/docker/libnetwork/netutils"
"github.com/docker/docker/libnetwork/portallocator"
"github.com/docker/docker/libnetwork/portmapper"
"github.com/docker/docker/libnetwork/types"
"github.com/ishidawataru/sctp"
)
func (n *bridgeNetwork) allocatePorts(ep *bridgeEndpoint, reqDefBindIP net.IP, ulPxyEnabled bool) ([]types.PortBinding, error) {
if ep.extConnConfig == nil || ep.extConnConfig.PortBindings == nil {
return nil, nil
}
defHostIP := net.IPv4zero // 0.0.0.0
if reqDefBindIP != nil {
defHostIP = reqDefBindIP
}
var containerIPv6 net.IP
if ep.addrv6 != nil {
containerIPv6 = ep.addrv6.IP
}
pb, err := n.allocatePortsInternal(ep.extConnConfig.PortBindings, ep.addr.IP, containerIPv6, defHostIP, ulPxyEnabled)
if err != nil {
return nil, err
}
return pb, nil
type portBinding struct {
types.PortBinding
stopProxy func() error
}
func (n *bridgeNetwork) allocatePortsInternal(bindings []types.PortBinding, containerIPv4, containerIPv6, defHostIP net.IP, ulPxyEnabled bool) ([]types.PortBinding, error) {
bs := make([]types.PortBinding, 0, len(bindings))
for _, c := range bindings {
bIPv4 := c.GetCopy()
bIPv6 := c.GetCopy()
// Allocate IPv4 Port mappings
if ok := n.validatePortBindingIPv4(&bIPv4, containerIPv4, defHostIP); ok {
if err := n.allocatePort(&bIPv4, ulPxyEnabled); err != nil {
// On allocation failure, release previously allocated ports. On cleanup error, just log a warning message
if cuErr := n.releasePortsInternal(bs); cuErr != nil {
log.G(context.TODO()).Warnf("allocation failure for %v, failed to clear previously allocated ipv4 port bindings: %v", bIPv4, cuErr)
}
return nil, err
// addPortMappings takes cfg, the configuration for port mappings, selects host
// ports when ranges are given, starts docker-proxy or its dummy to reserve
// host ports, and sets up iptables NAT/forwarding rules as necessary. If
// anything goes wrong, it will undo any work it's done and return an error.
// Otherwise, the returned slice of portBinding has an entry per address
// family (if cfg describes a mapping for 'any' host address, it's expanded
// into mappings for IPv4 and IPv6, because that's how the mapping is presented
// in 'inspect'). HostPort and HostPortEnd in each returned portBinding are set
// to the selected and reserved port.
func (n *bridgeNetwork) addPortMappings(
epAddrV4, epAddrV6 *net.IPNet,
cfg []types.PortBinding,
defHostIP net.IP,
) (_ []portBinding, retErr error) {
if len(defHostIP) == 0 {
defHostIP = net.IPv4zero
} else if addr4 := defHostIP.To4(); addr4 != nil {
// Unmap the address if it's IPv4-mapped IPv6.
defHostIP = addr4
}
var containerIPv4, containerIPv6 net.IP
if epAddrV4 != nil {
containerIPv4 = epAddrV4.IP
}
if epAddrV6 != nil {
containerIPv6 = epAddrV6.IP
}
bindings := make([]portBinding, 0, len(cfg)*2)
defer func() {
if retErr != nil {
if err := n.releasePortBindings(bindings); err != nil {
log.G(context.TODO()).Warnf("Release port bindings: %s", err.Error())
}
bs = append(bs, bIPv4)
}
}()
proxyPath := n.userlandProxyPath()
for _, c := range cfg {
toBind := make([]types.PortBinding, 0, 2)
if bindingIPv4, ok := configurePortBindingIPv4(c, containerIPv4, defHostIP); ok {
toBind = append(toBind, bindingIPv4)
}
// skip adding implicit v6 addr, when the kernel was booted with `ipv6.disable=1`
// https://github.com/moby/moby/issues/42288
isV6Binding := c.HostIP != nil && c.HostIP.To4() == nil
if !isV6Binding && !netutils.IsV6Listenable() {
continue
}
// Allocate IPv6 Port mappings
// If the container has no IPv6 address, allow proxying host IPv6 traffic to it
// by setting up the binding with the IPv4 interface if the userland proxy is enabled
// This change was added to keep backward compatibility
// TODO(robmry) - this will silently ignore port bindings with an explicit IPv6
// host address, when docker-proxy is disabled, and the container is IPv4-only.
// If there's no proxying and the container has no IPv6, should probably error if ...
// - the mapping's host address is IPv6, or
// - the mapping has no host address, but the default address is IPv6.
containerIP := containerIPv6
if ulPxyEnabled && (containerIPv6 == nil) {
if proxyPath != "" && (containerIPv6 == nil) {
containerIP = containerIPv4
}
if ok := n.validatePortBindingIPv6(&bIPv6, containerIP, defHostIP); ok {
if err := n.allocatePort(&bIPv6, ulPxyEnabled); err != nil {
// On allocation failure, release previously allocated ports. On cleanup error, just log a warning message
if cuErr := n.releasePortsInternal(bs); cuErr != nil {
log.G(context.TODO()).Warnf("allocation failure for %v, failed to clear previously allocated ipv6 port bindings: %v", bIPv6, cuErr)
}
return nil, err
}
bs = append(bs, bIPv6)
if bindingIPv6, ok := configurePortBindingIPv6(c, containerIP, defHostIP); ok {
toBind = append(toBind, bindingIPv6)
}
newB, err := bindHostPorts(toBind, proxyPath)
if err != nil {
return nil, err
}
bindings = append(bindings, newB...)
}
for _, b := range bindings {
if err := n.setPerPortIptables(b, true); err != nil {
return nil, err
}
}
return bs, nil
return bindings, nil
}
// validatePortBindingIPv4 validates the port binding, populates the missing Host IP field and returns true
// if this is a valid IPv4 binding, else returns false
func (n *bridgeNetwork) validatePortBindingIPv4(bnd *types.PortBinding, containerIPv4, defHostIP net.IP) bool {
// Return early if there is a valid Host IP, but its not a IPv4 address
if len(bnd.HostIP) > 0 && bnd.HostIP.To4() == nil {
return false
// configurePortBindingIPv4 returns a new port binding with the HostIP field populated
// if a binding is required, else nil.
func configurePortBindingIPv4(bnd types.PortBinding, containerIPv4, defHostIP net.IP) (types.PortBinding, bool) {
if len(containerIPv4) == 0 {
return types.PortBinding{}, false
}
// Adjust the host address in the operational binding
if len(bnd.HostIP) > 0 && bnd.HostIP.To4() == nil {
// The mapping is explicitly IPv6.
return types.PortBinding{}, false
}
// If there's no host address, use the default.
if len(bnd.HostIP) == 0 {
// Return early if the default binding address is an IPv6 address
if defHostIP.To4() == nil {
return false
// The default binding address is IPv6.
return types.PortBinding{}, false
}
bnd.HostIP = defHostIP
}
bnd.IP = containerIPv4
return true
}
// validatePortBindingIPv6 validates the port binding, populates the missing Host IP field and returns true
// if this is a valid IPv6 binding, else returns false
func (n *bridgeNetwork) validatePortBindingIPv6(bnd *types.PortBinding, containerIP, defHostIP net.IP) bool {
// Return early if there is no container endpoint
if containerIP == nil {
return false
}
// Return early if there is a valid Host IP, which is a IPv4 address
if len(bnd.HostIP) > 0 && bnd.HostIP.To4() != nil {
return false
}
// Setup a binding to "::" if Host IP is empty and the default binding IP is 0.0.0.0
if len(bnd.HostIP) == 0 {
if defHostIP.Equal(net.IPv4zero) {
bnd.HostIP = net.IPv6zero
// If the default binding IP is an IPv6 address, use it
} else if defHostIP.To4() == nil {
bnd.HostIP = defHostIP
// Return false if default binding ip is an IPv4 address
} else {
return false
}
}
bnd.IP = containerIP
return true
}
func (n *bridgeNetwork) allocatePort(bnd *types.PortBinding, ulPxyEnabled bool) error {
var (
host net.Addr
err error
)
// Unmap the addresses if they're IPv4-mapped IPv6.
bnd.HostIP = bnd.HostIP.To4()
bnd.IP = containerIPv4.To4()
// Adjust HostPortEnd if this is not a range.
if bnd.HostPortEnd == 0 {
bnd.HostPortEnd = bnd.HostPort
}
return bnd, true
}
// Construct the container side transport address
container, err := bnd.ContainerAddr()
if err != nil {
return err
// configurePortBindingIPv6 returns a new port binding with the HostIP field populated
// if a binding is required, else nil.
func configurePortBindingIPv6(bnd types.PortBinding, containerIP, defHostIP net.IP) (types.PortBinding, bool) {
if containerIP == nil {
return types.PortBinding{}, false
}
if len(bnd.HostIP) > 0 && bnd.HostIP.To4() != nil {
// The mapping is explicitly IPv4.
return types.PortBinding{}, false
}
portmapper := n.portMapper
// If there's no host address, use the default.
if len(bnd.HostIP) == 0 {
if defHostIP.Equal(net.IPv4zero) {
if !netutils.IsV6Listenable() {
// No implicit binding if the host has no IPv6 support.
return types.PortBinding{}, false
}
// Implicit binding to "::", no explicit HostIP and the default is 0.0.0.0
bnd.HostIP = net.IPv6zero
} else if defHostIP.To4() == nil {
// The default binding IP is an IPv6 address, use it.
bnd.HostIP = defHostIP
} else {
// The default binding IP is an IPv4 address, nothing to do here.
return types.PortBinding{}, false
}
}
bnd.IP = containerIP
// Adjust HostPortEnd if this is not a range.
if bnd.HostPortEnd == 0 {
bnd.HostPortEnd = bnd.HostPort
}
return bnd, true
}
if bnd.HostIP.To4() == nil {
portmapper = n.portMapperV6
// bindHostPorts allocates ports and starts docker-proxy for the given cfg. The
// caller is responsible for ensuring that all entries in cfg map the same proto,
// container port, and host port range (their host addresses must differ).
func bindHostPorts(cfg []types.PortBinding, proxyPath string) ([]portBinding, error) {
if len(cfg) == 0 {
return nil, nil
}
// Ensure that all of cfg's entries have the same proto and ports.
proto, port, hostPort, hostPortEnd := cfg[0].Proto, cfg[0].Port, cfg[0].HostPort, cfg[0].HostPortEnd
for _, c := range cfg[1:] {
if c.Proto != proto || c.Port != port || c.HostPort != hostPort || c.HostPortEnd != hostPortEnd {
return nil, types.InternalErrorf("port binding mismatch %d/%s:%d-%d, %d/%s:%d-%d",
port, proto, hostPort, hostPortEnd,
port, c.Proto, c.HostPort, c.HostPortEnd)
}
}
// Try up to maxAllocatePortAttempts times to get a port that's not already allocated.
var err error
for i := 0; i < maxAllocatePortAttempts; i++ {
if host, err = portmapper.MapRange(container, bnd.HostIP, int(bnd.HostPort), int(bnd.HostPortEnd), ulPxyEnabled); err == nil {
break
var b []portBinding
b, err = attemptBindHostPorts(cfg, proto.String(), hostPort, hostPortEnd, proxyPath)
if err == nil {
return b, nil
}
// There is no point in immediately retrying to map an explicitly chosen port.
if bnd.HostPort != 0 && bnd.HostPort == bnd.HostPortEnd {
log.G(context.TODO()).Warnf("Failed to allocate and map port %d-%d: %s", bnd.HostPort, bnd.HostPortEnd, err)
if hostPort != 0 && hostPort == hostPortEnd {
log.G(context.TODO()).Warnf("Failed to allocate and map port: %s", err)
break
}
log.G(context.TODO()).Warnf("Failed to allocate and map port: %s, retry: %d", err, i+1)
}
return nil, err
}
// Allow unit tests to supply a dummy StartProxy.
var startProxy = portmapper.StartProxy
// attemptBindHostPorts allocates host ports for each port mapping that requires
// one, and reserves those ports by starting docker-proxy.
//
// If the allocator doesn't have an available port in the required range, or the
// docker-proxy process doesn't start (perhaps because another process has
// already bound the port), all resources are released and an error is returned.
// When ports are successfully reserved, a portBinding is returned for each
// mapping.
func attemptBindHostPorts(
cfg []types.PortBinding,
proto string,
hostPortStart, hostPortEnd uint16,
proxyPath string,
) (_ []portBinding, retErr error) {
addrs := make([]net.IP, 0, len(cfg))
for _, c := range cfg {
addrs = append(addrs, c.HostIP)
}
pa := portallocator.Get()
port, err := pa.RequestPortsInRange(addrs, proto, int(hostPortStart), int(hostPortEnd))
if err != nil {
return err
return nil, err
}
defer func() {
if retErr != nil {
for _, a := range addrs {
pa.ReleasePort(a, proto, port)
}
}
}()
// Save the host port (regardless it was or not specified in the binding)
switch netAddr := host.(type) {
case *net.TCPAddr:
bnd.HostPort = uint16(host.(*net.TCPAddr).Port)
return nil
case *net.UDPAddr:
bnd.HostPort = uint16(host.(*net.UDPAddr).Port)
return nil
case *sctp.SCTPAddr:
bnd.HostPort = uint16(host.(*sctp.SCTPAddr).Port)
return nil
default:
// For completeness
return ErrUnsupportedAddressType(fmt.Sprintf("%T", netAddr))
res := make([]portBinding, 0, len(cfg))
for _, c := range cfg {
pb := portBinding{PortBinding: c.GetCopy()}
pb.stopProxy, err = startProxy(c.Proto.String(), c.HostIP, port, c.IP, int(c.Port), proxyPath)
if err != nil {
return nil, fmt.Errorf("failed to bind port %s:%d/%s: %w", c.HostIP, port, c.Proto, err)
}
defer func() {
if retErr != nil {
if err := pb.stopProxy(); err != nil {
log.G(context.TODO()).Warnf("Failed to stop userland proxy for port mapping %s: %s", pb, err)
}
}
}()
pb.HostPort = uint16(port)
pb.HostPortEnd = pb.HostPort
res = append(res, pb)
}
return res, nil
}
// releasePorts attempts to release all port bindings, does not stop on failure
func (n *bridgeNetwork) releasePorts(ep *bridgeEndpoint) error {
return n.releasePortsInternal(ep.portMapping)
n.Lock()
pbs := ep.portMapping
ep.portMapping = nil
n.Unlock()
return n.releasePortBindings(pbs)
}
func (n *bridgeNetwork) releasePortsInternal(bindings []types.PortBinding) error {
var errorBuf bytes.Buffer
func (n *bridgeNetwork) releasePortBindings(pbs []portBinding) error {
var errs []error
for _, pb := range pbs {
errP := pb.stopProxy()
if errP != nil {
errP = fmt.Errorf("failed to stop docker-proxy for port mapping %s: %w", pb, errP)
}
errN := n.setPerPortIptables(pb, false)
if errN != nil {
errN = fmt.Errorf("failed to remove iptables rules for port mapping %s: %w", pb, errN)
}
portallocator.Get().ReleasePort(pb.HostIP, pb.Proto.String(), int(pb.HostPort))
errs = append(errs, errP, errN)
}
return errors.Join(errs...)
}
// Attempt to release all port bindings, do not stop on failure
for _, m := range bindings {
if err := n.releasePort(m); err != nil {
errorBuf.WriteString(fmt.Sprintf("\ncould not release %v because of %v", m, err))
func (n *bridgeNetwork) setPerPortIptables(b portBinding, enable bool) error {
if (b.IP.To4() != nil) != (b.HostIP.To4() != nil) {
// The binding is between containerV4 and hostV6 (not vice-versa as that
// will have been rejected earlier). It's handled by docker-proxy, so no
// additional iptables rules are required.
return nil
}
v := iptables.IPv4
if b.IP.To4() == nil {
v = iptables.IPv6
}
natChain, _, _, _, err := n.getDriverChains(v)
if err != nil || natChain == nil {
// Nothing to do, iptables/ip6tables is not enabled.
return nil
}
action := iptables.Delete
if enable {
action = iptables.Insert
}
return natChain.Forward(
action,
b.HostIP,
int(b.HostPort),
b.Proto.String(),
b.IP.String(),
int(b.Port),
n.getNetworkBridgeName(),
)
}
func (n *bridgeNetwork) reapplyPerPortIptables4() {
n.reapplyPerPortIptables(func(b portBinding) bool { return b.IP.To4() != nil })
}
func (n *bridgeNetwork) reapplyPerPortIptables6() {
n.reapplyPerPortIptables(func(b portBinding) bool { return b.IP.To4() == nil })
}
func (n *bridgeNetwork) reapplyPerPortIptables(needsReconfig func(portBinding) bool) {
n.Lock()
var allPBs []portBinding
for _, ep := range n.endpoints {
allPBs = append(allPBs, ep.portMapping...)
}
n.Unlock()
for _, b := range allPBs {
if needsReconfig(b) {
if err := n.setPerPortIptables(b, true); err != nil {
log.G(context.TODO()).Warnf("Failed to reconfigure NAT %s: %s", b, err)
}
}
}
if errorBuf.Len() != 0 {
return errors.New(errorBuf.String())
}
return nil
}
func (n *bridgeNetwork) releasePort(bnd types.PortBinding) error {
// Construct the host side transport address
host, err := bnd.HostAddr()
if err != nil {
return err
}
portmapper := n.portMapper
if bnd.HostIP.To4() == nil {
portmapper = n.portMapperV6
}
return portmapper.Unmap(host)
}

View File

@@ -1,12 +1,20 @@
package bridge
import (
"errors"
"fmt"
"net"
"strings"
"testing"
"github.com/docker/docker/internal/testutils/netnsutils"
"github.com/docker/docker/libnetwork/iptables"
"github.com/docker/docker/libnetwork/netlabel"
"github.com/docker/docker/libnetwork/ns"
"github.com/docker/docker/libnetwork/portallocator"
"github.com/docker/docker/libnetwork/types"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestPortMappingConfig(t *testing.T) {
@@ -171,3 +179,398 @@ func loopbackUp() error {
}
return nlHandle.LinkSetUp(iface)
}
func TestBindHostPortsError(t *testing.T) {
cfg := []types.PortBinding{
{
Proto: types.TCP,
Port: 80,
HostPort: 8080,
HostPortEnd: 8080,
},
{
Proto: types.TCP,
Port: 80,
HostPort: 8080,
HostPortEnd: 8081,
},
}
pbs, err := bindHostPorts(cfg, "")
assert.Check(t, is.Error(err, "port binding mismatch 80/tcp:8080-8080, 80/tcp:8080-8081"))
assert.Check(t, pbs == nil)
}
func newIPNet(t *testing.T, cidr string) *net.IPNet {
t.Helper()
ip, ipNet, err := net.ParseCIDR(cidr)
assert.NilError(t, err)
ipNet.IP = ip
return ipNet
}
func TestAddPortMappings(t *testing.T) {
ctrIP4 := newIPNet(t, "172.19.0.2/16")
ctrIP4Mapped := newIPNet(t, "::ffff:172.19.0.2/112")
ctrIP6 := newIPNet(t, "fdf8:b88e:bb5c:3483::2/64")
firstEphemPort := uint16(portallocator.Get().Begin)
testcases := []struct {
name string
epAddrV4 *net.IPNet
epAddrV6 *net.IPNet
cfg []types.PortBinding
defHostIP net.IP
proxyPath string
busyPortIPv4 int
expErr string
expPBs []types.PortBinding
expProxyRunning bool
expReleaseErr string
expNAT4Rules []string
expFilter4Rules []string
expNAT6Rules []string
expFilter6Rules []string
}{
{
name: "defaults",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{
{Proto: types.TCP, Port: 22},
{Proto: types.TCP, Port: 80},
},
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort},
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort + 1},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort + 1},
},
},
{
name: "specific host port",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080}},
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
},
},
{
name: "specific host port in-use",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080}},
busyPortIPv4: 8080,
expErr: "failed to bind port 0.0.0.0:8080/tcp: busy port",
},
{
name: "ipv4 mapped container address with specific host port",
epAddrV4: ctrIP4Mapped,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080}},
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
},
},
{
name: "ipv4 mapped host address with specific host port",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostIP: newIPNet(t, "::ffff:127.0.0.1/128").IP, HostPort: 8080}},
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/32").IP, HostPort: 8080, HostPortEnd: 8080},
},
},
{
name: "host port range with first port in-use",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080, HostPortEnd: 8081}},
busyPortIPv4: 8080,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8081, HostPortEnd: 8081},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8081, HostPortEnd: 8081},
},
},
{
name: "multi host ips with host port range and first port in-use",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{
{Proto: types.TCP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8081},
{Proto: types.TCP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8081},
},
busyPortIPv4: 8080,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8081, HostPortEnd: 8081},
// Note that, unlike the previous test, IPv4/IPv6 get different host ports.
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
},
},
{
name: "host port range with busy port",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{
{Proto: types.TCP, Port: 80, HostPort: 8080, HostPortEnd: 8083},
{Proto: types.TCP, Port: 81, HostPort: 8080, HostPortEnd: 8083},
{Proto: types.TCP, Port: 82, HostPort: 8080, HostPortEnd: 8083},
{Proto: types.UDP, Port: 80, HostPort: 8080, HostPortEnd: 8083},
{Proto: types.UDP, Port: 81, HostPort: 8080, HostPortEnd: 8083},
{Proto: types.UDP, Port: 82, HostPort: 8080, HostPortEnd: 8083},
},
busyPortIPv4: 8082,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
{Proto: types.TCP, IP: ctrIP4.IP, Port: 81, HostIP: net.IPv4zero, HostPort: 8081, HostPortEnd: 8081},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 81, HostIP: net.IPv6zero, HostPort: 8081, HostPortEnd: 8081},
{Proto: types.TCP, IP: ctrIP4.IP, Port: 82, HostIP: net.IPv4zero, HostPort: 8083, HostPortEnd: 8083},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 82, HostIP: net.IPv6zero, HostPort: 8083, HostPortEnd: 8083},
{Proto: types.UDP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
{Proto: types.UDP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
{Proto: types.UDP, IP: ctrIP4.IP, Port: 81, HostIP: net.IPv4zero, HostPort: 8081, HostPortEnd: 8081},
{Proto: types.UDP, IP: ctrIP6.IP, Port: 81, HostIP: net.IPv6zero, HostPort: 8081, HostPortEnd: 8081},
{Proto: types.UDP, IP: ctrIP4.IP, Port: 82, HostIP: net.IPv4zero, HostPort: 8083, HostPortEnd: 8083},
{Proto: types.UDP, IP: ctrIP6.IP, Port: 82, HostIP: net.IPv6zero, HostPort: 8083, HostPortEnd: 8083},
},
},
{
name: "host port range exhausted",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{
{Proto: types.TCP, Port: 80, HostPort: 8080, HostPortEnd: 8082},
{Proto: types.TCP, Port: 81, HostPort: 8080, HostPortEnd: 8082},
{Proto: types.TCP, Port: 82, HostPort: 8080, HostPortEnd: 8082},
},
busyPortIPv4: 8081,
expErr: "failed to bind port 0.0.0.0:8081/tcp: busy port",
},
{
name: "map host ipv6 to ipv4 container with proxy",
epAddrV4: ctrIP4,
cfg: []types.PortBinding{
{Proto: types.TCP, HostIP: net.IPv4zero, Port: 80},
{Proto: types.TCP, HostIP: net.IPv6zero, Port: 80},
},
proxyPath: "/dummy/path/to/proxy",
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort},
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort},
},
},
{
name: "map host ipv6 to ipv4 container without proxy",
epAddrV4: ctrIP4,
cfg: []types.PortBinding{
{Proto: types.TCP, HostIP: net.IPv4zero, Port: 80},
{Proto: types.TCP, HostIP: net.IPv6zero, Port: 80}, // silently ignored
},
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort},
},
},
{
name: "default host ip is nonzero v4",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80}},
defHostIP: newIPNet(t, "10.11.12.13/24").IP,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "10.11.12.13/24").IP, HostPort: firstEphemPort},
},
},
{
name: "default host ip is nonzero IPv4-mapped IPv6",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80}},
defHostIP: newIPNet(t, "::ffff:10.11.12.13/120").IP,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "10.11.12.13/24").IP, HostPort: firstEphemPort},
},
},
{
name: "default host ip is v6",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80}},
defHostIP: net.IPv6zero,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort},
},
},
{
name: "default host ip is nonzero v6",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80}},
defHostIP: newIPNet(t, "::1/128").IP,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: newIPNet(t, "::1/128").IP, HostPort: firstEphemPort},
},
},
{
name: "error releasing bindings",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080}, {Proto: types.TCP, Port: 22, HostPort: 2222}},
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080},
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: 2222},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: 2222},
},
expReleaseErr: "failed to stop docker-proxy for port mapping tcp/172.19.0.2:80/0.0.0.0:8080: can't stop now\n" +
"failed to stop docker-proxy for port mapping tcp/fdf8:b88e:bb5c:3483::2:80/:::8080: can't stop now\n" +
"failed to stop docker-proxy for port mapping tcp/172.19.0.2:22/0.0.0.0:2222: can't stop now\n" +
"failed to stop docker-proxy for port mapping tcp/fdf8:b88e:bb5c:3483::2:22/:::2222: can't stop now",
},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
// Mock the startProxy function used by the code under test.
origStartProxy := startProxy
defer func() { startProxy = origStartProxy }()
proxies := map[proxyCall]bool{} // proxy -> is not stopped
startProxy = func(proto string,
hostIP net.IP, hostPort int,
containerIP net.IP, containerPort int,
proxyPath string,
) (stop func() error, retErr error) {
if tc.busyPortIPv4 > 0 && tc.busyPortIPv4 == hostPort && hostIP.To4() != nil {
return nil, errors.New("busy port")
}
c := newProxyCall(proto, hostIP, hostPort, containerIP, containerPort, proxyPath)
if _, ok := proxies[c]; ok {
return nil, fmt.Errorf("duplicate proxy: %#v", c)
}
proxies[c] = true
return func() error {
if tc.expReleaseErr != "" {
return errors.New("can't stop now")
}
if !proxies[c] {
return errors.New("already stopped")
}
proxies[c] = false
return nil
}, nil
}
n := &bridgeNetwork{
config: &networkConfiguration{
BridgeName: "dummybridge",
EnableIPv6: tc.epAddrV6 != nil,
},
driver: newDriver(),
}
genericOption := map[string]interface{}{
netlabel.GenericData: &configuration{
EnableIPTables: true,
EnableIP6Tables: true,
EnableUserlandProxy: tc.proxyPath != "",
UserlandProxyPath: tc.proxyPath,
},
}
err := n.driver.configure(genericOption)
assert.NilError(t, err)
err = portallocator.Get().ReleaseAll()
assert.NilError(t, err)
pbs, err := n.addPortMappings(tc.epAddrV4, tc.epAddrV6, tc.cfg, tc.defHostIP)
if tc.expErr != "" {
assert.ErrorContains(t, err, tc.expErr)
return
}
assert.NilError(t, err)
assert.Assert(t, is.Len(pbs, len(tc.expPBs)))
// Check the iptables rules.
for _, expPB := range tc.expPBs {
var addrM, addrD, addrH string
var ipv iptables.IPVersion
if expPB.IP.To4() == nil {
ipv = iptables.IPv6
addrM = ctrIP6.IP.String() + "/128"
addrD = "[" + ctrIP6.IP.String() + "]"
addrH = expPB.HostIP.String() + "/128"
} else {
ipv = iptables.IPv4
addrM = ctrIP4.IP.String() + "/32"
addrD = ctrIP4.IP.String()
addrH = expPB.HostIP.String() + "/32"
}
if expPB.HostIP.IsUnspecified() {
addrH = "0/0"
}
// Check the MASQUERADE rule.
masqRule := fmt.Sprintf("-s %s -d %s -p %s -m %s --dport %d -j MASQUERADE",
addrM, addrM, expPB.Proto, expPB.Proto, expPB.Port)
ir := iptRule{ipv: ipv, table: iptables.Nat, chain: "POSTROUTING", args: strings.Split(masqRule, " ")}
assert.Check(t, ir.Exists(), fmt.Sprintf("expected rule %s", ir))
// Check the DNAT rule.
dnatRule := ""
if tc.proxyPath != "" {
// No docker-proxy, so expect "hairpinMode".
dnatRule = "! -i dummybridge "
}
dnatRule += fmt.Sprintf("-d %s -p %s -m %s --dport %d -j DNAT --to-destination %s:%d",
addrH, expPB.Proto, expPB.Proto, expPB.HostPort, addrD, expPB.Port)
ir = iptRule{ipv: ipv, table: iptables.Nat, chain: "DOCKER", args: strings.Split(dnatRule, " ")}
assert.Check(t, ir.Exists(), fmt.Sprintf("expected rule %s", ir))
// Check that the container's port is open.
filterRule := fmt.Sprintf("-d %s ! -i dummybridge -o dummybridge -p %s -m %s --dport %d -j ACCEPT",
addrM, expPB.Proto, expPB.Proto, expPB.Port)
ir = iptRule{ipv: ipv, table: iptables.Filter, chain: "DOCKER", args: strings.Split(filterRule, " ")}
assert.Check(t, ir.Exists(), fmt.Sprintf("expected rule %s", ir))
}
// Release anything that was allocated.
err = n.releasePorts(&bridgeEndpoint{portMapping: pbs})
if tc.expReleaseErr == "" {
assert.Check(t, err)
} else {
assert.Check(t, is.Error(err, tc.expReleaseErr))
}
// Check a docker-proxy was started and stopped for each expected port binding.
expProxies := map[proxyCall]bool{}
for _, expPB := range tc.expPBs {
p := newProxyCall(expPB.Proto.String(),
expPB.HostIP, int(expPB.HostPort),
expPB.IP, int(expPB.Port), tc.proxyPath)
expProxies[p] = tc.expReleaseErr != ""
}
assert.Check(t, is.DeepEqual(expProxies, proxies))
})
}
}
// Type for tracking calls to StartProxy.
type proxyCall struct{ proto, host, container, proxyPath string }
func newProxyCall(proto string,
hostIP net.IP, hostPort int,
containerIP net.IP, containerPort int,
proxyPath string,
) proxyCall {
return proxyCall{
proto: proto,
host: fmt.Sprintf("%v:%v", hostIP, hostPort),
container: fmt.Sprintf("%v:%v", containerIP, containerPort),
proxyPath: proxyPath,
}
}

View File

@@ -20,7 +20,7 @@ func (n *bridgeNetwork) setupFirewalld(config *networkConfiguration, i *bridgeIn
}
iptables.OnReloaded(func() { n.setupIP4Tables(config, i) })
iptables.OnReloaded(n.portMapper.ReMapAll)
iptables.OnReloaded(n.reapplyPerPortIptables4)
return nil
}
@@ -36,6 +36,6 @@ func (n *bridgeNetwork) setupFirewalld6(config *networkConfiguration, i *bridgeI
}
iptables.OnReloaded(func() { n.setupIP6Tables(config, i) })
iptables.OnReloaded(n.portMapperV6.ReMapAll)
iptables.OnReloaded(n.reapplyPerPortIptables6)
return nil
}

View File

@@ -186,12 +186,6 @@ func (n *bridgeNetwork) setupIPTables(ipVersion iptables.IPVersion, maskedAddr *
n.registerIptCleanFunc(func() error {
return iptable.ProgramChain(filterChain, config.BridgeName, hairpinMode, false)
})
if ipVersion == iptables.IPv4 {
n.portMapper.SetIptablesChain(natChain, n.getNetworkBridgeName())
} else {
n.portMapperV6.SetIptablesChain(natChain, n.getNetworkBridgeName())
}
}
d.Lock()

View File

@@ -8,7 +8,6 @@ import (
"github.com/docker/docker/libnetwork/driverapi"
"github.com/docker/docker/libnetwork/iptables"
"github.com/docker/docker/libnetwork/netlabel"
"github.com/docker/docker/libnetwork/portmapper"
"github.com/vishvananda/netlink"
"gotest.tools/v3/assert"
)
@@ -159,9 +158,7 @@ func assertChainConfig(d *driver, t *testing.T) {
// Assert function which pushes chains based on bridge config parameters.
func assertBridgeConfig(config *networkConfiguration, br *bridgeInterface, d *driver, t *testing.T) {
nw := bridgeNetwork{
portMapper: portmapper.New(),
portMapperV6: portmapper.New(),
config: config,
config: config,
}
nw.driver = d

View File

@@ -71,7 +71,7 @@ func allocatePort(portMapper *portmapper.PortMapper, bnd *types.PortBinding, con
// Try up to maxAllocatePortAttempts times to get a port that's not already allocated.
for i := 0; i < maxAllocatePortAttempts; i++ {
if host, err = portMapper.MapRange(container, bnd.HostIP, int(bnd.HostPort), int(bnd.HostPortEnd), false); err == nil {
if host, err = portMapper.MapRange(container, bnd.HostIP, int(bnd.HostPort), int(bnd.HostPortEnd)); err == nil {
break
}
// There is no point in immediately retrying to map an explicitly chosen port.

View File

@@ -1,3 +1,5 @@
//go:build windows
package portmapper
import (
@@ -12,15 +14,12 @@ import (
)
type mapping struct {
proto string
userlandProxy userlandProxy
host net.Addr
container net.Addr
proto string
stopUserlandProxy func() error
host net.Addr
container net.Addr
}
// newProxy is used to mock out the proxy server in tests
var newProxy = newProxyCommand
var (
// ErrUnknownBackendAddressType refers to an unknown container or unsupported address type
ErrUnknownBackendAddressType = errors.New("unknown container address type not supported")
@@ -46,13 +45,8 @@ func NewWithPortAllocator(allocator *portallocator.PortAllocator, proxyPath stri
}
}
// Map maps the specified container transport address to the host's network address and transport port
func (pm *PortMapper) Map(container net.Addr, hostIP net.IP, hostPort int, useProxy bool) (host net.Addr, _ error) {
return pm.MapRange(container, hostIP, hostPort, hostPort, useProxy)
}
// MapRange maps the specified container transport address to the host's network address and transport port range
func (pm *PortMapper) MapRange(container net.Addr, hostIP net.IP, hostPortStart, hostPortEnd int, useProxy bool) (host net.Addr, retErr error) {
func (pm *PortMapper) MapRange(container net.Addr, hostIP net.IP, hostPortStart, hostPortEnd int) (host net.Addr, retErr error) {
pm.lock.Lock()
defer pm.lock.Unlock()
@@ -62,7 +56,7 @@ func (pm *PortMapper) MapRange(container net.Addr, hostIP net.IP, hostPortStart,
allocatedHostPort int
)
switch t := container.(type) {
switch container.(type) {
case *net.TCPAddr:
proto = "tcp"
@@ -82,18 +76,6 @@ func (pm *PortMapper) MapRange(container net.Addr, hostIP net.IP, hostPortStart,
host: &net.TCPAddr{IP: hostIP, Port: allocatedHostPort},
container: container,
}
if useProxy {
m.userlandProxy, err = newProxy(proto, hostIP, allocatedHostPort, t.IP, t.Port, pm.proxyPath)
if err != nil {
return nil, err
}
} else {
m.userlandProxy, err = newDummyProxy(proto, hostIP, allocatedHostPort)
if err != nil {
return nil, err
}
}
case *net.UDPAddr:
proto = "udp"
@@ -113,18 +95,6 @@ func (pm *PortMapper) MapRange(container net.Addr, hostIP net.IP, hostPortStart,
host: &net.UDPAddr{IP: hostIP, Port: allocatedHostPort},
container: container,
}
if useProxy {
m.userlandProxy, err = newProxy(proto, hostIP, allocatedHostPort, t.IP, t.Port, pm.proxyPath)
if err != nil {
return nil, err
}
} else {
m.userlandProxy, err = newDummyProxy(proto, hostIP, allocatedHostPort)
if err != nil {
return nil, err
}
}
case *sctp.SCTPAddr:
proto = "sctp"
@@ -144,22 +114,6 @@ func (pm *PortMapper) MapRange(container net.Addr, hostIP net.IP, hostPortStart,
host: &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: hostIP}}, Port: allocatedHostPort},
container: container,
}
if useProxy {
sctpAddr := container.(*sctp.SCTPAddr)
if len(sctpAddr.IPAddrs) == 0 {
return nil, ErrSCTPAddrNoIP
}
m.userlandProxy, err = newProxy(proto, hostIP, allocatedHostPort, sctpAddr.IPAddrs[0].IP, sctpAddr.Port, pm.proxyPath)
if err != nil {
return nil, err
}
} else {
m.userlandProxy, err = newDummyProxy(proto, hostIP, allocatedHostPort)
if err != nil {
return nil, err
}
}
default:
return nil, ErrUnknownBackendAddressType
}
@@ -174,9 +128,11 @@ func (pm *PortMapper) MapRange(container net.Addr, hostIP net.IP, hostPortStart,
return nil, err
}
if err := m.userlandProxy.Start(); err != nil {
var err error
m.stopUserlandProxy, err = newDummyProxy(m.proto, hostIP, allocatedHostPort)
if err != nil {
// FIXME(thaJeztah): both stopping the proxy and deleting iptables rules can produce an error, and both are not currently handled.
m.userlandProxy.Stop()
m.stopUserlandProxy()
// need to undo the iptables rules before we return
pm.DeleteForwardingTableEntry(m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort)
return nil, err
@@ -197,8 +153,8 @@ func (pm *PortMapper) Unmap(host net.Addr) error {
return ErrPortNotMapped
}
if data.userlandProxy != nil {
data.userlandProxy.Stop()
if data.stopUserlandProxy != nil {
data.stopUserlandProxy()
}
delete(pm.currentMappings, key)
@@ -226,20 +182,6 @@ func (pm *PortMapper) Unmap(host net.Addr) error {
return nil
}
// ReMapAll re-applies all port mappings
func (pm *PortMapper) ReMapAll() {
pm.lock.Lock()
defer pm.lock.Unlock()
log.G(context.TODO()).Debugln("Re-applying all port mappings.")
for _, data := range pm.currentMappings {
containerIP, containerPort := getIPAndPort(data.container)
hostIP, hostPort := getIPAndPort(data.host)
if err := pm.AppendForwardingTableEntry(data.proto, hostIP, hostPort, containerIP.String(), containerPort); err != nil {
log.G(context.TODO()).Errorf("Error on iptables add: %s", err)
}
}
}
func getKey(a net.Addr) string {
switch t := a.(type) {
case *net.TCPAddr:

View File

@@ -1,46 +0,0 @@
package portmapper
import (
"net"
"sync"
"github.com/docker/docker/libnetwork/iptables"
"github.com/docker/docker/libnetwork/portallocator"
)
// PortMapper manages the network address translation
type PortMapper struct {
bridgeName string
// udp:ip:port
currentMappings map[string]*mapping
lock sync.Mutex
proxyPath string
allocator *portallocator.PortAllocator
chain *iptables.ChainInfo
}
// SetIptablesChain sets the specified chain into portmapper
func (pm *PortMapper) SetIptablesChain(c *iptables.ChainInfo, bridgeName string) {
pm.chain = c
pm.bridgeName = bridgeName
}
// AppendForwardingTableEntry adds a port mapping to the forwarding table
func (pm *PortMapper) AppendForwardingTableEntry(proto string, sourceIP net.IP, sourcePort int, containerIP string, containerPort int) error {
return pm.forward(iptables.Append, proto, sourceIP, sourcePort, containerIP, containerPort)
}
// DeleteForwardingTableEntry removes a port mapping from the forwarding table
func (pm *PortMapper) DeleteForwardingTableEntry(proto string, sourceIP net.IP, sourcePort int, containerIP string, containerPort int) error {
return pm.forward(iptables.Delete, proto, sourceIP, sourcePort, containerIP, containerPort)
}
func (pm *PortMapper) forward(action iptables.Action, proto string, sourceIP net.IP, sourcePort int, containerIP string, containerPort int) error {
if pm.chain == nil {
return nil
}
return pm.chain.Forward(action, sourceIP, sourcePort, proto, containerIP, containerPort, pm.bridgeName)
}

View File

@@ -1,269 +0,0 @@
package portmapper
import (
"net"
"strings"
"testing"
"github.com/docker/docker/libnetwork/iptables"
)
func init() {
// override this func to mock out the proxy server
newProxy = newMockProxyCommand
}
func TestSetIptablesChain(t *testing.T) {
pm := New()
c := &iptables.ChainInfo{
Name: "TEST",
}
if pm.chain != nil {
t.Fatal("chain should be nil at init")
}
pm.SetIptablesChain(c, "lo")
if pm.chain == nil {
t.Fatal("chain should not be nil after set")
}
}
func TestMapTCPPorts(t *testing.T) {
pm := New()
dstIP1 := net.ParseIP("192.168.0.1")
dstIP2 := net.ParseIP("192.168.0.2")
dstAddr1 := &net.TCPAddr{IP: dstIP1, Port: 80}
dstAddr2 := &net.TCPAddr{IP: dstIP2, Port: 80}
srcAddr1 := &net.TCPAddr{Port: 1080, IP: net.ParseIP("172.16.0.1")}
srcAddr2 := &net.TCPAddr{Port: 1080, IP: net.ParseIP("172.16.0.2")}
addrEqual := func(addr1, addr2 net.Addr) bool {
return (addr1.Network() == addr2.Network()) && (addr1.String() == addr2.String())
}
if host, err := pm.Map(srcAddr1, dstIP1, 80, true); err != nil {
t.Fatalf("Failed to allocate port: %s", err)
} else if !addrEqual(dstAddr1, host) {
t.Fatalf("Incorrect mapping result: expected %s:%s, got %s:%s",
dstAddr1.String(), dstAddr1.Network(), host.String(), host.Network())
}
if _, err := pm.Map(srcAddr1, dstIP1, 80, true); err == nil {
t.Fatalf("Port is in use - mapping should have failed")
}
if _, err := pm.Map(srcAddr2, dstIP1, 80, true); err == nil {
t.Fatalf("Port is in use - mapping should have failed")
}
if _, err := pm.Map(srcAddr2, dstIP2, 80, true); err != nil {
t.Fatalf("Failed to allocate port: %s", err)
}
if pm.Unmap(dstAddr1) != nil {
t.Fatalf("Failed to release port")
}
if pm.Unmap(dstAddr2) != nil {
t.Fatalf("Failed to release port")
}
if pm.Unmap(dstAddr2) == nil {
t.Fatalf("Port already released, but no error reported")
}
}
func TestGetUDPKey(t *testing.T) {
addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.5"), Port: 53}
key := getKey(addr)
if expected := "192.168.1.5:53/udp"; key != expected {
t.Fatalf("expected key %s got %s", expected, key)
}
}
func TestGetTCPKey(t *testing.T) {
addr := &net.TCPAddr{IP: net.ParseIP("192.168.1.5"), Port: 80}
key := getKey(addr)
if expected := "192.168.1.5:80/tcp"; key != expected {
t.Fatalf("expected key %s got %s", expected, key)
}
}
func TestGetUDPIPAndPort(t *testing.T) {
addr := &net.UDPAddr{IP: net.ParseIP("192.168.1.5"), Port: 53}
ip, port := getIPAndPort(addr)
if expected := "192.168.1.5"; ip.String() != expected {
t.Fatalf("expected ip %s got %s", expected, ip)
}
if ep := 53; port != ep {
t.Fatalf("expected port %d got %d", ep, port)
}
}
func TestMapUDPPorts(t *testing.T) {
pm := New()
dstIP1 := net.ParseIP("192.168.0.1")
dstIP2 := net.ParseIP("192.168.0.2")
dstAddr1 := &net.UDPAddr{IP: dstIP1, Port: 80}
dstAddr2 := &net.UDPAddr{IP: dstIP2, Port: 80}
srcAddr1 := &net.UDPAddr{Port: 1080, IP: net.ParseIP("172.16.0.1")}
srcAddr2 := &net.UDPAddr{Port: 1080, IP: net.ParseIP("172.16.0.2")}
addrEqual := func(addr1, addr2 net.Addr) bool {
return (addr1.Network() == addr2.Network()) && (addr1.String() == addr2.String())
}
if host, err := pm.Map(srcAddr1, dstIP1, 80, true); err != nil {
t.Fatalf("Failed to allocate port: %s", err)
} else if !addrEqual(dstAddr1, host) {
t.Fatalf("Incorrect mapping result: expected %s:%s, got %s:%s",
dstAddr1.String(), dstAddr1.Network(), host.String(), host.Network())
}
if _, err := pm.Map(srcAddr1, dstIP1, 80, true); err == nil {
t.Fatalf("Port is in use - mapping should have failed")
}
if _, err := pm.Map(srcAddr2, dstIP1, 80, true); err == nil {
t.Fatalf("Port is in use - mapping should have failed")
}
if _, err := pm.Map(srcAddr2, dstIP2, 80, true); err != nil {
t.Fatalf("Failed to allocate port: %s", err)
}
if pm.Unmap(dstAddr1) != nil {
t.Fatalf("Failed to release port")
}
if pm.Unmap(dstAddr2) != nil {
t.Fatalf("Failed to release port")
}
if pm.Unmap(dstAddr2) == nil {
t.Fatalf("Port already released, but no error reported")
}
}
func TestMapAllPortsSingleInterface(t *testing.T) {
pm := New()
dstIP1 := net.ParseIP("0.0.0.0")
srcAddr1 := &net.TCPAddr{Port: 1080, IP: net.ParseIP("172.16.0.1")}
hosts := []net.Addr{}
var host net.Addr
var err error
defer func() {
for _, val := range hosts {
pm.Unmap(val)
}
}()
for i := 0; i < 10; i++ {
start, end := pm.allocator.Begin, pm.allocator.End
for i := start; i < end; i++ {
if host, err = pm.Map(srcAddr1, dstIP1, 0, true); err != nil {
t.Fatal(err)
}
hosts = append(hosts, host)
}
if _, err := pm.Map(srcAddr1, dstIP1, start, true); err == nil {
t.Fatalf("Port %d should be bound but is not", start)
}
for _, val := range hosts {
if err := pm.Unmap(val); err != nil {
t.Fatal(err)
}
}
hosts = []net.Addr{}
}
}
func TestMapTCPDummyListen(t *testing.T) {
pm := New()
dstIP := net.ParseIP("0.0.0.0")
dstAddr := &net.TCPAddr{IP: dstIP, Port: 80}
// no-op for dummy
srcAddr := &net.TCPAddr{Port: 1080, IP: net.ParseIP("172.16.0.1")}
addrEqual := func(addr1, addr2 net.Addr) bool {
return (addr1.Network() == addr2.Network()) && (addr1.String() == addr2.String())
}
if host, err := pm.Map(srcAddr, dstIP, 80, false); err != nil {
t.Fatalf("Failed to allocate port: %s", err)
} else if !addrEqual(dstAddr, host) {
t.Fatalf("Incorrect mapping result: expected %s:%s, got %s:%s",
dstAddr.String(), dstAddr.Network(), host.String(), host.Network())
}
if _, err := net.Listen("tcp", "0.0.0.0:80"); err == nil {
t.Fatal("Listen on mapped port without proxy should fail")
} else {
if !strings.Contains(err.Error(), "address already in use") {
t.Fatalf("Error should be about address already in use, got %v", err)
}
}
if _, err := net.Listen("tcp", "0.0.0.0:81"); err != nil {
t.Fatal(err)
}
if host, err := pm.Map(srcAddr, dstIP, 81, false); err == nil {
t.Fatalf("Bound port shouldn't be allocated, but it was on: %v", host)
} else {
if !strings.Contains(err.Error(), "address already in use") {
t.Fatalf("Error should be about address already in use, got %v", err)
}
}
}
func TestMapUDPDummyListen(t *testing.T) {
pm := New()
dstIP := net.ParseIP("0.0.0.0")
dstAddr := &net.UDPAddr{IP: dstIP, Port: 80}
// no-op for dummy
srcAddr := &net.UDPAddr{Port: 1080, IP: net.ParseIP("172.16.0.1")}
addrEqual := func(addr1, addr2 net.Addr) bool {
return (addr1.Network() == addr2.Network()) && (addr1.String() == addr2.String())
}
if host, err := pm.Map(srcAddr, dstIP, 80, false); err != nil {
t.Fatalf("Failed to allocate port: %s", err)
} else if !addrEqual(dstAddr, host) {
t.Fatalf("Incorrect mapping result: expected %s:%s, got %s:%s",
dstAddr.String(), dstAddr.Network(), host.String(), host.Network())
}
if _, err := net.ListenUDP("udp", &net.UDPAddr{IP: dstIP, Port: 80}); err == nil {
t.Fatal("Listen on mapped port without proxy should fail")
} else {
if !strings.Contains(err.Error(), "address already in use") {
t.Fatalf("Error should be about address already in use, got %v", err)
}
}
if _, err := net.ListenUDP("udp", &net.UDPAddr{IP: dstIP, Port: 81}); err != nil {
t.Fatal(err)
}
if host, err := pm.Map(srcAddr, dstIP, 81, false); err == nil {
t.Fatalf("Bound port shouldn't be allocated, but it was on: %v", host)
} else {
if !strings.Contains(err.Error(), "address already in use") {
t.Fatalf("Error should be about address already in use, got %v", err)
}
}
}

View File

@@ -1,17 +0,0 @@
package portmapper
import "net"
func newMockProxyCommand(proto string, hostIP net.IP, hostPort int, containerIP net.IP, containerPort int, userlandProxyPath string) (userlandProxy, error) {
return &mockProxyCommand{}, nil
}
type mockProxyCommand struct{}
func (p *mockProxyCommand) Start() error {
return nil
}
func (p *mockProxyCommand) Stop() error {
return nil
}

View File

@@ -8,11 +8,6 @@ import (
"github.com/ishidawataru/sctp"
)
type userlandProxy interface {
Start() error
Stop() error
}
// ipVersion refers to IP version - v4 or v6
type ipVersion string
@@ -32,28 +27,31 @@ type dummyProxy struct {
ipVersion ipVersion
}
func newDummyProxy(proto string, hostIP net.IP, hostPort int) (userlandProxy, error) {
func newDummyProxy(proto string, hostIP net.IP, hostPort int) (stop func() error, retErr error) {
// detect version of hostIP to bind only to correct version
version := ipv4
if hostIP.To4() == nil {
version = ipv6
}
var addr net.Addr
switch proto {
case "tcp":
addr := &net.TCPAddr{IP: hostIP, Port: hostPort}
return &dummyProxy{addr: addr, ipVersion: version}, nil
addr = &net.TCPAddr{IP: hostIP, Port: hostPort}
case "udp":
addr := &net.UDPAddr{IP: hostIP, Port: hostPort}
return &dummyProxy{addr: addr, ipVersion: version}, nil
addr = &net.UDPAddr{IP: hostIP, Port: hostPort}
case "sctp":
addr := &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: hostIP}}, Port: hostPort}
return &dummyProxy{addr: addr, ipVersion: version}, nil
addr = &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: hostIP}}, Port: hostPort}
default:
return nil, fmt.Errorf("Unknown addr type: %s", proto)
}
p := &dummyProxy{addr: addr, ipVersion: version}
if err := p.start(); err != nil {
return nil, err
}
return p.stop, nil
}
func (p *dummyProxy) Start() error {
func (p *dummyProxy) start() error {
switch addr := p.addr.(type) {
case *net.TCPAddr:
l, err := net.ListenTCP("tcp"+string(p.ipVersion), addr)
@@ -79,7 +77,7 @@ func (p *dummyProxy) Start() error {
return nil
}
func (p *dummyProxy) Stop() error {
func (p *dummyProxy) stop() error {
if p.listener != nil {
return p.listener.Close()
}

View File

@@ -12,12 +12,31 @@ import (
"time"
)
func newProxyCommand(proto string, hostIP net.IP, hostPort int, containerIP net.IP, containerPort int, proxyPath string) (userlandProxy, error) {
// StartProxy starts the proxy process at proxyPath, or instantiates a dummy proxy
// to bind the host port if proxyPath is the empty string.
func StartProxy(
proto string,
hostIP net.IP, hostPort int,
containerIP net.IP, containerPort int,
proxyPath string,
) (stop func() error, retErr error) {
if proxyPath == "" {
return newDummyProxy(proto, hostIP, hostPort)
}
return newProxyCommand(proto, hostIP, hostPort, containerIP, containerPort, proxyPath)
}
func newProxyCommand(
proto string,
hostIP net.IP, hostPort int,
containerIP net.IP, containerPort int,
proxyPath string,
) (stop func() error, retErr error) {
if proxyPath == "" {
return nil, fmt.Errorf("no path provided for userland-proxy binary")
}
return &proxyCommand{
p := &proxyCommand{
cmd: &exec.Cmd{
Path: proxyPath,
Args: []string{
@@ -33,7 +52,11 @@ func newProxyCommand(proto string, hostIP net.IP, hostPort int, containerIP net.
},
},
wait: make(chan error, 1),
}, nil
}
if err := p.start(); err != nil {
return nil, err
}
return p.stop, nil
}
// proxyCommand wraps an exec.Cmd to run the userland TCP and UDP
@@ -43,7 +66,7 @@ type proxyCommand struct {
wait chan error
}
func (p *proxyCommand) Start() error {
func (p *proxyCommand) start() error {
r, w, err := os.Pipe()
if err != nil {
return fmt.Errorf("proxy unable to open os.Pipe %s", err)
@@ -103,7 +126,7 @@ func (p *proxyCommand) Start() error {
}
}
func (p *proxyCommand) Stop() error {
func (p *proxyCommand) stop() error {
if p.cmd.Process != nil {
if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
return err

View File

@@ -1,10 +0,0 @@
package portmapper
import (
"errors"
"net"
)
func newProxyCommand(proto string, hostIP net.IP, hostPort int, containerIP net.IP, containerPort int, proxyPath string) (userlandProxy, error) {
return nil, errors.New("proxy is unsupported on windows")
}

View File

@@ -118,7 +118,7 @@ func (p *PortBinding) GetCopy() PortBinding {
}
// String returns the PortBinding structure in string form
func (p *PortBinding) String() string {
func (p PortBinding) String() string {
ret := fmt.Sprintf("%s/", p.Proto)
if p.IP != nil {
ret += p.IP.String()