libnet/d/bridge: mv portmapper to libnet/pms/{nat,routed}

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
This commit is contained in:
Albin Kerouanton
2025-07-02 14:59:26 +02:00
parent 289ef96d8b
commit 4e246efcd1
14 changed files with 731 additions and 556 deletions

View File

@@ -1458,9 +1458,10 @@ func (daemon *Daemon) networkOptions(conf *config.Config, pg plugingetter.Plugin
nwconfig.OptionLabels(conf.Labels), nwconfig.OptionLabels(conf.Labels),
nwconfig.OptionNetworkControlPlaneMTU(conf.NetworkControlPlaneMTU), nwconfig.OptionNetworkControlPlaneMTU(conf.NetworkControlPlaneMTU),
nwconfig.OptionFirewallBackend(conf.FirewallBackend), nwconfig.OptionFirewallBackend(conf.FirewallBackend),
driverOptions(conf),
} }
options = append(options, networkPlatformOptions(conf)...)
defaultAddressPools := ipamutils.GetLocalScopeDefaultNetworks() defaultAddressPools := ipamutils.GetLocalScopeDefaultNetworks()
if len(conf.NetworkConfig.DefaultAddressPools.Value()) > 0 { if len(conf.NetworkConfig.DefaultAddressPools.Value()) > 0 {
defaultAddressPools = conf.NetworkConfig.DefaultAddressPools.Value() defaultAddressPools = conf.NetworkConfig.DefaultAddressPools.Value()

View File

@@ -924,20 +924,23 @@ func setHostGatewayIP(controller *libnetwork.Controller, config *config.Config)
} }
} }
func driverOptions(config *config.Config) nwconfig.Option { // networkPlatformOptions returns a slice of platform-specific libnetwork
return nwconfig.OptionDriverConfig("bridge", options.Generic{ // options.
netlabel.GenericData: options.Generic{ func networkPlatformOptions(conf *config.Config) []nwconfig.Option {
"EnableIPForwarding": config.BridgeConfig.EnableIPForward, return []nwconfig.Option{
"DisableFilterForwardDrop": config.BridgeConfig.DisableFilterForwardDrop, nwconfig.OptionRootless(conf.Rootless),
"EnableIPTables": config.BridgeConfig.EnableIPTables, nwconfig.OptionUserlandProxy(conf.EnableUserlandProxy, conf.UserlandProxyPath),
"EnableIP6Tables": config.BridgeConfig.EnableIP6Tables, nwconfig.OptionDriverConfig("bridge", options.Generic{
"EnableUserlandProxy": config.EnableUserlandProxy, netlabel.GenericData: options.Generic{
"UserlandProxyPath": config.UserlandProxyPath, "EnableIPForwarding": conf.BridgeConfig.EnableIPForward,
"Hairpin": !config.EnableUserlandProxy || config.UserlandProxyPath == "", "DisableFilterForwardDrop": conf.BridgeConfig.DisableFilterForwardDrop,
"AllowDirectRouting": config.BridgeConfig.AllowDirectRouting, "EnableIPTables": conf.BridgeConfig.EnableIPTables,
"Rootless": config.Rootless, "EnableIP6Tables": conf.BridgeConfig.EnableIP6Tables,
}, "Hairpin": !conf.EnableUserlandProxy || conf.UserlandProxyPath == "",
}) "AllowDirectRouting": conf.BridgeConfig.AllowDirectRouting,
},
}),
}
} }
type defBrOptsV4 struct { type defBrOptsV4 struct {

View File

@@ -523,7 +523,7 @@ func (daemon *Daemon) conditionalUnmountOnCleanup(container *container.Container
return daemon.Unmount(container) return daemon.Unmount(container)
} }
func driverOptions(_ *config.Config) nwconfig.Option { func networkPlatformOptions(_ *config.Config) []nwconfig.Option {
return nil return nil
} }

View File

@@ -43,6 +43,9 @@ type Config struct {
ActiveSandboxes map[string]any ActiveSandboxes map[string]any
PluginGetter plugingetter.PluginGetter PluginGetter plugingetter.PluginGetter
FirewallBackend string FirewallBackend string
Rootless bool
EnableUserlandProxy bool
UserlandProxyPath string
} }
// New creates a new Config and initializes it with the given Options. // New creates a new Config and initializes it with the given Options.
@@ -162,3 +165,20 @@ func OptionFirewallBackend(val string) Option {
c.FirewallBackend = val c.FirewallBackend = val
} }
} }
// OptionRootless returns an option setter that indicates whether the daemon is
// running in rootless mode.
func OptionRootless(rootless bool) Option {
return func(c *Config) {
c.Rootless = rootless
}
}
// OptionUserlandProxy returns an option setter that indicates whether the
// userland proxy is enabled, and sets the path to the proxy binary.
func OptionUserlandProxy(enabled bool, proxyPath string) Option {
return func(c *Config) {
c.EnableUserlandProxy = enabled
c.UserlandProxyPath = proxyPath
}
}

View File

@@ -24,7 +24,6 @@ import (
"github.com/docker/docker/daemon/libnetwork/drvregistry" "github.com/docker/docker/daemon/libnetwork/drvregistry"
"github.com/docker/docker/daemon/libnetwork/internal/netiputil" "github.com/docker/docker/daemon/libnetwork/internal/netiputil"
"github.com/docker/docker/daemon/libnetwork/internal/nftables" "github.com/docker/docker/daemon/libnetwork/internal/nftables"
"github.com/docker/docker/daemon/libnetwork/internal/rlkclient"
"github.com/docker/docker/daemon/libnetwork/iptables" "github.com/docker/docker/daemon/libnetwork/iptables"
"github.com/docker/docker/daemon/libnetwork/netlabel" "github.com/docker/docker/daemon/libnetwork/netlabel"
"github.com/docker/docker/daemon/libnetwork/netutils" "github.com/docker/docker/daemon/libnetwork/netutils"
@@ -51,7 +50,6 @@ const (
vethPrefix = "veth" vethPrefix = "veth"
vethLen = len(vethPrefix) + 7 vethLen = len(vethPrefix) + 7
defaultContainerVethPrefix = "eth" defaultContainerVethPrefix = "eth"
maxAllocatePortAttempts = 10
) )
const ( const (
@@ -74,13 +72,10 @@ type configuration struct {
DisableFilterForwardDrop bool DisableFilterForwardDrop bool
EnableIPTables bool EnableIPTables bool
EnableIP6Tables bool EnableIP6Tables bool
EnableUserlandProxy bool
UserlandProxyPath string
// Hairpin indicates whether packets sent from a container to a host port // Hairpin indicates whether packets sent from a container to a host port
// published by another container on the same bridge network should be // published by another container on the same bridge network should be
// hairpinned. // hairpinned.
Hairpin bool Hairpin bool
Rootless bool
AllowDirectRouting bool AllowDirectRouting bool
} }
@@ -161,25 +156,14 @@ type bridgeNetwork struct {
sync.Mutex sync.Mutex
} }
type portDriverClient interface {
ChildHostIP(hostIP netip.Addr) netip.Addr
AddPort(ctx context.Context, proto string, hostIP, childIP netip.Addr, hostPort int) (func() error, error)
}
// Allow unit tests to supply a dummy RootlessKit port driver client.
var newPortDriverClient = func(ctx context.Context) (portDriverClient, error) {
return rlkclient.NewPortDriverClient(ctx)
}
type driver struct { type driver struct {
config configuration config configuration
networks map[string]*bridgeNetwork networks map[string]*bridgeNetwork
store *datastore.Store store *datastore.Store
nlh nlwrap.Handle nlh nlwrap.Handle
portDriverClient portDriverClient configNetwork sync.Mutex
configNetwork sync.Mutex firewaller firewaller.Firewaller
firewaller firewaller.Firewaller portmappers *drvregistry.PortMappers
portmappers *drvregistry.PortMappers
sync.Mutex sync.Mutex
} }
@@ -476,15 +460,6 @@ func (n *bridgeNetwork) gwMode(v firewaller.IPVersion) gwMode {
return n.config.GwModeIPv6 return n.config.GwModeIPv6
} }
func (n *bridgeNetwork) userlandProxyPath() string {
n.Lock()
defer n.Unlock()
if n.driver == nil {
return ""
}
return n.driver.userlandProxyPath()
}
func (n *bridgeNetwork) hairpin() bool { func (n *bridgeNetwork) hairpin() bool {
n.Lock() n.Lock()
defer n.Unlock() defer n.Unlock()
@@ -494,13 +469,13 @@ func (n *bridgeNetwork) hairpin() bool {
return n.driver.config.Hairpin return n.driver.config.Hairpin
} }
func (n *bridgeNetwork) getPortDriverClient() portDriverClient { func (n *bridgeNetwork) portMappers() *drvregistry.PortMappers {
n.Lock() n.Lock()
defer n.Unlock() defer n.Unlock()
if n.driver == nil { if n.driver == nil {
return nil return nil
} }
return n.driver.getPortDriverClient() return n.driver.portmappers
} }
func (n *bridgeNetwork) getEndpoint(eid string) (*bridgeEndpoint, error) { func (n *bridgeNetwork) getEndpoint(eid string) (*bridgeEndpoint, error) {
@@ -546,17 +521,7 @@ func (d *driver) configure(option map[string]interface{}) error {
return err return err
} }
var pdc portDriverClient
if config.Rootless {
var err error
pdc, err = newPortDriverClient(context.TODO())
if err != nil {
return err
}
}
d.Lock() d.Lock()
d.portDriverClient = pdc
d.config = config d.config = config
d.Unlock() d.Unlock()
@@ -604,22 +569,6 @@ func (d *driver) getNetwork(id string) (*bridgeNetwork, error) {
return nil, types.NotFoundErrorf("network not found: %s", id) 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 (d *driver) getPortDriverClient() portDriverClient {
d.Lock()
defer d.Unlock()
return d.portDriverClient
}
func parseNetworkGenericOptions(data interface{}) (*networkConfiguration, error) { func parseNetworkGenericOptions(data interface{}) (*networkConfiguration, error) {
var ( var (
err error err error
@@ -1639,7 +1588,7 @@ func (ep *bridgeEndpoint) trimPortBindings(ctx context.Context, n *bridgeNetwork
return nil, nil return nil, nil
} }
if err := releasePortBindings(toDrop, n.firewallerNetwork); err != nil { if err := n.unmapPBs(ctx, toDrop); err != nil {
log.G(ctx).WithFields(log.Fields{ log.G(ctx).WithFields(log.Fields{
"error": err, "error": err,
"gw4": pbmReq.ipv4, "gw4": pbmReq.ipv4,

View File

@@ -8,7 +8,6 @@ import (
"maps" "maps"
"net" "net"
"net/netip" "net/netip"
"os/exec"
"slices" "slices"
"strconv" "strconv"
"testing" "testing"
@@ -796,21 +795,16 @@ func testQueryEndpointInfo(t *testing.T, ulPxyEnabled bool) {
defer netnsutils.SetupTestOSContext(t)() defer netnsutils.SetupTestOSContext(t)()
useStubFirewaller(t) useStubFirewaller(t)
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{}) pms := drvregistry.PortMappers{}
pm := &stubPortMapper{}
err := pms.Register("nat", pm)
assert.NilError(t, err)
d := newDriver(storeutils.NewTempStore(t), &pms)
portallocator.Get().ReleaseAll() portallocator.Get().ReleaseAll()
var proxyBinary string
var err error
if ulPxyEnabled {
proxyBinary, err = exec.LookPath("docker-proxy")
if err != nil {
t.Fatalf("failed to lookup userland-proxy binary: %v", err)
}
}
config := &configuration{ config := &configuration{
EnableIPTables: true, EnableIPTables: true,
EnableUserlandProxy: ulPxyEnabled,
UserlandProxyPath: proxyBinary,
} }
genericOption := make(map[string]interface{}) genericOption := make(map[string]interface{})
genericOption[netlabel.GenericData] = config genericOption[netlabel.GenericData] = config
@@ -865,15 +859,15 @@ func testQueryEndpointInfo(t *testing.T, ulPxyEnabled bool) {
if !ok { if !ok {
t.Fatal("Endpoint operational data does not contain port mapping data") t.Fatal("Endpoint operational data does not contain port mapping data")
} }
pm, ok := pmd.([]types.PortBinding) pbs, ok := pmd.([]types.PortBinding)
if !ok { if !ok {
t.Fatal("Unexpected format for port mapping in endpoint operational data") t.Fatal("Unexpected format for port mapping in endpoint operational data")
} }
if len(ep.portMapping) != len(pm) { if len(ep.portMapping) != len(pbs) {
t.Fatal("Incomplete data for port mapping in endpoint operational data") t.Fatal("Incomplete data for port mapping in endpoint operational data")
} }
for i, pb := range ep.portMapping { for i, pb := range ep.portMapping {
if !comparePortBinding(&pb.PortBinding, &pm[i]) { if !comparePortBinding(&pb.PortBinding, &pbs[i]) {
t.Fatal("Unexpected data for port mapping in endpoint operational data") t.Fatal("Unexpected data for port mapping in endpoint operational data")
} }
} }

View File

@@ -8,25 +8,15 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/netip"
"os"
"slices" "slices"
"strconv"
"syscall"
"github.com/containerd/log" "github.com/containerd/log"
"github.com/docker/docker/daemon/libnetwork/drivers/bridge/internal/firewaller"
"github.com/docker/docker/daemon/libnetwork/internal/rlkclient"
"github.com/docker/docker/daemon/libnetwork/netutils" "github.com/docker/docker/daemon/libnetwork/netutils"
"github.com/docker/docker/daemon/libnetwork/portallocator"
"github.com/docker/docker/daemon/libnetwork/portmapper"
"github.com/docker/docker/daemon/libnetwork/portmapperapi" "github.com/docker/docker/daemon/libnetwork/portmapperapi"
"github.com/docker/docker/daemon/libnetwork/types" "github.com/docker/docker/daemon/libnetwork/types"
"github.com/docker/docker/internal/sliceutil"
) )
// Allow unit tests to supply a dummy StartProxy.
var startProxy = portmapper.StartProxy
// addPortMappings takes cfg, the configuration for port mappings, selects host // addPortMappings takes cfg, the configuration for port mappings, selects host
// ports when ranges are given, binds host ports to check they're available and // ports when ranges are given, binds host ports to check they're available and
// reserve them, starts docker-proxy if required, and sets up iptables // reserve them, starts docker-proxy if required, and sets up iptables
@@ -50,20 +40,23 @@ func (n *bridgeNetwork) addPortMappings(
defHostIP = addr4 defHostIP = addr4
} }
pms := n.portMappers()
bindings := make([]portmapperapi.PortBinding, 0, len(cfg)*2) bindings := make([]portmapperapi.PortBinding, 0, len(cfg)*2)
defer func() { defer func() {
if retErr != nil { if retErr != nil {
if err := releasePortBindings(bindings, n.firewallerNetwork); err != nil { if err := n.unmapPBs(ctx, bindings); err != nil {
log.G(ctx).Warnf("Release port bindings: %s", err.Error()) log.G(ctx).WithFields(log.Fields{
"bindings": bindings,
"error": err,
"origErr": retErr,
}).Warn("Failed to unmap port bindings after error")
} }
} }
}() }()
bindingReqs := n.sortAndNormPBs(ctx, ep, cfg, defHostIP, pbmReq) bindingReqs := n.sortAndNormPBs(ctx, ep, cfg, defHostIP, pbmReq)
proxyPath := n.userlandProxyPath()
pdc := n.getPortDriverClient()
// toBind accumulates port bindings that should be allocated the same host port // toBind accumulates port bindings that should be allocated the same host port
// (if required by NAT config). If the host address is unspecified, and defHostIP // (if required by NAT config). If the host address is unspecified, and defHostIP
// is 0.0.0.0, one iteration of the loop may generate bindings for v4 and v6. If // is 0.0.0.0, one iteration of the loop may generate bindings for v4 and v6. If
@@ -76,52 +69,30 @@ func (n *bridgeNetwork) addPortMappings(
var toBind []portmapperapi.PortBindingReq var toBind []portmapperapi.PortBindingReq
for i, c := range bindingReqs { for i, c := range bindingReqs {
toBind = append(toBind, c) toBind = append(toBind, c)
if i < len(bindingReqs)-1 && c.DisableNAT == bindingReqs[i+1].DisableNAT && needSamePort(c, bindingReqs[i+1]) { if i < len(bindingReqs)-1 && c.Mapper == bindingReqs[i+1].Mapper && needSamePort(c, bindingReqs[i+1]) {
// This port binding matches the next, apart from host IP. So, continue // This port binding matches the next, apart from host IP. So, continue
// collecting bindings, then allocate the same host port for all addresses. // collecting bindings, then allocate the same host port for all addresses.
continue continue
} }
var newB []portmapperapi.PortBinding pm, err := pms.Get(c.Mapper)
var err error
if c.DisableNAT {
newB, err = setupForwardedPorts(ctx, toBind, n.firewallerNetwork)
} else {
newB, err = bindHostPorts(ctx, toBind, proxyPath, pdc, n.firewallerNetwork)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
bindings = append(bindings, newB...)
newB, err := pm.MapPorts(ctx, toBind, n.firewallerNetwork)
if err != nil {
return nil, err
}
bindings = append(bindings, sliceutil.Map(newB, func(b portmapperapi.PortBinding) portmapperapi.PortBinding {
b.Mapper = c.Mapper
return b
})...)
// Reset toBind now the ports are bound. // Reset toBind now the ports are bound.
toBind = toBind[:0] toBind = toBind[:0]
} }
// Start userland proxy processes.
if proxyPath != "" {
for i := range bindings {
if bindings[i].BoundSocket == nil || bindings[i].RootlesskitUnsupported || bindings[i].StopProxy != nil {
continue
}
var err error
bindings[i].StopProxy, err = startProxy(
bindings[i].ChildPortBinding(), proxyPath, bindings[i].BoundSocket,
)
if err != nil {
return nil, fmt.Errorf("failed to start userland proxy for port mapping %s: %w",
bindings[i].PortBinding, err)
}
if err := bindings[i].BoundSocket.Close(); err != nil {
log.G(ctx).WithFields(log.Fields{
"error": err,
"mapping": bindings[i].PortBinding,
}).Warnf("failed to close proxy socket")
}
bindings[i].BoundSocket = nil
}
}
return bindings, nil return bindings, nil
} }
@@ -283,7 +254,10 @@ func configurePortBindingIPv4(
// Unmap the addresses if they're IPv4-mapped IPv6. // Unmap the addresses if they're IPv4-mapped IPv6.
bnd.HostIP = bnd.HostIP.To4() bnd.HostIP = bnd.HostIP.To4()
bnd.IP = containerIPv4.To4() bnd.IP = containerIPv4.To4()
bnd.DisableNAT = disableNAT bnd.Mapper = "nat"
if disableNAT {
bnd.Mapper = "routed"
}
return bnd, true return bnd, true
} }
@@ -340,236 +314,13 @@ func configurePortBindingIPv6(
} }
bnd.IP = containerIP bnd.IP = containerIP
bnd.DisableNAT = disableNAT bnd.Mapper = "nat"
if disableNAT {
bnd.Mapper = "routed"
}
return bnd, true return bnd, true
} }
func setChildHostIP(pdc portDriverClient, req portmapperapi.PortBindingReq) portmapperapi.PortBindingReq {
if pdc == nil {
req.ChildHostIP = req.HostIP
return req
}
hip, _ := netip.AddrFromSlice(req.HostIP)
req.ChildHostIP = pdc.ChildHostIP(hip).AsSlice()
return req
}
// setupForwardedPorts sets up firewall rules to allow direct remote access to
// the container's ports in cfg.
func setupForwardedPorts(ctx context.Context, cfg []portmapperapi.PortBindingReq, fwn firewaller.Network) ([]portmapperapi.PortBinding, error) {
if len(cfg) == 0 {
return nil, nil
}
res := make([]portmapperapi.PortBinding, 0, len(cfg))
bindings := make([]types.PortBinding, 0, len(cfg))
for _, c := range cfg {
pb := portmapperapi.PortBinding{PortBinding: c.GetCopy()}
if pb.HostPort != 0 || pb.HostPortEnd != 0 {
log.G(ctx).WithFields(log.Fields{"mapping": pb}).Infof(
"Host port ignored, because NAT is disabled")
pb.HostPort = 0
pb.HostPortEnd = 0
}
res = append(res, pb)
bindings = append(bindings, pb.PortBinding)
}
if err := fwn.AddPorts(ctx, bindings); err != nil {
return nil, err
}
return res, nil
}
// bindHostPorts allocates and binds host ports 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(
ctx context.Context,
cfg []portmapperapi.PortBindingReq,
proxyPath string,
pdc portDriverClient,
fwn firewaller.Network,
) ([]portmapperapi.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++ {
var b []portmapperapi.PortBinding
b, err = attemptBindHostPorts(ctx, cfg, proto, hostPort, hostPortEnd, proxyPath, pdc, fwn)
if err == nil {
return b, nil
}
// There is no point in immediately retrying to map an explicitly chosen port.
if hostPort != 0 && hostPort == hostPortEnd {
log.G(ctx).WithError(err).Warnf("Failed to allocate and map port")
break
}
log.G(ctx).WithFields(log.Fields{
"error": err,
"attempt": i + 1,
}).Warn("Failed to allocate and map port")
}
return nil, err
}
// attemptBindHostPorts allocates host ports for each NAT port mapping, and
// reserves those ports by binding them.
//
// If the allocator doesn't have an available port in the required range, or the
// port can't be bound (perhaps because another process has already bound it),
// all resources are released and an error is returned. When ports are
// successfully reserved, a PortBinding is returned for each mapping.
func attemptBindHostPorts(
ctx context.Context,
cfg []portmapperapi.PortBindingReq,
proto types.Protocol,
hostPortStart, hostPortEnd uint16,
proxyPath string,
pdc portDriverClient,
fwn firewaller.Network,
) (_ []portmapperapi.PortBinding, retErr error) {
var err error
var port int
addrs := make([]net.IP, 0, len(cfg))
for i := range cfg {
cfg[i] = setChildHostIP(pdc, cfg[i])
addrs = append(addrs, cfg[i].ChildHostIP)
}
pa := portallocator.NewOSAllocator()
port, socks, err := pa.RequestPortsInRange(addrs, proto, int(hostPortStart), int(hostPortEnd))
if err != nil {
return nil, err
}
defer func() {
if retErr != nil {
pa.ReleasePorts(addrs, proto, port)
}
}()
if len(socks) != len(cfg) {
for _, sock := range socks {
if err := sock.Close(); err != nil {
log.G(ctx).WithError(err).Warn("Failed to close socket")
}
}
return nil, types.InternalErrorf("port allocator returned %d sockets for %d port bindings", len(socks), len(cfg))
}
res := make([]portmapperapi.PortBinding, 0, len(cfg))
defer func() {
if retErr != nil {
if err := releasePortBindings(res, fwn); err != nil {
log.G(ctx).WithError(err).Warn("Failed to release port bindings")
}
}
}()
for i := range cfg {
pb := portmapperapi.PortBinding{
PortBinding: cfg[i].PortBinding.GetCopy(),
BoundSocket: socks[i],
ChildHostIP: cfg[i].ChildHostIP,
}
pb.PortBinding.HostPort = uint16(port)
pb.PortBinding.HostPortEnd = pb.HostPort
res = append(res, pb)
}
if err := configPortDriver(ctx, res, pdc); err != nil {
return nil, err
}
if err := fwn.AddPorts(ctx, mergeChildHostIPs(res)); err != nil {
return nil, err
}
// Now the firewall rules are set up, it's safe to listen on the socket. (Listening
// earlier could result in dropped connections if the proxy becomes unreachable due
// to NAT rules sending packets directly to the container.)
//
// If not starting the proxy, nothing will ever accept a connection on the
// socket. Listen here anyway because SO_REUSEADDR is set, so bind() won't notice
// the problem if a port's bound to both INADDR_ANY and a specific address. (Also
// so the binding shows up in "netstat -at".)
if err := listenBoundPorts(res, proxyPath); err != nil {
return nil, err
}
return res, nil
}
// configPortDriver passes the port binding's details to rootlesskit, and updates the
// port binding with callbacks to remove the rootlesskit config (or marks the binding as
// unsupported by rootlesskit).
func configPortDriver(ctx context.Context, pbs []portmapperapi.PortBinding, pdc portDriverClient) error {
for i := range pbs {
b := pbs[i]
if pdc != nil && b.HostPort != 0 {
var err error
hip, ok := netip.AddrFromSlice(b.HostIP)
if !ok {
return fmt.Errorf("invalid host IP address in %s", b)
}
chip, ok := netip.AddrFromSlice(b.ChildHostIP)
if !ok {
return fmt.Errorf("invalid child host IP address %s in %s", b.ChildHostIP, b)
}
pbs[i].PortDriverRemove, err = pdc.AddPort(ctx, b.Proto.String(), hip, chip, int(b.HostPort))
if err != nil {
var pErr *rlkclient.ProtocolUnsupportedError
if errors.As(err, &pErr) {
log.G(ctx).WithFields(log.Fields{
"error": pErr,
}).Warnf("discarding request for %q", net.JoinHostPort(hip.String(), strconv.Itoa(int(b.HostPort))))
pbs[i].RootlesskitUnsupported = true
continue
}
return err
}
}
}
return nil
}
func listenBoundPorts(pbs []portmapperapi.PortBinding, proxyPath string) error {
for i := range pbs {
if pbs[i].BoundSocket == nil || pbs[i].RootlesskitUnsupported || pbs[i].Proto == types.UDP {
continue
}
rc, err := pbs[i].BoundSocket.SyscallConn()
if err != nil {
return fmt.Errorf("raw conn not available on %s socket: %w", pbs[i].Proto, err)
}
if errC := rc.Control(func(fd uintptr) {
somaxconn := 0
// SCTP sockets do not support somaxconn=0
if proxyPath != "" || pbs[i].Proto == types.SCTP {
somaxconn = -1 // silently capped to "/proc/sys/net/core/somaxconn"
}
err = syscall.Listen(int(fd), somaxconn)
}); errC != nil {
return fmt.Errorf("failed to Control %s socket: %w", pbs[i].Proto, err)
}
if err != nil {
return fmt.Errorf("failed to listen on %s socket: %w", pbs[i].Proto, err)
}
}
return nil
}
// releasePorts attempts to release all port bindings, does not stop on failure // releasePorts attempts to release all port bindings, does not stop on failure
func (n *bridgeNetwork) releasePorts(ep *bridgeEndpoint) error { func (n *bridgeNetwork) releasePorts(ep *bridgeEndpoint) error {
n.Lock() n.Lock()
@@ -578,36 +329,25 @@ func (n *bridgeNetwork) releasePorts(ep *bridgeEndpoint) error {
ep.portBindingState = portBindingMode{} ep.portBindingState = portBindingMode{}
n.Unlock() n.Unlock()
return releasePortBindings(pbs, n.firewallerNetwork) return n.unmapPBs(context.TODO(), pbs)
} }
func releasePortBindings(pbs []portmapperapi.PortBinding, fwn firewaller.Network) error { func (n *bridgeNetwork) unmapPBs(ctx context.Context, bindings []portmapperapi.PortBinding) error {
pms := n.portMappers()
var errs []error var errs []error
for _, pb := range pbs { for _, b := range bindings {
if pb.BoundSocket != nil { pm, err := pms.Get(b.Mapper)
if err := pb.BoundSocket.Close(); err != nil { if err != nil {
errs = append(errs, fmt.Errorf("failed to close socket for port mapping %s: %w", pb, err)) errs = append(errs, fmt.Errorf("unmapping port binding %s: %w", b.PortBinding, err))
} continue
} }
if pb.PortDriverRemove != nil {
if err := pb.PortDriverRemove(); err != nil { if err := pm.UnmapPorts(ctx, []portmapperapi.PortBinding{b}, n.firewallerNetwork); err != nil {
errs = append(errs, err) errs = append(errs, fmt.Errorf("unmapping port binding %s: %w", b.PortBinding, err))
}
}
if pb.StopProxy != nil {
if err := pb.StopProxy(); err != nil && !errors.Is(err, os.ErrProcessDone) {
errs = append(errs, fmt.Errorf("failed to stop userland proxy for port mapping %s: %w", pb, err))
}
}
}
if err := fwn.DelPorts(context.TODO(), mergeChildHostIPs(pbs)); err != nil {
errs = append(errs, err)
}
for _, pb := range pbs {
if pb.HostPort > 0 {
portallocator.Get().ReleasePort(pb.ChildHostIP, pb.Proto.String(), int(pb.HostPort))
} }
} }
return errors.Join(errs...) return errors.Join(errs...)
} }

View File

@@ -1,3 +1,6 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23
package bridge package bridge
import ( import (
@@ -7,6 +10,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"slices"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@@ -19,7 +23,10 @@ import (
"github.com/docker/docker/daemon/libnetwork/ns" "github.com/docker/docker/daemon/libnetwork/ns"
"github.com/docker/docker/daemon/libnetwork/portallocator" "github.com/docker/docker/daemon/libnetwork/portallocator"
"github.com/docker/docker/daemon/libnetwork/portmapperapi" "github.com/docker/docker/daemon/libnetwork/portmapperapi"
"github.com/docker/docker/daemon/libnetwork/portmappers/nat"
"github.com/docker/docker/daemon/libnetwork/portmappers/routed"
"github.com/docker/docker/daemon/libnetwork/types" "github.com/docker/docker/daemon/libnetwork/types"
"github.com/docker/docker/internal/sliceutil"
"github.com/docker/docker/internal/testutils/netnsutils" "github.com/docker/docker/internal/testutils/netnsutils"
"github.com/docker/docker/internal/testutils/storeutils" "github.com/docker/docker/internal/testutils/storeutils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@@ -32,7 +39,12 @@ func TestPortMappingConfig(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)() defer netnsutils.SetupTestOSContext(t)()
useStubFirewaller(t) useStubFirewaller(t)
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{}) pms := drvregistry.PortMappers{}
pm := &stubPortMapper{}
err := pms.Register("nat", pm)
assert.NilError(t, err)
d := newDriver(storeutils.NewTempStore(t), &pms)
config := &configuration{ config := &configuration{
EnableIPTables: true, EnableIPTables: true,
@@ -61,7 +73,7 @@ func TestPortMappingConfig(t *testing.T) {
} }
ipdList4 := getIPv4Data(t) ipdList4 := getIPv4Data(t)
err := d.CreateNetwork(context.Background(), "dummy", netOptions, nil, ipdList4, getIPv6Data(t)) err = d.CreateNetwork(context.Background(), "dummy", netOptions, nil, ipdList4, getIPv6Data(t))
if err != nil { if err != nil {
t.Fatalf("Failed to create bridge: %v", err) t.Fatalf("Failed to create bridge: %v", err)
} }
@@ -117,7 +129,12 @@ func TestPortMappingV6Config(t *testing.T) {
t.Fatalf("Could not bring loopback iface up: %v", err) t.Fatalf("Could not bring loopback iface up: %v", err)
} }
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{}) pms := drvregistry.PortMappers{}
pm := &stubPortMapper{}
err := pms.Register("nat", pm)
assert.NilError(t, err)
d := newDriver(storeutils.NewTempStore(t), &pms)
config := &configuration{ config := &configuration{
EnableIPTables: true, EnableIPTables: true,
@@ -147,7 +164,7 @@ func TestPortMappingV6Config(t *testing.T) {
ipdList4 := getIPv4Data(t) ipdList4 := getIPv4Data(t)
ipdList6 := getIPv6Data(t) ipdList6 := getIPv6Data(t)
err := d.CreateNetwork(context.Background(), "dummy", netOptions, nil, ipdList4, ipdList6) err = d.CreateNetwork(context.Background(), "dummy", netOptions, nil, ipdList4, ipdList6)
if err != nil { if err != nil {
t.Fatalf("Failed to create bridge: %v", err) t.Fatalf("Failed to create bridge: %v", err)
} }
@@ -196,30 +213,6 @@ func loopbackUp() error {
return nlHandle.LinkSetUp(iface) return nlHandle.LinkSetUp(iface)
} }
func TestBindHostPortsError(t *testing.T) {
cfg := []portmapperapi.PortBindingReq{
{
PortBinding: types.PortBinding{
Proto: types.TCP,
Port: 80,
HostPort: 8080,
HostPortEnd: 8080,
},
},
{
PortBinding: types.PortBinding{
Proto: types.TCP,
Port: 80,
HostPort: 8080,
HostPortEnd: 8081,
},
},
}
pbs, err := bindHostPorts(context.Background(), cfg, "", nil, nil)
assert.Check(t, is.Error(err, "port binding mismatch 80/tcp:8080-8080, 80/tcp:8080-8081"))
assert.Check(t, is.Nil(pbs))
}
func newIPNet(t *testing.T, cidr string) *net.IPNet { func newIPNet(t *testing.T, cidr string) *net.IPNet {
t.Helper() t.Helper()
ip, ipNet, err := net.ParseCIDR(cidr) ip, ipNet, err := net.ParseCIDR(cidr)
@@ -242,7 +235,7 @@ func TestAddPortMappings(t *testing.T) {
gwMode6 gwMode gwMode6 gwMode
cfg []portmapperapi.PortBindingReq cfg []portmapperapi.PortBindingReq
defHostIP net.IP defHostIP net.IP
proxyPath string enableProxy bool
hairpin bool hairpin bool
busyPortIPv4 int busyPortIPv4 int
rootless bool rootless bool
@@ -267,7 +260,7 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort}, {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: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort},
@@ -276,24 +269,24 @@ func TestAddPortMappings(t *testing.T) {
}, },
}, },
{ {
name: "specific host port", name: "specific host port",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080}, {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: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
}, },
}, },
{ {
name: "nat explicitly enabled", name: "nat explicitly enabled",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
gwMode4: gwModeNAT, gwMode4: gwModeNAT,
gwMode6: gwModeNAT, gwMode6: gwModeNAT,
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080}, {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: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
@@ -304,27 +297,27 @@ func TestAddPortMappings(t *testing.T) {
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
busyPortIPv4: 8080, busyPortIPv4: 8080,
expErr: "failed to bind host port 0.0.0.0:8080/tcp: address already in use", expErr: "failed to bind host port 0.0.0.0:8080/tcp: address already in use",
}, },
{ {
name: "ipv4 mapped container address with specific host port", name: "ipv4 mapped container address with specific host port",
epAddrV4: ctrIP4Mapped, epAddrV4: ctrIP4Mapped,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080}, {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: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
}, },
}, },
{ {
name: "ipv4 mapped host address with specific host port", name: "ipv4 mapped host address with specific host port",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostIP: newIPNet(t, "::ffff:127.0.0.1/128").IP, HostPort: 8080}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostIP: newIPNet(t, "::ffff:127.0.0.1/128").IP, HostPort: 8080}}},
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/32").IP, HostPort: 8080, HostPortEnd: 8080}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/32").IP, HostPort: 8080, HostPortEnd: 8080},
}, },
@@ -334,7 +327,7 @@ func TestAddPortMappings(t *testing.T) {
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080, HostPortEnd: 8081}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080, HostPortEnd: 8081}}},
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
busyPortIPv4: 8080, busyPortIPv4: 8080,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8081, HostPortEnd: 8081}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8081, HostPortEnd: 8081},
@@ -349,7 +342,7 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8081}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8081}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8081}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8081}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
busyPortIPv4: 8080, busyPortIPv4: 8080,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8081}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8081},
@@ -368,7 +361,7 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.UDP, Port: 81, HostPort: 8080, HostPortEnd: 8083}}, {PortBinding: types.PortBinding{Proto: types.UDP, Port: 81, HostPort: 8080, HostPortEnd: 8083}},
{PortBinding: types.PortBinding{Proto: types.UDP, Port: 82, HostPort: 8080, HostPortEnd: 8083}}, {PortBinding: types.PortBinding{Proto: types.UDP, Port: 82, HostPort: 8080, HostPortEnd: 8083}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
busyPortIPv4: 8082, busyPortIPv4: 8082,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
@@ -394,7 +387,7 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 81, HostPort: 8080, HostPortEnd: 8082}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 81, HostPort: 8080, HostPortEnd: 8082}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 82, HostPort: 8080, HostPortEnd: 8082}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 82, HostPort: 8080, HostPortEnd: 8082}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
busyPortIPv4: 8081, busyPortIPv4: 8081,
expErr: "failed to bind host port 0.0.0.0:8081", expErr: "failed to bind host port 0.0.0.0:8081",
}, },
@@ -405,7 +398,7 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, HostIP: net.IPv4zero, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, HostIP: net.IPv4zero, Port: 80}},
{PortBinding: types.PortBinding{Proto: types.TCP, HostIP: net.IPv6zero, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, HostIP: net.IPv6zero, Port: 80}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
expPBs: []types.PortBinding{ 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.IPv4zero, HostPort: firstEphemPort},
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort},
@@ -417,7 +410,7 @@ func TestAddPortMappings(t *testing.T) {
cfg: []portmapperapi.PortBindingReq{ cfg: []portmapperapi.PortBindingReq{
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
noProxy6To4: true, noProxy6To4: true,
expPBs: []types.PortBinding{ 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.IPv4zero, HostPort: firstEphemPort},
@@ -436,45 +429,45 @@ func TestAddPortMappings(t *testing.T) {
}, },
}, },
{ {
name: "default host ip is nonzero v4", name: "default host ip is nonzero v4",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
defHostIP: newIPNet(t, "127.0.0.1/8").IP, defHostIP: newIPNet(t, "127.0.0.1/8").IP,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/8").IP, HostPort: firstEphemPort}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/8").IP, HostPort: firstEphemPort},
}, },
}, },
{ {
name: "default host ip is nonzero IPv4-mapped IPv6", name: "default host ip is nonzero IPv4-mapped IPv6",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
defHostIP: newIPNet(t, "::ffff:127.0.0.1/72").IP, defHostIP: newIPNet(t, "::ffff:127.0.0.1/72").IP,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/8").IP, HostPort: firstEphemPort}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/8").IP, HostPort: firstEphemPort},
}, },
}, },
{ {
name: "default host ip is v6", name: "default host ip is v6",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
defHostIP: net.IPv6zero, defHostIP: net.IPv6zero,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort},
}, },
}, },
{ {
name: "default host ip is nonzero v6", name: "default host ip is nonzero v6",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}}, cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
defHostIP: newIPNet(t, "::1/128").IP, defHostIP: newIPNet(t, "::1/128").IP,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: newIPNet(t, "::1/128").IP, HostPort: firstEphemPort}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: newIPNet(t, "::1/128").IP, HostPort: firstEphemPort},
}, },
@@ -487,17 +480,17 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22, HostPort: 2222}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22, HostPort: 2222}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: 2222}, {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}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: 2222},
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080}, {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: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080},
}, },
expReleaseErr: "failed to stop userland proxy for port mapping 0.0.0.0:2222:172.19.0.2:22/tcp: can't stop now\n" + expReleaseErr: "unmapping port binding 0.0.0.0:2222:172.19.0.2:22/tcp: failed to stop userland proxy: can't stop now\n" +
"failed to stop userland proxy for port mapping [::]:2222:[fdf8:b88e:bb5c:3483::2]:22/tcp: can't stop now\n" + "unmapping port binding [::]:2222:[fdf8:b88e:bb5c:3483::2]:22/tcp: failed to stop userland proxy: can't stop now\n" +
"failed to stop userland proxy for port mapping 0.0.0.0:8080:172.19.0.2:80/tcp: can't stop now\n" + "unmapping port binding 0.0.0.0:8080:172.19.0.2:80/tcp: failed to stop userland proxy: can't stop now\n" +
"failed to stop userland proxy for port mapping [::]:8080:[fdf8:b88e:bb5c:3483::2]:80/tcp: can't stop now", "unmapping port binding [::]:8080:[fdf8:b88e:bb5c:3483::2]:80/tcp: failed to stop userland proxy: can't stop now",
}, },
{ {
name: "disable nat6", name: "disable nat6",
@@ -507,8 +500,8 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
gwMode6: gwModeRouted, gwMode6: gwModeRouted,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero},
@@ -524,9 +517,9 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
gwMode6: gwModeRouted, gwMode6: gwModeRouted,
defHostIP: net.IPv6loopback, defHostIP: net.IPv6loopback,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero},
@@ -540,8 +533,8 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
gwMode4: gwModeRouted, gwMode4: gwModeRouted,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort},
@@ -557,9 +550,9 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
gwMode4: gwModeRouted, gwMode4: gwModeRouted,
gwMode6: gwModeRouted, gwMode6: gwModeRouted,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero},
@@ -587,12 +580,12 @@ func TestAddPortMappings(t *testing.T) {
expLogs: []string{"Cannot map from default host binding address to an IPv4-only container because the userland proxy is disabled"}, expLogs: []string{"Cannot map from default host binding address to an IPv4-only container because the userland proxy is disabled"},
}, },
{ {
name: "routed mode specific address", name: "routed mode specific address",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
gwMode4: gwModeRouted, gwMode4: gwModeRouted,
gwMode6: gwModeRouted, gwMode6: gwModeRouted,
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
cfg: []portmapperapi.PortBindingReq{ cfg: []portmapperapi.PortBindingReq{
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22, HostIP: newIPNet(t, "127.0.0.1/8").IP}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22, HostIP: newIPNet(t, "127.0.0.1/8").IP}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22, HostIP: net.IPv6loopback}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22, HostIP: net.IPv6loopback}},
@@ -607,12 +600,12 @@ func TestAddPortMappings(t *testing.T) {
}, },
}, },
{ {
name: "routed4 nat6 with ipv4 default binding", name: "routed4 nat6 with ipv4 default binding",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
gwMode4: gwModeRouted, gwMode4: gwModeRouted,
defHostIP: newIPNet(t, "127.0.0.1/8").IP, defHostIP: newIPNet(t, "127.0.0.1/8").IP,
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
cfg: []portmapperapi.PortBindingReq{ cfg: []portmapperapi.PortBindingReq{
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
}, },
@@ -621,12 +614,12 @@ func TestAddPortMappings(t *testing.T) {
}, },
}, },
{ {
name: "routed4 nat6 with ipv6 default binding", name: "routed4 nat6 with ipv6 default binding",
epAddrV4: ctrIP4, epAddrV4: ctrIP4,
epAddrV6: ctrIP6, epAddrV6: ctrIP6,
gwMode4: gwModeRouted, gwMode4: gwModeRouted,
defHostIP: net.IPv6loopback, defHostIP: net.IPv6loopback,
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
cfg: []portmapperapi.PortBindingReq{ cfg: []portmapperapi.PortBindingReq{
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
}, },
@@ -672,7 +665,7 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 12345, HostPort: 12345, HostPortEnd: 12346}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 12345, HostPort: 12345, HostPortEnd: 12346}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 12345, HostPort: 12345}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 12345, HostPort: 12345}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 12345, HostIP: net.IPv4zero, HostPort: 12345}, {Proto: types.TCP, IP: ctrIP4.IP, Port: 12345, HostIP: net.IPv4zero, HostPort: 12345},
{Proto: types.TCP, IP: ctrIP6.IP, Port: 12345, HostIP: net.IPv6zero, HostPort: 12345}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 12345, HostIP: net.IPv6zero, HostPort: 12345},
@@ -693,8 +686,8 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
}, },
proxyPath: "/dummy/path/to/proxy", enableProxy: true,
rootless: true, rootless: true,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort}, {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: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort},
@@ -727,17 +720,12 @@ func TestAddPortMappings(t *testing.T) {
useStubFirewaller(t) useStubFirewaller(t)
// Mock the startProxy function used by the code under test. // Mock the startProxy function used by the code under test.
origStartProxy := startProxy
defer func() { startProxy = origStartProxy }()
proxies := map[proxyCall]bool{} // proxy -> is not stopped proxies := map[proxyCall]bool{} // proxy -> is not stopped
startProxy = func(pb types.PortBinding, startProxy := func(pb types.PortBinding, listenSock *os.File) (stop func() error, retErr error) {
proxyPath string,
listenSock *os.File,
) (stop func() error, retErr error) {
if tc.busyPortIPv4 > 0 && tc.busyPortIPv4 == int(pb.HostPort) && pb.HostIP.To4() != nil { if tc.busyPortIPv4 > 0 && tc.busyPortIPv4 == int(pb.HostPort) && pb.HostIP.To4() != nil {
return nil, errors.New("busy port") return nil, errors.New("busy port")
} }
c := newProxyCall(pb.Proto.String(), pb.HostIP, int(pb.HostPort), pb.IP, int(pb.Port), proxyPath) c := newProxyCall(pb.Proto.String(), pb.HostIP, int(pb.HostPort), pb.IP, int(pb.Port))
if _, ok := proxies[c]; ok { if _, ok := proxies[c]; ok {
return nil, fmt.Errorf("duplicate proxy: %#v", c) return nil, fmt.Errorf("duplicate proxy: %#v", c)
} }
@@ -754,13 +742,6 @@ func TestAddPortMappings(t *testing.T) {
}, nil }, nil
} }
// Mock the RootlessKit port driver.
origNewPortDriverClient := newPortDriverClient
defer func() { newPortDriverClient = origNewPortDriverClient }()
newPortDriverClient = func(ctx context.Context) (portDriverClient, error) {
return newMockPortDriverClient(ctx)
}
if len(tc.hostAddrs) > 0 { if len(tc.hostAddrs) > 0 {
dummyLink := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "br-dummy"}} dummyLink := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "br-dummy"}}
err := netlink.LinkAdd(dummyLink) err := netlink.LinkAdd(dummyLink)
@@ -783,6 +764,21 @@ func TestAddPortMappings(t *testing.T) {
defer ul.Close() defer ul.Close()
} }
var pdc nat.PortDriverClient
if tc.rootless {
pdc = newMockPortDriverClient()
}
pms := &drvregistry.PortMappers{}
err := nat.Register(pms, nat.Config{
RlkClient: pdc,
EnableProxy: tc.enableProxy,
StartProxy: startProxy,
})
assert.NilError(t, err)
err = routed.Register(pms)
assert.NilError(t, err)
n := &bridgeNetwork{ n := &bridgeNetwork{
config: &networkConfiguration{ config: &networkConfiguration{
BridgeName: "dummybridge", BridgeName: "dummybridge",
@@ -792,26 +788,22 @@ func TestAddPortMappings(t *testing.T) {
GwModeIPv6: tc.gwMode6, GwModeIPv6: tc.gwMode6,
}, },
bridge: &bridgeInterface{}, bridge: &bridgeInterface{},
driver: newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{}), driver: newDriver(storeutils.NewTempStore(t), pms),
} }
genericOption := map[string]interface{}{ genericOption := map[string]interface{}{
netlabel.GenericData: &configuration{ netlabel.GenericData: &configuration{
EnableIPTables: true, EnableIPTables: true,
EnableIP6Tables: true, EnableIP6Tables: true,
EnableUserlandProxy: tc.proxyPath != "", Hairpin: tc.hairpin,
UserlandProxyPath: tc.proxyPath,
Hairpin: tc.hairpin,
Rootless: tc.rootless,
}, },
} }
err := n.driver.configure(genericOption) err = n.driver.configure(genericOption)
assert.NilError(t, err) assert.NilError(t, err)
fwn, err := n.newFirewallerNetwork(context.Background()) fwn, err := n.newFirewallerNetwork(context.Background())
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, fwn != nil, "no firewaller network") assert.Check(t, fwn != nil, "no firewaller network")
n.firewallerNetwork = fwn n.firewallerNetwork = fwn
assert.Check(t, is.Equal(n.driver.portDriverClient == nil, !tc.rootless))
expChildIP := func(hostIP net.IP) net.IP { expChildIP := func(hostIP net.IP) net.IP {
if !tc.rootless { if !tc.rootless {
return hostIP return hostIP
@@ -860,7 +852,7 @@ func TestAddPortMappings(t *testing.T) {
assert.Assert(t, is.Len(pbs, len(tc.expPBs))) assert.Assert(t, is.Len(pbs, len(tc.expPBs)))
fw := n.driver.firewaller.(*firewaller.StubFirewaller) fw := n.driver.firewaller.(*firewaller.StubFirewaller)
assert.Check(t, is.Equal(fw.Hairpin, tc.proxyPath == "")) assert.Check(t, is.Equal(fw.Hairpin, !tc.enableProxy))
assert.Check(t, fw.IPv4) assert.Check(t, fw.IPv4)
assert.Check(t, fw.IPv6) assert.Check(t, fw.IPv6)
@@ -903,7 +895,7 @@ func TestAddPortMappings(t *testing.T) {
} }
// Check a docker-proxy was started and stopped for each expected port binding. // Check a docker-proxy was started and stopped for each expected port binding.
if tc.proxyPath != "" { if tc.enableProxy {
expProxies := map[proxyCall]bool{} expProxies := map[proxyCall]bool{}
for _, expPB := range tc.expPBs { for _, expPB := range tc.expPBs {
hip := expChildIP(expPB.HostIP) hip := expChildIP(expPB.HostIP)
@@ -913,7 +905,7 @@ func TestAddPortMappings(t *testing.T) {
} }
p := newProxyCall(expPB.Proto.String(), p := newProxyCall(expPB.Proto.String(),
hip, int(expPB.HostPort), hip, int(expPB.HostPort),
expPB.IP, int(expPB.Port), tc.proxyPath) expPB.IP, int(expPB.Port))
expProxies[p] = tc.expReleaseErr != "" expProxies[p] = tc.expReleaseErr != ""
} }
assert.Check(t, is.DeepEqual(expProxies, proxies)) assert.Check(t, is.DeepEqual(expProxies, proxies))
@@ -921,8 +913,8 @@ func TestAddPortMappings(t *testing.T) {
// Check the port driver has seen the expected port mappings and no others, // Check the port driver has seen the expected port mappings and no others,
// and that they have all been closed. // and that they have all been closed.
if n.driver.portDriverClient != nil { if pdc != nil {
pdc := n.driver.portDriverClient.(*mockPortDriverClient) pdc := pdc.(*mockPortDriverClient)
expPorts := map[mockPortDriverPort]bool{} expPorts := map[mockPortDriverPort]bool{}
for _, expPB := range tc.expPBs { for _, expPB := range tc.expPBs {
if expPB.HostPort == 0 { if expPB.HostPort == 0 {
@@ -943,18 +935,16 @@ func TestAddPortMappings(t *testing.T) {
} }
// Type for tracking calls to StartProxy. // Type for tracking calls to StartProxy.
type proxyCall struct{ proto, host, container, proxyPath string } type proxyCall struct{ proto, host, container string }
func newProxyCall(proto string, func newProxyCall(proto string,
hostIP net.IP, hostPort int, hostIP net.IP, hostPort int,
containerIP net.IP, containerPort int, containerIP net.IP, containerPort int,
proxyPath string,
) proxyCall { ) proxyCall {
return proxyCall{ return proxyCall{
proto: proto, proto: proto,
host: fmt.Sprintf("%v:%v", hostIP, hostPort), host: fmt.Sprintf("%v:%v", hostIP, hostPort),
container: fmt.Sprintf("%v:%v", containerIP, containerPort), container: fmt.Sprintf("%v:%v", containerIP, containerPort),
proxyPath: proxyPath,
} }
} }
@@ -975,10 +965,10 @@ type mockPortDriverClient struct {
openPorts map[mockPortDriverPort]bool openPorts map[mockPortDriverPort]bool
} }
func newMockPortDriverClient(_ context.Context) (*mockPortDriverClient, error) { func newMockPortDriverClient() *mockPortDriverClient {
return &mockPortDriverClient{ return &mockPortDriverClient{
openPorts: map[mockPortDriverPort]bool{}, openPorts: map[mockPortDriverPort]bool{},
}, nil }
} }
func (c *mockPortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr { func (c *mockPortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr {
@@ -1002,3 +992,33 @@ func (c *mockPortDriverClient) AddPort(_ context.Context, proto string, hostIP,
return nil return nil
}, nil }, nil
} }
type stubPortMapper struct {
reqs [][]portmapperapi.PortBindingReq
mapped []portmapperapi.PortBinding
}
func (pm *stubPortMapper) MapPorts(_ context.Context, reqs []portmapperapi.PortBindingReq, _ portmapperapi.Firewaller) ([]portmapperapi.PortBinding, error) {
if len(reqs) == 0 {
return []portmapperapi.PortBinding{}, nil
}
pm.reqs = append(pm.reqs, reqs)
pbs := sliceutil.Map(reqs, func(req portmapperapi.PortBindingReq) portmapperapi.PortBinding {
return portmapperapi.PortBinding{PortBinding: req.PortBinding}
})
pm.mapped = append(pm.mapped, pbs...)
return pbs, nil
}
func (pm *stubPortMapper) UnmapPorts(_ context.Context, reqs []portmapperapi.PortBinding, _ portmapperapi.Firewaller) error {
for _, req := range reqs {
idx := slices.IndexFunc(pm.mapped, func(pb portmapperapi.PortBinding) bool {
return pb.Equal(&req.PortBinding)
})
if idx == -1 {
return fmt.Errorf("stubPortMapper.UnmapPorts: pb doesn't exist %v", req)
}
pm.mapped = slices.Delete(pm.mapped, idx, idx)
}
return nil
}

View File

@@ -3,6 +3,7 @@ package libnetwork
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"github.com/docker/docker/daemon/libnetwork/config" "github.com/docker/docker/daemon/libnetwork/config"
"github.com/docker/docker/daemon/libnetwork/datastore" "github.com/docker/docker/daemon/libnetwork/datastore"
@@ -14,6 +15,11 @@ import (
"github.com/docker/docker/daemon/libnetwork/drivers/null" "github.com/docker/docker/daemon/libnetwork/drivers/null"
"github.com/docker/docker/daemon/libnetwork/drivers/overlay" "github.com/docker/docker/daemon/libnetwork/drivers/overlay"
"github.com/docker/docker/daemon/libnetwork/drvregistry" "github.com/docker/docker/daemon/libnetwork/drvregistry"
"github.com/docker/docker/daemon/libnetwork/internal/rlkclient"
"github.com/docker/docker/daemon/libnetwork/portmapper"
"github.com/docker/docker/daemon/libnetwork/portmappers/nat"
"github.com/docker/docker/daemon/libnetwork/portmappers/routed"
"github.com/docker/docker/daemon/libnetwork/types"
) )
func registerNetworkDrivers(r driverapi.Registerer, store *datastore.Store, pms *drvregistry.PortMappers, driverConfig func(string) map[string]interface{}) error { func registerNetworkDrivers(r driverapi.Registerer, store *datastore.Store, pms *drvregistry.PortMappers, driverConfig func(string) map[string]interface{}) error {
@@ -45,5 +51,28 @@ func registerNetworkDrivers(r driverapi.Registerer, store *datastore.Store, pms
} }
func registerPortMappers(ctx context.Context, r *drvregistry.PortMappers, cfg *config.Config) error { func registerPortMappers(ctx context.Context, r *drvregistry.PortMappers, cfg *config.Config) error {
var pdc *rlkclient.PortDriverClient
if cfg.Rootless {
var err error
pdc, err = rlkclient.NewPortDriverClient(ctx)
if err != nil {
return fmt.Errorf("failed to create port driver client: %w", err)
}
}
if err := nat.Register(r, nat.Config{
RlkClient: pdc,
StartProxy: func(pb types.PortBinding, file *os.File) (func() error, error) {
return portmapper.StartProxy(pb, cfg.UserlandProxyPath, file)
},
EnableProxy: cfg.EnableUserlandProxy && cfg.UserlandProxyPath != "",
}); err != nil {
return fmt.Errorf("registering nat portmapper: %w", err)
}
if err := routed.Register(r); err != nil {
return fmt.Errorf("registering routed portmapper: %w", err)
}
return nil return nil
} }

View File

@@ -5,6 +5,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strings"
"github.com/docker/docker/daemon/libnetwork/types" "github.com/docker/docker/daemon/libnetwork/types"
) )
@@ -36,14 +37,12 @@ type PortMapper interface {
type PortBindingReq struct { type PortBindingReq struct {
types.PortBinding types.PortBinding
// Mapper is the name of the port mapper used to process this PortBindingReq.
Mapper string
// ChildHostIP is a temporary field used to pass the host IP address as // ChildHostIP is a temporary field used to pass the host IP address as
// seen from the daemon. (It'll be removed once the portmapper API is // seen from the daemon. (It'll be removed once the portmapper API is
// implemented). // implemented).
ChildHostIP net.IP `json:"-"` ChildHostIP net.IP `json:"-"`
// DisableNAT is a temporary field used to indicate whether the port is
// mapped on the host or not. (It'll be removed once the portmapper API is
// implemented).
DisableNAT bool `json:"-"`
} }
// Compare defines an ordering over PortBindingReq such that bindings that // Compare defines an ordering over PortBindingReq such that bindings that
@@ -58,11 +57,8 @@ type PortBindingReq struct {
// - same host ports or ranges are adjacent, then // - same host ports or ranges are adjacent, then
// - ordered by container IP (then host IP, if set). // - ordered by container IP (then host IP, if set).
func (pbReq PortBindingReq) Compare(other PortBindingReq) int { func (pbReq PortBindingReq) Compare(other PortBindingReq) int {
if pbReq.DisableNAT != other.DisableNAT { if pbReq.Mapper != other.Mapper {
if pbReq.DisableNAT { return strings.Compare(pbReq.Mapper, other.Mapper)
return 1 // NAT disabled bindings come last
}
return -1
} }
// Exact host port < host port range. // Exact host port < host port range.
aIsRange := pbReq.HostPort == 0 || pbReq.HostPort != pbReq.HostPortEnd aIsRange := pbReq.HostPort == 0 || pbReq.HostPort != pbReq.HostPortEnd
@@ -97,6 +93,8 @@ func (pbReq PortBindingReq) Compare(other PortBindingReq) int {
type PortBinding struct { type PortBinding struct {
types.PortBinding types.PortBinding
// Mapper is the name of the port mapper used to process this PortBinding.
Mapper string
// BoundSocket is used to reserve a host port for the binding. If the // BoundSocket is used to reserve a host port for the binding. If the
// userland proxy is in-use, it's passed to the proxy when the proxy is // userland proxy is in-use, it's passed to the proxy when the proxy is
// started, then it's closed and set to nil here. // started, then it's closed and set to nil here.

View File

@@ -24,7 +24,7 @@ func TestPortBindingReqsCompare(t *testing.T) {
assert.Check(t, pb.Compare(pb) == 0) //nolint:gocritic // ignore "dupArg: suspicious method call with the same argument and receiver (gocritic)" assert.Check(t, pb.Compare(pb) == 0) //nolint:gocritic // ignore "dupArg: suspicious method call with the same argument and receiver (gocritic)"
pbA, pbB = pb, pb pbA, pbB = pb, pb
pbB.DisableNAT = true pbB.Mapper = "routed"
assert.Check(t, pbA.Compare(pbB) < 0) assert.Check(t, pbA.Compare(pbB) < 0)
assert.Check(t, pbB.Compare(pbA) > 0) assert.Check(t, pbB.Compare(pbA) > 0)

View File

@@ -0,0 +1,325 @@
package nat
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"os"
"strconv"
"syscall"
"github.com/containerd/log"
"github.com/docker/docker/daemon/libnetwork/internal/rlkclient"
"github.com/docker/docker/daemon/libnetwork/portallocator"
"github.com/docker/docker/daemon/libnetwork/portmapperapi"
"github.com/docker/docker/daemon/libnetwork/types"
)
const (
driverName = "nat"
maxAllocatePortAttempts = 10
)
type PortDriverClient interface {
ChildHostIP(hostIP netip.Addr) netip.Addr
AddPort(ctx context.Context, proto string, hostIP, childIP netip.Addr, hostPort int) (func() error, error)
}
type proxyStarter func(types.PortBinding, *os.File) (func() error, error)
// Register the "nat" port-mapper with libnetwork.
func Register(r portmapperapi.Registerer, cfg Config) error {
return r.Register(driverName, NewPortMapper(cfg))
}
type PortMapper struct {
// pdc is used to interact with rootlesskit port driver.
pdc PortDriverClient
startProxy proxyStarter
enableProxy bool
}
type Config struct {
// RlkClient is called by MapPorts to determine the ChildHostIP and ask
// rootlesskit to map ports in its netns.
RlkClient PortDriverClient
StartProxy proxyStarter
EnableProxy bool
}
func NewPortMapper(cfg Config) PortMapper {
return PortMapper{
pdc: cfg.RlkClient,
startProxy: cfg.StartProxy,
enableProxy: cfg.EnableProxy,
}
}
// MapPorts allocates and binds host ports 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 (pm PortMapper) MapPorts(ctx context.Context, cfg []portmapperapi.PortBindingReq, fwn portmapperapi.Firewaller) ([]portmapperapi.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 bindings []portmapperapi.PortBinding
var err error
for i := 0; i < maxAllocatePortAttempts; i++ {
bindings, err = pm.attemptBindHostPorts(ctx, cfg, proto, hostPort, hostPortEnd, fwn)
if err == nil {
break
}
// There is no point in immediately retrying to map an explicitly chosen port.
if hostPort != 0 && hostPort == hostPortEnd {
log.G(ctx).WithError(err).Warnf("Failed to allocate and map port")
return nil, err
}
log.G(ctx).WithFields(log.Fields{
"error": err,
"attempt": i + 1,
}).Warn("Failed to allocate and map port")
}
if err != nil {
// If the retry budget is exhausted and no free port could be found, return
// the latest error.
return nil, err
}
// Start userland proxy processes.
if pm.enableProxy {
for i := range bindings {
if bindings[i].BoundSocket == nil || bindings[i].RootlesskitUnsupported || bindings[i].StopProxy != nil {
continue
}
var err error
bindings[i].StopProxy, err = pm.startProxy(
bindings[i].ChildPortBinding(), bindings[i].BoundSocket,
)
if err != nil {
return nil, fmt.Errorf("failed to start userland proxy for port mapping %s: %w",
bindings[i].PortBinding, err)
}
if err := bindings[i].BoundSocket.Close(); err != nil {
log.G(ctx).WithFields(log.Fields{
"error": err,
"mapping": bindings[i].PortBinding,
}).Warnf("failed to close proxy socket")
}
bindings[i].BoundSocket = nil
}
}
return bindings, nil
}
func (pm PortMapper) UnmapPorts(ctx context.Context, pbs []portmapperapi.PortBinding, fwn portmapperapi.Firewaller) error {
var errs []error
for _, pb := range pbs {
if pb.BoundSocket != nil {
if err := pb.BoundSocket.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close socket for port mapping %s: %w", pb, err))
}
}
if pb.PortDriverRemove != nil {
if err := pb.PortDriverRemove(); err != nil {
errs = append(errs, err)
}
}
if pb.StopProxy != nil {
if err := pb.StopProxy(); err != nil && !errors.Is(err, os.ErrProcessDone) {
errs = append(errs, fmt.Errorf("failed to stop userland proxy: %w", err))
}
}
}
if err := fwn.DelPorts(ctx, mergeChildHostIPs(pbs)); err != nil {
errs = append(errs, err)
}
for _, pb := range pbs {
portallocator.Get().ReleasePort(pb.ChildHostIP, pb.Proto.String(), int(pb.HostPort))
}
return errors.Join(errs...)
}
// attemptBindHostPorts allocates host ports for each NAT port mapping, and
// reserves those ports by binding them.
//
// If the allocator doesn't have an available port in the required range, or the
// port can't be bound (perhaps because another process has already bound it),
// all resources are released and an error is returned. When ports are
// successfully reserved, a PortBinding is returned for each mapping.
func (pm PortMapper) attemptBindHostPorts(
ctx context.Context,
cfg []portmapperapi.PortBindingReq,
proto types.Protocol,
hostPortStart, hostPortEnd uint16,
fwn portmapperapi.Firewaller,
) (_ []portmapperapi.PortBinding, retErr error) {
var err error
var port int
addrs := make([]net.IP, 0, len(cfg))
for i := range cfg {
cfg[i] = setChildHostIP(pm.pdc, cfg[i])
addrs = append(addrs, cfg[i].ChildHostIP)
}
pa := portallocator.NewOSAllocator()
port, socks, err := pa.RequestPortsInRange(addrs, proto, int(hostPortStart), int(hostPortEnd))
if err != nil {
return nil, err
}
defer func() {
if retErr != nil {
pa.ReleasePorts(addrs, proto, port)
}
}()
if len(socks) != len(cfg) {
for _, sock := range socks {
if err := sock.Close(); err != nil {
log.G(ctx).WithError(err).Warn("Failed to close socket")
}
}
return nil, types.InternalErrorf("port allocator returned %d sockets for %d port bindings", len(socks), len(cfg))
}
res := make([]portmapperapi.PortBinding, 0, len(cfg))
defer func() {
if retErr != nil {
if err := pm.UnmapPorts(ctx, res, fwn); err != nil {
log.G(ctx).WithFields(log.Fields{
"pbs": res,
"error": err,
}).Warn("Failed to release port bindings")
}
}
}()
for i := range cfg {
pb := portmapperapi.PortBinding{
PortBinding: cfg[i].PortBinding.GetCopy(),
BoundSocket: socks[i],
ChildHostIP: cfg[i].ChildHostIP,
}
pb.PortBinding.HostPort = uint16(port)
pb.PortBinding.HostPortEnd = pb.HostPort
res = append(res, pb)
}
if err := configPortDriver(ctx, res, pm.pdc); err != nil {
return nil, err
}
if err := fwn.AddPorts(ctx, mergeChildHostIPs(res)); err != nil {
return nil, err
}
// Now the firewall rules are set up, it's safe to listen on the socket. (Listening
// earlier could result in dropped connections if the proxy becomes unreachable due
// to NAT rules sending packets directly to the container.)
//
// If not starting the proxy, nothing will ever accept a connection on the
// socket. Listen here anyway because SO_REUSEADDR is set, so bind() won't notice
// the problem if a port's bound to both INADDR_ANY and a specific address. (Also
// so the binding shows up in "netstat -at".)
if err := listenBoundPorts(res, pm.enableProxy); err != nil {
return nil, err
}
return res, nil
}
func setChildHostIP(pdc PortDriverClient, req portmapperapi.PortBindingReq) portmapperapi.PortBindingReq {
if pdc == nil {
req.ChildHostIP = req.HostIP
return req
}
hip, _ := netip.AddrFromSlice(req.HostIP)
req.ChildHostIP = pdc.ChildHostIP(hip).AsSlice()
return req
}
// mergeChildHostIPs take a slice of PortBinding and returns a slice of
// types.PortBinding, where the HostIP in each of the results has the
// value of ChildHostIP from the input (if present).
func mergeChildHostIPs(pbs []portmapperapi.PortBinding) []types.PortBinding {
res := make([]types.PortBinding, 0, len(pbs))
for _, b := range pbs {
pb := b.PortBinding
if b.ChildHostIP != nil {
pb.HostIP = b.ChildHostIP
}
res = append(res, pb)
}
return res
}
// configPortDriver passes the port binding's details to rootlesskit, and updates the
// port binding with callbacks to remove the rootlesskit config (or marks the binding as
// unsupported by rootlesskit).
func configPortDriver(ctx context.Context, pbs []portmapperapi.PortBinding, pdc PortDriverClient) error {
for i := range pbs {
b := pbs[i]
if pdc != nil && b.HostPort != 0 {
var err error
hip, ok := netip.AddrFromSlice(b.HostIP)
if !ok {
return fmt.Errorf("invalid host IP address in %s", b)
}
chip, ok := netip.AddrFromSlice(b.ChildHostIP)
if !ok {
return fmt.Errorf("invalid child host IP address %s in %s", b.ChildHostIP, b)
}
pbs[i].PortDriverRemove, err = pdc.AddPort(ctx, b.Proto.String(), hip, chip, int(b.HostPort))
if err != nil {
var pErr *rlkclient.ProtocolUnsupportedError
if errors.As(err, &pErr) {
log.G(ctx).WithFields(log.Fields{
"error": pErr,
}).Warnf("discarding request for %q", net.JoinHostPort(hip.String(), strconv.Itoa(int(b.HostPort))))
pbs[i].RootlesskitUnsupported = true
continue
}
return err
}
}
}
return nil
}
func listenBoundPorts(pbs []portmapperapi.PortBinding, proxyEnabled bool) error {
for i := range pbs {
if pbs[i].BoundSocket == nil || pbs[i].RootlesskitUnsupported || pbs[i].Proto == types.UDP {
continue
}
rc, err := pbs[i].BoundSocket.SyscallConn()
if err != nil {
return fmt.Errorf("raw conn not available on %d socket: %w", pbs[i].Proto, err)
}
if errC := rc.Control(func(fd uintptr) {
somaxconn := 0
// SCTP sockets do not support somaxconn=0
if proxyEnabled || pbs[i].Proto == types.SCTP {
somaxconn = -1 // silently capped to "/proc/sys/net/core/somaxconn"
}
err = syscall.Listen(int(fd), somaxconn)
}); errC != nil {
return fmt.Errorf("failed to Control %s socket: %w", pbs[i].Proto, err)
}
if err != nil {
return fmt.Errorf("failed to listen on %s socket: %w", pbs[i].Proto, err)
}
}
return nil
}

View File

@@ -0,0 +1,36 @@
package nat
import (
"context"
"testing"
"github.com/docker/docker/daemon/libnetwork/portmapperapi"
"github.com/docker/docker/daemon/libnetwork/types"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestBindHostPortsError(t *testing.T) {
cfg := []portmapperapi.PortBindingReq{
{
PortBinding: types.PortBinding{
Proto: types.TCP,
Port: 80,
HostPort: 8080,
HostPortEnd: 8080,
},
},
{
PortBinding: types.PortBinding{
Proto: types.TCP,
Port: 80,
HostPort: 8080,
HostPortEnd: 8081,
},
},
}
pm := &PortMapper{}
pbs, err := pm.MapPorts(context.Background(), cfg, nil)
assert.Check(t, is.Error(err, "port binding mismatch 80/tcp:8080-8080, 80/tcp:8080-8081"))
assert.Check(t, is.Nil(pbs))
}

View File

@@ -0,0 +1,60 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23
package routed
import (
"context"
"github.com/containerd/log"
"github.com/docker/docker/daemon/libnetwork/portmapperapi"
"github.com/docker/docker/daemon/libnetwork/types"
"github.com/docker/docker/internal/sliceutil"
)
const driverName = "routed"
// Register the "routed" port-mapper with libnetwork.
func Register(r portmapperapi.Registerer) error {
return r.Register(driverName, NewPortMapper())
}
type PortMapper struct{}
func NewPortMapper() PortMapper {
return PortMapper{}
}
// MapPorts sets up firewall rules to allow direct remote access to pbs.
func (pm PortMapper) MapPorts(ctx context.Context, reqs []portmapperapi.PortBindingReq, fwn portmapperapi.Firewaller) ([]portmapperapi.PortBinding, error) {
if len(reqs) == 0 {
return nil, nil
}
res := make([]portmapperapi.PortBinding, 0, len(reqs))
bindings := make([]types.PortBinding, 0, len(reqs))
for _, c := range reqs {
pb := portmapperapi.PortBinding{PortBinding: c.GetCopy()}
if pb.HostPort != 0 || pb.HostPortEnd != 0 {
log.G(ctx).WithFields(log.Fields{"mapping": pb}).Infof(
"Host port ignored, because NAT is disabled")
pb.HostPort = 0
pb.HostPortEnd = 0
}
res = append(res, pb)
bindings = append(bindings, pb.PortBinding)
}
if err := fwn.AddPorts(ctx, bindings); err != nil {
return nil, err
}
return res, nil
}
// UnmapPorts removes firewall rules allowing direct remote access to the pbs.
func (pm PortMapper) UnmapPorts(ctx context.Context, pbs []portmapperapi.PortBinding, fwn portmapperapi.Firewaller) error {
return fwn.DelPorts(ctx, sliceutil.Map(pbs, func(pb portmapperapi.PortBinding) types.PortBinding {
return pb.PortBinding
}))
}