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.OptionNetworkControlPlaneMTU(conf.NetworkControlPlaneMTU),
nwconfig.OptionFirewallBackend(conf.FirewallBackend),
driverOptions(conf),
}
options = append(options, networkPlatformOptions(conf)...)
defaultAddressPools := ipamutils.GetLocalScopeDefaultNetworks()
if len(conf.NetworkConfig.DefaultAddressPools.Value()) > 0 {
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 {
return nwconfig.OptionDriverConfig("bridge", options.Generic{
netlabel.GenericData: options.Generic{
"EnableIPForwarding": config.BridgeConfig.EnableIPForward,
"DisableFilterForwardDrop": config.BridgeConfig.DisableFilterForwardDrop,
"EnableIPTables": config.BridgeConfig.EnableIPTables,
"EnableIP6Tables": config.BridgeConfig.EnableIP6Tables,
"EnableUserlandProxy": config.EnableUserlandProxy,
"UserlandProxyPath": config.UserlandProxyPath,
"Hairpin": !config.EnableUserlandProxy || config.UserlandProxyPath == "",
"AllowDirectRouting": config.BridgeConfig.AllowDirectRouting,
"Rootless": config.Rootless,
},
})
// networkPlatformOptions returns a slice of platform-specific libnetwork
// options.
func networkPlatformOptions(conf *config.Config) []nwconfig.Option {
return []nwconfig.Option{
nwconfig.OptionRootless(conf.Rootless),
nwconfig.OptionUserlandProxy(conf.EnableUserlandProxy, conf.UserlandProxyPath),
nwconfig.OptionDriverConfig("bridge", options.Generic{
netlabel.GenericData: options.Generic{
"EnableIPForwarding": conf.BridgeConfig.EnableIPForward,
"DisableFilterForwardDrop": conf.BridgeConfig.DisableFilterForwardDrop,
"EnableIPTables": conf.BridgeConfig.EnableIPTables,
"EnableIP6Tables": conf.BridgeConfig.EnableIP6Tables,
"Hairpin": !conf.EnableUserlandProxy || conf.UserlandProxyPath == "",
"AllowDirectRouting": conf.BridgeConfig.AllowDirectRouting,
},
}),
}
}
type defBrOptsV4 struct {

View File

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

View File

@@ -43,6 +43,9 @@ type Config struct {
ActiveSandboxes map[string]any
PluginGetter plugingetter.PluginGetter
FirewallBackend string
Rootless bool
EnableUserlandProxy bool
UserlandProxyPath string
}
// New creates a new Config and initializes it with the given Options.
@@ -162,3 +165,20 @@ func OptionFirewallBackend(val string) Option {
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/internal/netiputil"
"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/netlabel"
"github.com/docker/docker/daemon/libnetwork/netutils"
@@ -51,7 +50,6 @@ const (
vethPrefix = "veth"
vethLen = len(vethPrefix) + 7
defaultContainerVethPrefix = "eth"
maxAllocatePortAttempts = 10
)
const (
@@ -74,13 +72,10 @@ type configuration struct {
DisableFilterForwardDrop bool
EnableIPTables bool
EnableIP6Tables bool
EnableUserlandProxy bool
UserlandProxyPath string
// Hairpin indicates whether packets sent from a container to a host port
// published by another container on the same bridge network should be
// hairpinned.
Hairpin bool
Rootless bool
AllowDirectRouting bool
}
@@ -161,25 +156,14 @@ type bridgeNetwork struct {
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 {
config configuration
networks map[string]*bridgeNetwork
store *datastore.Store
nlh nlwrap.Handle
portDriverClient portDriverClient
configNetwork sync.Mutex
firewaller firewaller.Firewaller
portmappers *drvregistry.PortMappers
config configuration
networks map[string]*bridgeNetwork
store *datastore.Store
nlh nlwrap.Handle
configNetwork sync.Mutex
firewaller firewaller.Firewaller
portmappers *drvregistry.PortMappers
sync.Mutex
}
@@ -476,15 +460,6 @@ func (n *bridgeNetwork) gwMode(v firewaller.IPVersion) gwMode {
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 {
n.Lock()
defer n.Unlock()
@@ -494,13 +469,13 @@ func (n *bridgeNetwork) hairpin() bool {
return n.driver.config.Hairpin
}
func (n *bridgeNetwork) getPortDriverClient() portDriverClient {
func (n *bridgeNetwork) portMappers() *drvregistry.PortMappers {
n.Lock()
defer n.Unlock()
if n.driver == nil {
return nil
}
return n.driver.getPortDriverClient()
return n.driver.portmappers
}
func (n *bridgeNetwork) getEndpoint(eid string) (*bridgeEndpoint, error) {
@@ -546,17 +521,7 @@ func (d *driver) configure(option map[string]interface{}) error {
return err
}
var pdc portDriverClient
if config.Rootless {
var err error
pdc, err = newPortDriverClient(context.TODO())
if err != nil {
return err
}
}
d.Lock()
d.portDriverClient = pdc
d.config = config
d.Unlock()
@@ -604,22 +569,6 @@ 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 (d *driver) getPortDriverClient() portDriverClient {
d.Lock()
defer d.Unlock()
return d.portDriverClient
}
func parseNetworkGenericOptions(data interface{}) (*networkConfiguration, error) {
var (
err error
@@ -1639,7 +1588,7 @@ func (ep *bridgeEndpoint) trimPortBindings(ctx context.Context, n *bridgeNetwork
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{
"error": err,
"gw4": pbmReq.ipv4,

View File

@@ -8,7 +8,6 @@ import (
"maps"
"net"
"net/netip"
"os/exec"
"slices"
"strconv"
"testing"
@@ -796,21 +795,16 @@ func testQueryEndpointInfo(t *testing.T, ulPxyEnabled bool) {
defer netnsutils.SetupTestOSContext(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()
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{
EnableIPTables: true,
EnableUserlandProxy: ulPxyEnabled,
UserlandProxyPath: proxyBinary,
EnableIPTables: true,
}
genericOption := make(map[string]interface{})
genericOption[netlabel.GenericData] = config
@@ -865,15 +859,15 @@ func testQueryEndpointInfo(t *testing.T, ulPxyEnabled bool) {
if !ok {
t.Fatal("Endpoint operational data does not contain port mapping data")
}
pm, ok := pmd.([]types.PortBinding)
pbs, ok := pmd.([]types.PortBinding)
if !ok {
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")
}
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")
}
}

View File

@@ -8,25 +8,15 @@ import (
"errors"
"fmt"
"net"
"net/netip"
"os"
"slices"
"strconv"
"syscall"
"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/portallocator"
"github.com/docker/docker/daemon/libnetwork/portmapper"
"github.com/docker/docker/daemon/libnetwork/portmapperapi"
"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
// 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
@@ -50,20 +40,23 @@ func (n *bridgeNetwork) addPortMappings(
defHostIP = addr4
}
pms := n.portMappers()
bindings := make([]portmapperapi.PortBinding, 0, len(cfg)*2)
defer func() {
if retErr != nil {
if err := releasePortBindings(bindings, n.firewallerNetwork); err != nil {
log.G(ctx).Warnf("Release port bindings: %s", err.Error())
if err := n.unmapPBs(ctx, bindings); err != nil {
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)
proxyPath := n.userlandProxyPath()
pdc := n.getPortDriverClient()
// 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
// 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
for i, c := range bindingReqs {
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
// collecting bindings, then allocate the same host port for all addresses.
continue
}
var newB []portmapperapi.PortBinding
var err error
if c.DisableNAT {
newB, err = setupForwardedPorts(ctx, toBind, n.firewallerNetwork)
} else {
newB, err = bindHostPorts(ctx, toBind, proxyPath, pdc, n.firewallerNetwork)
}
pm, err := pms.Get(c.Mapper)
if err != nil {
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.
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
}
@@ -283,7 +254,10 @@ func configurePortBindingIPv4(
// Unmap the addresses if they're IPv4-mapped IPv6.
bnd.HostIP = bnd.HostIP.To4()
bnd.IP = containerIPv4.To4()
bnd.DisableNAT = disableNAT
bnd.Mapper = "nat"
if disableNAT {
bnd.Mapper = "routed"
}
return bnd, true
}
@@ -340,236 +314,13 @@ func configurePortBindingIPv6(
}
bnd.IP = containerIP
bnd.DisableNAT = disableNAT
bnd.Mapper = "nat"
if disableNAT {
bnd.Mapper = "routed"
}
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
func (n *bridgeNetwork) releasePorts(ep *bridgeEndpoint) error {
n.Lock()
@@ -578,36 +329,25 @@ func (n *bridgeNetwork) releasePorts(ep *bridgeEndpoint) error {
ep.portBindingState = portBindingMode{}
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
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))
}
for _, b := range bindings {
pm, err := pms.Get(b.Mapper)
if err != nil {
errs = append(errs, fmt.Errorf("unmapping port binding %s: %w", b.PortBinding, err))
continue
}
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 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))
if err := pm.UnmapPorts(ctx, []portmapperapi.PortBinding{b}, n.firewallerNetwork); err != nil {
errs = append(errs, fmt.Errorf("unmapping port binding %s: %w", b.PortBinding, err))
}
}
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
import (
@@ -7,6 +10,7 @@ import (
"net"
"net/netip"
"os"
"slices"
"strconv"
"strings"
"syscall"
@@ -19,7 +23,10 @@ import (
"github.com/docker/docker/daemon/libnetwork/ns"
"github.com/docker/docker/daemon/libnetwork/portallocator"
"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/internal/sliceutil"
"github.com/docker/docker/internal/testutils/netnsutils"
"github.com/docker/docker/internal/testutils/storeutils"
"github.com/sirupsen/logrus"
@@ -32,7 +39,12 @@ func TestPortMappingConfig(t *testing.T) {
defer netnsutils.SetupTestOSContext(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{
EnableIPTables: true,
@@ -61,7 +73,7 @@ func TestPortMappingConfig(t *testing.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 {
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)
}
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{
EnableIPTables: true,
@@ -147,7 +164,7 @@ func TestPortMappingV6Config(t *testing.T) {
ipdList4 := getIPv4Data(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 {
t.Fatalf("Failed to create bridge: %v", err)
}
@@ -196,30 +213,6 @@ func loopbackUp() error {
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 {
t.Helper()
ip, ipNet, err := net.ParseCIDR(cidr)
@@ -242,7 +235,7 @@ func TestAddPortMappings(t *testing.T) {
gwMode6 gwMode
cfg []portmapperapi.PortBindingReq
defHostIP net.IP
proxyPath string
enableProxy bool
hairpin bool
busyPortIPv4 int
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: 80}},
},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
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},
@@ -276,24 +269,24 @@ func TestAddPortMappings(t *testing.T) {
},
},
{
name: "specific host port",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
proxyPath: "/dummy/path/to/proxy",
name: "specific host port",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
enableProxy: true,
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: "nat explicitly enabled",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
gwMode4: gwModeNAT,
gwMode6: gwModeNAT,
proxyPath: "/dummy/path/to/proxy",
name: "nat explicitly enabled",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
gwMode4: gwModeNAT,
gwMode6: gwModeNAT,
enableProxy: true,
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},
@@ -304,27 +297,27 @@ func TestAddPortMappings(t *testing.T) {
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
busyPortIPv4: 8080,
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",
epAddrV4: ctrIP4Mapped,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
proxyPath: "/dummy/path/to/proxy",
name: "ipv4 mapped container address with specific host port",
epAddrV4: ctrIP4Mapped,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080}}},
enableProxy: true,
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: []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",
name: "ipv4 mapped host address with specific host port",
epAddrV4: ctrIP4,
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}}},
enableProxy: true,
expPBs: []types.PortBinding{
{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,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80, HostPort: 8080, HostPortEnd: 8081}}},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
busyPortIPv4: 8080,
expPBs: []types.PortBinding{
{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.IPv6zero, HostPort: 8080, HostPortEnd: 8081}},
},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
busyPortIPv4: 8080,
expPBs: []types.PortBinding{
{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: 82, HostPort: 8080, HostPortEnd: 8083}},
},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
busyPortIPv4: 8082,
expPBs: []types.PortBinding{
{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: 82, HostPort: 8080, HostPortEnd: 8082}},
},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
busyPortIPv4: 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.IPv6zero, Port: 80}},
},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
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},
@@ -417,7 +410,7 @@ func TestAddPortMappings(t *testing.T) {
cfg: []portmapperapi.PortBindingReq{
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
noProxy6To4: true,
expPBs: []types.PortBinding{
{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",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
proxyPath: "/dummy/path/to/proxy",
defHostIP: newIPNet(t, "127.0.0.1/8").IP,
name: "default host ip is nonzero v4",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
enableProxy: true,
defHostIP: newIPNet(t, "127.0.0.1/8").IP,
expPBs: []types.PortBinding{
{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",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
proxyPath: "/dummy/path/to/proxy",
defHostIP: newIPNet(t, "::ffff:127.0.0.1/72").IP,
name: "default host ip is nonzero IPv4-mapped IPv6",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
enableProxy: true,
defHostIP: newIPNet(t, "::ffff:127.0.0.1/72").IP,
expPBs: []types.PortBinding{
{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",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
proxyPath: "/dummy/path/to/proxy",
defHostIP: net.IPv6zero,
name: "default host ip is v6",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
enableProxy: true,
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: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
proxyPath: "/dummy/path/to/proxy",
defHostIP: newIPNet(t, "::1/128").IP,
name: "default host ip is nonzero v6",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
cfg: []portmapperapi.PortBindingReq{{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}},
enableProxy: true,
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},
},
@@ -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: 22, HostPort: 2222}},
},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
expPBs: []types.PortBinding{
{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: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, 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" +
"failed to stop userland proxy for port mapping [::]:2222:[fdf8:b88e:bb5c:3483::2]:22/tcp: 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" +
"failed to stop userland proxy for port mapping [::]:8080:[fdf8:b88e:bb5c:3483::2]:80/tcp: can't stop now",
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" +
"unmapping port binding [::]:2222:[fdf8:b88e:bb5c:3483::2]:22/tcp: failed to stop userland proxy: 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" +
"unmapping port binding [::]:8080:[fdf8:b88e:bb5c:3483::2]:80/tcp: failed to stop userland proxy: can't stop now",
},
{
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: 80}},
},
proxyPath: "/dummy/path/to/proxy",
gwMode6: gwModeRouted,
enableProxy: true,
gwMode6: gwModeRouted,
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},
@@ -524,9 +517,9 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
},
proxyPath: "/dummy/path/to/proxy",
gwMode6: gwModeRouted,
defHostIP: net.IPv6loopback,
enableProxy: true,
gwMode6: gwModeRouted,
defHostIP: net.IPv6loopback,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, 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: 80}},
},
proxyPath: "/dummy/path/to/proxy",
gwMode4: gwModeRouted,
enableProxy: true,
gwMode4: gwModeRouted,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero},
{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: 80}},
},
proxyPath: "/dummy/path/to/proxy",
gwMode4: gwModeRouted,
gwMode6: gwModeRouted,
enableProxy: true,
gwMode4: gwModeRouted,
gwMode6: gwModeRouted,
expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero},
{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"},
},
{
name: "routed mode specific address",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
gwMode4: gwModeRouted,
gwMode6: gwModeRouted,
proxyPath: "/dummy/path/to/proxy",
name: "routed mode specific address",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
gwMode4: gwModeRouted,
gwMode6: gwModeRouted,
enableProxy: true,
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: net.IPv6loopback}},
@@ -607,12 +600,12 @@ func TestAddPortMappings(t *testing.T) {
},
},
{
name: "routed4 nat6 with ipv4 default binding",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
gwMode4: gwModeRouted,
defHostIP: newIPNet(t, "127.0.0.1/8").IP,
proxyPath: "/dummy/path/to/proxy",
name: "routed4 nat6 with ipv4 default binding",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
gwMode4: gwModeRouted,
defHostIP: newIPNet(t, "127.0.0.1/8").IP,
enableProxy: true,
cfg: []portmapperapi.PortBindingReq{
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
},
@@ -621,12 +614,12 @@ func TestAddPortMappings(t *testing.T) {
},
},
{
name: "routed4 nat6 with ipv6 default binding",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
gwMode4: gwModeRouted,
defHostIP: net.IPv6loopback,
proxyPath: "/dummy/path/to/proxy",
name: "routed4 nat6 with ipv6 default binding",
epAddrV4: ctrIP4,
epAddrV6: ctrIP6,
gwMode4: gwModeRouted,
defHostIP: net.IPv6loopback,
enableProxy: true,
cfg: []portmapperapi.PortBindingReq{
{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}},
},
proxyPath: "/dummy/path/to/proxy",
enableProxy: true,
expPBs: []types.PortBinding{
{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},
@@ -693,8 +686,8 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
},
proxyPath: "/dummy/path/to/proxy",
rootless: true,
enableProxy: true,
rootless: true,
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},
@@ -727,17 +720,12 @@ func TestAddPortMappings(t *testing.T) {
useStubFirewaller(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(pb types.PortBinding,
proxyPath string,
listenSock *os.File,
) (stop func() error, retErr error) {
startProxy := func(pb types.PortBinding, listenSock *os.File) (stop func() error, retErr error) {
if tc.busyPortIPv4 > 0 && tc.busyPortIPv4 == int(pb.HostPort) && pb.HostIP.To4() != nil {
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 {
return nil, fmt.Errorf("duplicate proxy: %#v", c)
}
@@ -754,13 +742,6 @@ func TestAddPortMappings(t *testing.T) {
}, 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 {
dummyLink := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "br-dummy"}}
err := netlink.LinkAdd(dummyLink)
@@ -783,6 +764,21 @@ func TestAddPortMappings(t *testing.T) {
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{
config: &networkConfiguration{
BridgeName: "dummybridge",
@@ -792,26 +788,22 @@ func TestAddPortMappings(t *testing.T) {
GwModeIPv6: tc.gwMode6,
},
bridge: &bridgeInterface{},
driver: newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{}),
driver: newDriver(storeutils.NewTempStore(t), pms),
}
genericOption := map[string]interface{}{
netlabel.GenericData: &configuration{
EnableIPTables: true,
EnableIP6Tables: true,
EnableUserlandProxy: tc.proxyPath != "",
UserlandProxyPath: tc.proxyPath,
Hairpin: tc.hairpin,
Rootless: tc.rootless,
EnableIPTables: true,
EnableIP6Tables: true,
Hairpin: tc.hairpin,
},
}
err := n.driver.configure(genericOption)
err = n.driver.configure(genericOption)
assert.NilError(t, err)
fwn, err := n.newFirewallerNetwork(context.Background())
assert.NilError(t, err)
assert.Check(t, fwn != nil, "no firewaller network")
n.firewallerNetwork = fwn
assert.Check(t, is.Equal(n.driver.portDriverClient == nil, !tc.rootless))
expChildIP := func(hostIP net.IP) net.IP {
if !tc.rootless {
return hostIP
@@ -860,7 +852,7 @@ func TestAddPortMappings(t *testing.T) {
assert.Assert(t, is.Len(pbs, len(tc.expPBs)))
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.IPv6)
@@ -903,7 +895,7 @@ func TestAddPortMappings(t *testing.T) {
}
// Check a docker-proxy was started and stopped for each expected port binding.
if tc.proxyPath != "" {
if tc.enableProxy {
expProxies := map[proxyCall]bool{}
for _, expPB := range tc.expPBs {
hip := expChildIP(expPB.HostIP)
@@ -913,7 +905,7 @@ func TestAddPortMappings(t *testing.T) {
}
p := newProxyCall(expPB.Proto.String(),
hip, int(expPB.HostPort),
expPB.IP, int(expPB.Port), tc.proxyPath)
expPB.IP, int(expPB.Port))
expProxies[p] = tc.expReleaseErr != ""
}
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,
// and that they have all been closed.
if n.driver.portDriverClient != nil {
pdc := n.driver.portDriverClient.(*mockPortDriverClient)
if pdc != nil {
pdc := pdc.(*mockPortDriverClient)
expPorts := map[mockPortDriverPort]bool{}
for _, expPB := range tc.expPBs {
if expPB.HostPort == 0 {
@@ -943,18 +935,16 @@ func TestAddPortMappings(t *testing.T) {
}
// 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,
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,
}
}
@@ -975,10 +965,10 @@ type mockPortDriverClient struct {
openPorts map[mockPortDriverPort]bool
}
func newMockPortDriverClient(_ context.Context) (*mockPortDriverClient, error) {
func newMockPortDriverClient() *mockPortDriverClient {
return &mockPortDriverClient{
openPorts: map[mockPortDriverPort]bool{},
}, nil
}
}
func (c *mockPortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr {
@@ -1002,3 +992,33 @@ func (c *mockPortDriverClient) AddPort(_ context.Context, proto string, hostIP,
return 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 (
"context"
"fmt"
"os"
"github.com/docker/docker/daemon/libnetwork/config"
"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/overlay"
"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 {
@@ -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 {
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
}

View File

@@ -5,6 +5,7 @@ import (
"net"
"net/netip"
"os"
"strings"
"github.com/docker/docker/daemon/libnetwork/types"
)
@@ -36,14 +37,12 @@ type PortMapper interface {
type PortBindingReq struct {
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
// seen from the daemon. (It'll be removed once the portmapper API is
// implemented).
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
@@ -58,11 +57,8 @@ type PortBindingReq struct {
// - same host ports or ranges are adjacent, then
// - ordered by container IP (then host IP, if set).
func (pbReq PortBindingReq) Compare(other PortBindingReq) int {
if pbReq.DisableNAT != other.DisableNAT {
if pbReq.DisableNAT {
return 1 // NAT disabled bindings come last
}
return -1
if pbReq.Mapper != other.Mapper {
return strings.Compare(pbReq.Mapper, other.Mapper)
}
// Exact host port < host port range.
aIsRange := pbReq.HostPort == 0 || pbReq.HostPort != pbReq.HostPortEnd
@@ -97,6 +93,8 @@ func (pbReq PortBindingReq) Compare(other PortBindingReq) int {
type PortBinding struct {
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
// 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.

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)"
pbA, pbB = pb, pb
pbB.DisableNAT = true
pbB.Mapper = "routed"
assert.Check(t, pbA.Compare(pbB) < 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
}))
}