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:
Albin Kerouanton
2024-05-13 12:35:02 +02:00
parent 32418e9753
commit d18b88fd32
6 changed files with 89 additions and 4 deletions

View File

@@ -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

View File

@@ -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)
})
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}