libnet/portmapperapi: add PortMapper interface, registry

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
This commit is contained in:
Albin Kerouanton
2025-06-27 19:37:55 +02:00
parent afb231d027
commit 41cd92f6c0
11 changed files with 207 additions and 29 deletions

View File

@@ -86,6 +86,7 @@ type Controller struct {
id string
drvRegistry drvregistry.Networks
ipamRegistry drvregistry.IPAMs
pmRegistry drvregistry.PortMappers
sandboxes map[string]*Sandbox
cfg *config.Config
store *datastore.Store
@@ -173,13 +174,20 @@ func New(ctx context.Context, cfgOptions ...config.Option) (_ *Controller, retEr
}
c.drvRegistry.Notify = c
// Register portmappers before network drivers to make sure they can
// restore existing sandboxes (with port mappings) during their
// initialization, if the daemon is started in live restore mode.
if err := registerPortMappers(ctx, &c.pmRegistry, c.cfg); err != nil {
return nil, err
}
// External plugins don't need config passed through daemon. They can
// bootstrap themselves.
if err := remotedriver.Register(&c.drvRegistry, c.cfg.PluginGetter); err != nil {
return nil, err
}
if err := registerNetworkDrivers(&c.drvRegistry, c.store, c.makeDriverConfig); err != nil {
if err := registerNetworkDrivers(&c.drvRegistry, c.store, &c.pmRegistry, c.makeDriverConfig); err != nil {
return nil, err
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/docker/docker/daemon/libnetwork/drivers/bridge/internal/iptabler"
"github.com/docker/docker/daemon/libnetwork/drivers/bridge/internal/nftabler"
"github.com/docker/docker/daemon/libnetwork/drivers/bridge/internal/rlkclient"
"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/iptables"
@@ -178,6 +179,7 @@ type driver struct {
portDriverClient portDriverClient
configNetwork sync.Mutex
firewaller firewaller.Firewaller
portmappers *drvregistry.PortMappers
sync.Mutex
}
@@ -192,17 +194,18 @@ const (
)
// New constructs a new bridge driver
func newDriver(store *datastore.Store) *driver {
func newDriver(store *datastore.Store, pms *drvregistry.PortMappers) *driver {
return &driver{
store: store,
nlh: ns.NlHandle(),
networks: map[string]*bridgeNetwork{},
store: store,
nlh: ns.NlHandle(),
networks: map[string]*bridgeNetwork{},
portmappers: pms,
}
}
// Register registers a new instance of bridge driver.
func Register(r driverapi.Registerer, store *datastore.Store, config map[string]interface{}) error {
d := newDriver(store)
func Register(r driverapi.Registerer, store *datastore.Store, pms *drvregistry.PortMappers, config map[string]interface{}) error {
d := newDriver(store, pms)
if err := d.configure(config); err != nil {
return err
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/docker/docker/daemon/libnetwork/driverapi"
"github.com/docker/docker/daemon/libnetwork/drivers/bridge/internal/firewaller"
"github.com/docker/docker/daemon/libnetwork/drvregistry"
"github.com/docker/docker/daemon/libnetwork/internal/netiputil"
"github.com/docker/docker/daemon/libnetwork/ipamapi"
"github.com/docker/docker/daemon/libnetwork/ipams/defaultipam"
@@ -280,7 +281,7 @@ func getIPv6Data(t *testing.T) []driverapi.IPAMData {
func TestCreateFullOptions(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
config := &configuration{
EnableIPForwarding: true,
@@ -335,7 +336,7 @@ func TestCreateFullOptions(t *testing.T) {
func TestCreateNoConfig(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
err := d.configure(nil)
assert.NilError(t, err)
@@ -350,7 +351,7 @@ func TestCreateNoConfig(t *testing.T) {
func TestCreateFullOptionsLabels(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
config := &configuration{
EnableIPForwarding: true,
@@ -537,7 +538,7 @@ func TestCreateVeth(t *testing.T) {
func TestCreate(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
if err := d.configure(nil); err != nil {
t.Fatalf("Failed to setup driver config: %v", err)
@@ -563,7 +564,7 @@ func TestCreate(t *testing.T) {
func TestCreateFail(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
if err := d.configure(nil); err != nil {
t.Fatalf("Failed to setup driver config: %v", err)
@@ -582,7 +583,7 @@ func TestCreateMultipleNetworks(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
useStubFirewaller(t)
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
checkFirewallerNetworks := func() {
t.Helper()
@@ -795,7 +796,7 @@ func testQueryEndpointInfo(t *testing.T, ulPxyEnabled bool) {
defer netnsutils.SetupTestOSContext(t)()
useStubFirewaller(t)
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
portallocator.Get().ReleaseAll()
var proxyBinary string
@@ -909,7 +910,7 @@ func TestLinkContainers(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
useStubFirewaller(t)
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
config := &configuration{
EnableIPTables: true,
@@ -1178,7 +1179,7 @@ func TestValidateFixedCIDRV6(t *testing.T) {
func TestSetDefaultGw(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
if err := d.configure(nil); err != nil {
t.Fatalf("Failed to setup driver config: %v", err)
@@ -1228,7 +1229,7 @@ func TestSetDefaultGw(t *testing.T) {
func TestCreateWithExistingBridge(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
if err := d.configure(nil); err != nil {
t.Fatalf("Failed to setup driver config: %v", err)
@@ -1300,7 +1301,7 @@ func TestCreateParallel(t *testing.T) {
c := netnsutils.SetupTestOSContextEx(t)
defer c.Cleanup(t)
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
portallocator.Get().ReleaseAll()
if err := d.configure(nil); err != nil {
@@ -1351,7 +1352,7 @@ func useStubFirewaller(t *testing.T) {
// Regression test for https://github.com/moby/moby/issues/46445
func TestSetupIP6TablesWithHostIPv4(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
dc := &configuration{
EnableIPTables: true,
EnableIP6Tables: true,

View File

@@ -5,6 +5,7 @@ import (
"testing"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/docker/daemon/libnetwork/drvregistry"
"github.com/docker/docker/daemon/libnetwork/netlabel"
"github.com/docker/docker/internal/nlwrap"
"github.com/docker/docker/internal/testutils/netnsutils"
@@ -15,7 +16,7 @@ import (
func TestLinkCreate(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
err := d.configure(nil)
assert.NilError(t, err)
@@ -78,7 +79,7 @@ func TestLinkCreate(t *testing.T) {
func TestLinkCreateTwo(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
err := d.configure(nil)
assert.NilError(t, err)
@@ -106,7 +107,7 @@ func TestLinkCreateTwo(t *testing.T) {
func TestLinkCreateNoEnableIPv6(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
err := d.configure(nil)
assert.NilError(t, err)
@@ -131,7 +132,7 @@ func TestLinkCreateNoEnableIPv6(t *testing.T) {
func TestLinkDelete(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
err := d.configure(nil)
assert.NilError(t, err)

View File

@@ -14,6 +14,7 @@ import (
"github.com/containerd/log"
"github.com/docker/docker/daemon/libnetwork/drivers/bridge/internal/firewaller"
"github.com/docker/docker/daemon/libnetwork/drvregistry"
"github.com/docker/docker/daemon/libnetwork/netlabel"
"github.com/docker/docker/daemon/libnetwork/ns"
"github.com/docker/docker/daemon/libnetwork/portallocator"
@@ -31,7 +32,7 @@ func TestPortMappingConfig(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
useStubFirewaller(t)
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
config := &configuration{
EnableIPTables: true,
@@ -116,7 +117,7 @@ func TestPortMappingV6Config(t *testing.T) {
t.Fatalf("Could not bring loopback iface up: %v", err)
}
d := newDriver(storeutils.NewTempStore(t))
d := newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{})
config := &configuration{
EnableIPTables: true,
@@ -791,7 +792,7 @@ func TestAddPortMappings(t *testing.T) {
GwModeIPv6: tc.gwMode6,
},
bridge: &bridgeInterface{},
driver: newDriver(storeutils.NewTempStore(t)),
driver: newDriver(storeutils.NewTempStore(t), &drvregistry.PortMappers{}),
}
genericOption := map[string]interface{}{
netlabel.GenericData: &configuration{

View File

@@ -1,8 +1,10 @@
package libnetwork
import (
"context"
"fmt"
"github.com/docker/docker/daemon/libnetwork/config"
"github.com/docker/docker/daemon/libnetwork/datastore"
"github.com/docker/docker/daemon/libnetwork/driverapi"
"github.com/docker/docker/daemon/libnetwork/drivers/bridge"
@@ -11,14 +13,17 @@ import (
"github.com/docker/docker/daemon/libnetwork/drivers/macvlan"
"github.com/docker/docker/daemon/libnetwork/drivers/null"
"github.com/docker/docker/daemon/libnetwork/drivers/overlay"
"github.com/docker/docker/daemon/libnetwork/drvregistry"
)
func registerNetworkDrivers(r driverapi.Registerer, store *datastore.Store, 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 {
for _, nr := range []struct {
ntype string
register func(driverapi.Registerer, *datastore.Store, map[string]interface{}) error
}{
{ntype: bridge.NetworkType, register: bridge.Register},
{ntype: bridge.NetworkType, register: func(r driverapi.Registerer, store *datastore.Store, cfg map[string]interface{}) error {
return bridge.Register(r, store, pms, cfg)
}},
{ntype: host.NetworkType, register: func(r driverapi.Registerer, _ *datastore.Store, _ map[string]interface{}) error {
return host.Register(r)
}},
@@ -38,3 +43,7 @@ func registerNetworkDrivers(r driverapi.Registerer, store *datastore.Store, driv
return nil
}
func registerPortMappers(ctx context.Context, r *drvregistry.PortMappers, cfg *config.Config) error {
return nil
}

View File

@@ -1,16 +1,19 @@
package libnetwork
import (
"context"
"fmt"
"github.com/docker/docker/daemon/libnetwork/config"
"github.com/docker/docker/daemon/libnetwork/datastore"
"github.com/docker/docker/daemon/libnetwork/driverapi"
"github.com/docker/docker/daemon/libnetwork/drivers/null"
"github.com/docker/docker/daemon/libnetwork/drivers/windows"
"github.com/docker/docker/daemon/libnetwork/drivers/windows/overlay"
"github.com/docker/docker/daemon/libnetwork/drvregistry"
)
func registerNetworkDrivers(r driverapi.Registerer, store *datastore.Store, _ func(string) map[string]interface{}) error {
func registerNetworkDrivers(r driverapi.Registerer, store *datastore.Store, _ *drvregistry.PortMappers, _ func(string) map[string]interface{}) error {
for _, nr := range []struct {
ntype string
register func(driverapi.Registerer) error
@@ -25,3 +28,7 @@ func registerNetworkDrivers(r driverapi.Registerer, store *datastore.Store, _ fu
return windows.RegisterBuiltinLocalDrivers(r, store)
}
func registerPortMappers(ctx context.Context, r *drvregistry.PortMappers, cfg *config.Config) error {
return nil
}

View File

@@ -0,0 +1,41 @@
package drvregistry
import (
"errors"
"fmt"
"strings"
"github.com/docker/docker/daemon/libnetwork/portmapperapi"
)
type PortMappers struct {
drivers map[string]portmapperapi.PortMapper
}
// Register a portmapper with the registry.
func (r *PortMappers) Register(name string, pm portmapperapi.PortMapper) error {
if strings.TrimSpace(name) == "" {
return errors.New("portmapper name cannot be empty")
}
if _, ok := r.drivers[name]; ok {
return errors.New("portmapper already registered")
}
if r.drivers == nil {
r.drivers = make(map[string]portmapperapi.PortMapper)
}
r.drivers[name] = pm
return nil
}
// Get retrieves a portmapper by name from the registry.
func (r *PortMappers) Get(name string) (portmapperapi.PortMapper, error) {
pm, ok := r.drivers[name]
if !ok {
return nil, fmt.Errorf("portmapper %s not found", name)
}
return pm, nil
}

View File

@@ -0,0 +1,67 @@
package drvregistry
import (
"context"
"testing"
"github.com/docker/docker/daemon/libnetwork/portmapperapi"
"gotest.tools/v3/assert"
)
type fakePortMapper struct{}
func (f fakePortMapper) MapPorts(_ context.Context, _ []portmapperapi.PortBindingReq, _ portmapperapi.Firewaller) ([]portmapperapi.PortBinding, error) {
return nil, nil
}
func (f fakePortMapper) UnmapPorts(_ context.Context, _ []portmapperapi.PortBinding, _ portmapperapi.Firewaller) error {
return nil
}
func TestRegisterPortMappers(t *testing.T) {
t.Run("register port mapper", func(t *testing.T) {
var pms PortMappers
pm := fakePortMapper{}
err := pms.Register("test", pm)
assert.NilError(t, err)
})
t.Run("empty name", func(t *testing.T) {
var pms PortMappers
err := pms.Register("", nil)
assert.ErrorContains(t, err, "portmapper name cannot be empty")
})
t.Run("duplicate port mapper", func(t *testing.T) {
var pms PortMappers
err := pms.Register("test", nil)
assert.NilError(t, err)
err = pms.Register("test", nil)
assert.ErrorContains(t, err, "portmapper already registered")
})
}
func TestGetPortMapper(t *testing.T) {
t.Run("get existing port mapper", func(t *testing.T) {
var pms PortMappers
pm := fakePortMapper{}
err := pms.Register("test", pm)
assert.NilError(t, err)
retrieved, err := pms.Get("test")
assert.NilError(t, err)
assert.Equal(t, retrieved, pm)
})
t.Run("get nonexistent port mapper", func(t *testing.T) {
var pms PortMappers
_, err := pms.Get("nonexistent")
assert.ErrorContains(t, err, "portmapper nonexistent not found")
})
}

View File

@@ -1,6 +1,7 @@
package portmapperapi
import (
"context"
"net"
"net/netip"
"os"
@@ -8,6 +9,31 @@ import (
"github.com/docker/docker/daemon/libnetwork/types"
)
// Registerer provides a callback interface for registering port-mappers.
type Registerer interface {
// Register provides a way for port-mappers to dynamically register with libnetwork.
Register(name string, driver PortMapper) error
}
// PortMapper maps / unmaps container ports to host ports.
type PortMapper interface {
// MapPorts takes a list of port binding requests, and returns a list of
// PortBinding. Both lists MUST have the same size.
//
// Multiple port bindings are passed when they're all requesting the
// same port range, or an ephemeral port, over multiple IP addresses and
// all pointing to the same container port. In that case, the PortMapper
// MUST assign the same HostPort for all IP addresses.
//
// When an ephemeral port, or a single port from a range is requested
// MapPorts should attempt a few times to find a free port available
// across all IP addresses.
MapPorts(ctx context.Context, reqs []PortBindingReq, fwn Firewaller) ([]PortBinding, error)
// UnmapPorts takes a list of port bindings to unmap.
UnmapPorts(ctx context.Context, pbs []PortBinding, fwn Firewaller) error
}
type PortBindingReq struct {
types.PortBinding
// ChildHostIP is a temporary field used to pass the host IP address as

View File

@@ -0,0 +1,14 @@
package portmapperapi
import (
"context"
"github.com/docker/docker/daemon/libnetwork/types"
)
type Firewaller interface {
// AddPorts adds the configuration needed for NATing ports.
AddPorts(ctx context.Context, pbs []types.PortBinding) error
// DelPorts deletes the configuration needed for NATing ports.
DelPorts(ctx context.Context, pbs []types.PortBinding) error
}