mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
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:
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user