diff --git a/daemon/libnetwork/controller.go b/daemon/libnetwork/controller.go index 5c30f06080..39e58c090d 100644 --- a/daemon/libnetwork/controller.go +++ b/daemon/libnetwork/controller.go @@ -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 } diff --git a/daemon/libnetwork/drivers/bridge/bridge_linux.go b/daemon/libnetwork/drivers/bridge/bridge_linux.go index 545a73fb8a..ea58536670 100644 --- a/daemon/libnetwork/drivers/bridge/bridge_linux.go +++ b/daemon/libnetwork/drivers/bridge/bridge_linux.go @@ -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 } diff --git a/daemon/libnetwork/drivers/bridge/bridge_linux_test.go b/daemon/libnetwork/drivers/bridge/bridge_linux_test.go index 7a866e18fe..b7640e8378 100644 --- a/daemon/libnetwork/drivers/bridge/bridge_linux_test.go +++ b/daemon/libnetwork/drivers/bridge/bridge_linux_test.go @@ -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, diff --git a/daemon/libnetwork/drivers/bridge/network_linux_test.go b/daemon/libnetwork/drivers/bridge/network_linux_test.go index 8d86f48a5a..02cb033ea3 100644 --- a/daemon/libnetwork/drivers/bridge/network_linux_test.go +++ b/daemon/libnetwork/drivers/bridge/network_linux_test.go @@ -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) diff --git a/daemon/libnetwork/drivers/bridge/port_mapping_linux_test.go b/daemon/libnetwork/drivers/bridge/port_mapping_linux_test.go index 8348e3aee8..bf0d1133e7 100644 --- a/daemon/libnetwork/drivers/bridge/port_mapping_linux_test.go +++ b/daemon/libnetwork/drivers/bridge/port_mapping_linux_test.go @@ -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{ diff --git a/daemon/libnetwork/drivers_linux.go b/daemon/libnetwork/drivers_linux.go index 356577f59c..14af028864 100644 --- a/daemon/libnetwork/drivers_linux.go +++ b/daemon/libnetwork/drivers_linux.go @@ -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 +} diff --git a/daemon/libnetwork/drivers_windows.go b/daemon/libnetwork/drivers_windows.go index fcbc6922ca..4893b3e431 100644 --- a/daemon/libnetwork/drivers_windows.go +++ b/daemon/libnetwork/drivers_windows.go @@ -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 +} diff --git a/daemon/libnetwork/drvregistry/portmappers.go b/daemon/libnetwork/drvregistry/portmappers.go new file mode 100644 index 0000000000..234e681227 --- /dev/null +++ b/daemon/libnetwork/drvregistry/portmappers.go @@ -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 +} diff --git a/daemon/libnetwork/drvregistry/portmappers_test.go b/daemon/libnetwork/drvregistry/portmappers_test.go new file mode 100644 index 0000000000..b3490821ef --- /dev/null +++ b/daemon/libnetwork/drvregistry/portmappers_test.go @@ -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") + }) +} diff --git a/daemon/libnetwork/portmapperapi/api.go b/daemon/libnetwork/portmapperapi/api.go index df2f8191cb..8de53d5649 100644 --- a/daemon/libnetwork/portmapperapi/api.go +++ b/daemon/libnetwork/portmapperapi/api.go @@ -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 diff --git a/daemon/libnetwork/portmapperapi/firewaller.go b/daemon/libnetwork/portmapperapi/firewaller.go new file mode 100644 index 0000000000..ebb521f53d --- /dev/null +++ b/daemon/libnetwork/portmapperapi/firewaller.go @@ -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 +}