Skip to content

Commit

Permalink
daemon: add ULA prefix by default
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
akerouanton committed May 29, 2024
1 parent 32418e9 commit c395a33
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 4 deletions.
33 changes: 32 additions & 1 deletion daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand All @@ -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 contains := slices.ContainsFunc(defaultAddressPools, func(nw *ipamutils.NetworkToSplit) bool {
return nw.Base.Addr().Is6() && !nw.Base.Addr().Is4In6()
}); !contains {
defaultAddressPools = append(defaultAddressPools, deriveULABaseNetwork(hostID))
}
options = append(options, nwconfig.OptionDefaultAddressPoolConfig(defaultAddressPools))

if conf.LiveRestoreEnabled && len(activeSandboxes) != 0 {
Expand All @@ -1491,6 +1505,23 @@ func (daemon *Daemon) networkOptions(conf *config.Config, pg plugingetter.Plugin
return options, nil
}

// deriveULABaseNetwork derives a Global ID based on 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
Expand Down
28 changes: 28 additions & 0 deletions daemon/daemon_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package daemon // import "github.com/docker/docker/daemon"

import (
"net/netip"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -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)
})
}
}
2 changes: 1 addition & 1 deletion daemon/daemon_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion daemon/daemon_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion daemon/reload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
26 changes: 26 additions & 0 deletions integration/network/bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions libnetwork/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Config struct {
Scope datastore.ScopeCfg
ActiveSandboxes map[string]interface{}
PluginGetter plugingetter.PluginGetter
HostID string // HostID is a unique and stable ID associated with this host.
}

// New creates a new Config and initializes it with the given Options.
Expand Down

0 comments on commit c395a33

Please sign in to comment.