mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
daemon: add ULA prefix by default
So far, Moby only had IPv4 prefixes in its 'default-address-pools'. To get dynamic IPv6 subnet allocations, users had to redefine this parameter to include IPv6 base network(s). This is needlessly complex and against Moby's 'batteries-included' principle. This change generates a ULA base network by deriving a ULA Global ID from the Engine's Host ID and put that base network into 'default-address-pools'. This Host ID is stable over time (except if users remove their '/var/lib/docker/engine-id') and thus the GID is stable too. This ULA base network won't be put into 'default-address-pools' if users have manually configured it. This is loosely based on https://datatracker.ietf.org/doc/html/rfc4193#section-3.2.2. Signed-off-by: Albin Kerouanton <albinker@gmail.com>
This commit is contained in:
@@ -10,12 +10,16 @@ package daemon // import "github.com/docker/docker/daemon"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -61,6 +65,7 @@ import (
|
||||
"github.com/docker/docker/libnetwork/cluster"
|
||||
nwconfig "github.com/docker/docker/libnetwork/config"
|
||||
"github.com/docker/docker/libnetwork/ipamutils"
|
||||
"github.com/docker/docker/libnetwork/ipbits"
|
||||
"github.com/docker/docker/pkg/authorization"
|
||||
"github.com/docker/docker/pkg/fileutils"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
@@ -1462,7 +1467,7 @@ func isBridgeNetworkDisabled(conf *config.Config) bool {
|
||||
return conf.BridgeConfig.Iface == config.DisableNetworkBridge
|
||||
}
|
||||
|
||||
func (daemon *Daemon) networkOptions(conf *config.Config, pg plugingetter.PluginGetter, activeSandboxes map[string]interface{}) ([]nwconfig.Option, error) {
|
||||
func (daemon *Daemon) networkOptions(conf *config.Config, pg plugingetter.PluginGetter, hostID string, activeSandboxes map[string]interface{}) ([]nwconfig.Option, error) {
|
||||
dd := runconfig.DefaultDaemonNetworkMode()
|
||||
|
||||
options := []nwconfig.Option{
|
||||
@@ -1479,6 +1484,15 @@ func (daemon *Daemon) networkOptions(conf *config.Config, pg plugingetter.Plugin
|
||||
if len(conf.NetworkConfig.DefaultAddressPools.Value()) > 0 {
|
||||
defaultAddressPools = conf.NetworkConfig.DefaultAddressPools.Value()
|
||||
}
|
||||
// If the Engine admin don't configure default-address-pools or if they
|
||||
// don't provide any IPv6 prefix, we derive a ULA prefix from the daemon's
|
||||
// hostID and add it to the pools. This makes dynamic IPv6 subnet
|
||||
// allocation possible out-of-the-box.
|
||||
if !slices.ContainsFunc(defaultAddressPools, func(nw *ipamutils.NetworkToSplit) bool {
|
||||
return nw.Base.Addr().Is6() && !nw.Base.Addr().Is4In6()
|
||||
}) {
|
||||
defaultAddressPools = append(defaultAddressPools, deriveULABaseNetwork(hostID))
|
||||
}
|
||||
options = append(options, nwconfig.OptionDefaultAddressPoolConfig(defaultAddressPools))
|
||||
|
||||
if conf.LiveRestoreEnabled && len(activeSandboxes) != 0 {
|
||||
@@ -1491,6 +1505,23 @@ func (daemon *Daemon) networkOptions(conf *config.Config, pg plugingetter.Plugin
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// deriveULABaseNetwork derives a Global ID from the provided hostID and
|
||||
// appends it to the ULA prefix (with L bit set) to generate a ULA prefix
|
||||
// unique to this host. The returned ipamutils.NetworkToSplit is stable over
|
||||
// time if hostID doesn't change.
|
||||
//
|
||||
// This is loosely based on the algorithm described in https://datatracker.ietf.org/doc/html/rfc4193#section-3.2.2.
|
||||
func deriveULABaseNetwork(hostID string) *ipamutils.NetworkToSplit {
|
||||
sha := sha256.Sum256([]byte(hostID))
|
||||
gid := binary.BigEndian.Uint64(sha[:]) & (1<<40 - 1) // Keep the 40 least significant bits.
|
||||
addr := ipbits.Add(netip.MustParseAddr("fd00::"), gid, 80)
|
||||
|
||||
return &ipamutils.NetworkToSplit{
|
||||
Base: netip.PrefixFrom(addr, 48),
|
||||
Size: 64,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCluster returns the cluster
|
||||
func (daemon *Daemon) GetCluster() Cluster {
|
||||
return daemon.cluster
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package daemon // import "github.com/docker/docker/daemon"
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -313,3 +314,30 @@ func TestFindNetworkErrorType(t *testing.T) {
|
||||
t.Error("The FindNetwork method MUST always return an error that implements the NotFound interface and is ErrNoSuchNetwork")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeriveULABaseNetwork checks that for a given hostID, the derived prefix is stable over time.
|
||||
func TestDeriveULABaseNetwork(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
hostID string
|
||||
expPrefix netip.Prefix
|
||||
}{
|
||||
{
|
||||
name: "Empty hostID",
|
||||
expPrefix: netip.MustParsePrefix("fd42:98fc:1c14::/48"),
|
||||
},
|
||||
{
|
||||
name: "499d4bc0-b0b3-416f-b1ee-cf6486315593",
|
||||
hostID: "499d4bc0-b0b3-416f-b1ee-cf6486315593",
|
||||
expPrefix: netip.MustParsePrefix("fd62:fb69:18af::/48"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nw := deriveULABaseNetwork(tc.hostID)
|
||||
assert.Equal(t, nw.Base, tc.expPrefix)
|
||||
assert.Equal(t, nw.Size, 64)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,7 +835,7 @@ func configureKernelSecuritySupport(config *config.Config, driverName string) er
|
||||
// network settings. If there's active sandboxes, configuration changes will not
|
||||
// take effect.
|
||||
func (daemon *Daemon) initNetworkController(cfg *config.Config, activeSandboxes map[string]interface{}) error {
|
||||
netOptions, err := daemon.networkOptions(cfg, daemon.PluginStore, activeSandboxes)
|
||||
netOptions, err := daemon.networkOptions(cfg, daemon.PluginStore, daemon.id, activeSandboxes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ func configureMaxThreads(config *config.Config) error {
|
||||
}
|
||||
|
||||
func (daemon *Daemon) initNetworkController(daemonCfg *config.Config, activeSandboxes map[string]interface{}) error {
|
||||
netOptions, err := daemon.networkOptions(daemonCfg, nil, nil)
|
||||
netOptions, err := daemon.networkOptions(daemonCfg, nil, daemon.id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ func TestDaemonReloadNetworkDiagnosticPort(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
netOptions, err := daemon.networkOptions(&config.Config{CommonConfig: config.CommonConfig{Root: t.TempDir()}}, nil, nil)
|
||||
netOptions, err := daemon.networkOptions(&config.Config{CommonConfig: config.CommonConfig{Root: t.TempDir()}}, nil, "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
ctr "github.com/docker/docker/integration/internal/container"
|
||||
@@ -43,3 +45,27 @@ func TestCreateWithMultiNetworks(t *testing.T) {
|
||||
ifacesWithAddress := strings.Count(res.Stdout.String(), "\n")
|
||||
assert.Equal(t, ifacesWithAddress, 3)
|
||||
}
|
||||
|
||||
func TestCreateWithIPv6DefaultsToULAPrefix(t *testing.T) {
|
||||
// On Windows, network creation fails with this error message: Error response from daemon: this request is not supported by the 'windows' ipam driver
|
||||
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
|
||||
|
||||
ctx := setupTest(t)
|
||||
apiClient := testEnv.APIClient()
|
||||
|
||||
const nwName = "testnetula"
|
||||
network.CreateNoError(ctx, t, apiClient, nwName, network.WithIPv6())
|
||||
defer network.RemoveNoError(ctx, t, apiClient, nwName)
|
||||
|
||||
nw, err := apiClient.NetworkInspect(ctx, "testnetula", types.NetworkInspectOptions{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
for _, ipam := range nw.IPAM.Config {
|
||||
ipr := netip.MustParsePrefix(ipam.Subnet)
|
||||
if netip.MustParsePrefix("fd00::/8").Overlaps(ipr) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("Network %s has no ULA prefix, expected one.", nwName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user