Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect IPv6 support in containers, generate '/etc/hosts' accordingly. #47062

Merged
merged 2 commits into from Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 0 additions & 24 deletions daemon/oci_linux.go
Expand Up @@ -23,7 +23,6 @@ import (
"github.com/docker/docker/oci/caps"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/rootless/specconv"
"github.com/docker/docker/pkg/stringid"
volumemounts "github.com/docker/docker/volume/mounts"
"github.com/moby/sys/mount"
"github.com/moby/sys/mountinfo"
Expand Down Expand Up @@ -61,28 +60,6 @@ func withRlimits(daemon *Daemon, daemonCfg *dconfig.Config, c *container.Contain
}
}

// withLibnetwork sets the libnetwork hook
func withLibnetwork(daemon *Daemon, daemonCfg *dconfig.Config, c *container.Container) coci.SpecOpts {
return func(ctx context.Context, _ coci.Client, _ *containers.Container, s *coci.Spec) error {
if c.Config.NetworkDisabled {
return nil
}
for _, ns := range s.Linux.Namespaces {
if ns.Type == specs.NetworkNamespace && ns.Path == "" {
if s.Hooks == nil {
s.Hooks = &specs.Hooks{}
}
shortNetCtlrID := stringid.TruncateID(daemon.netController.ID())
s.Hooks.Prestart = append(s.Hooks.Prestart, specs.Hook{
Path: filepath.Join("/proc", strconv.Itoa(os.Getpid()), "exe"),
Args: []string{"libnetwork-setkey", "-exec-root=" + daemonCfg.GetExecRoot(), c.ID, shortNetCtlrID},
})
}
}
return nil
}
}

// withRootless sets the spec to the rootless configuration
func withRootless(daemon *Daemon, daemonCfg *dconfig.Config) coci.SpecOpts {
return func(_ context.Context, _ coci.Client, _ *containers.Container, s *coci.Spec) error {
Expand Down Expand Up @@ -1070,7 +1047,6 @@ func (daemon *Daemon) createSpec(ctx context.Context, daemonCfg *configStore, c
WithCapabilities(c),
WithSeccomp(daemon, c),
withMounts(daemon, daemonCfg, c, mounts),
withLibnetwork(daemon, &daemonCfg.Config, c),
WithApparmor(c),
WithSelinux(c),
WithOOMScore(&c.HostConfig.OomScoreAdj),
Expand Down
4 changes: 4 additions & 0 deletions daemon/start.go
Expand Up @@ -236,6 +236,10 @@ func (daemon *Daemon) containerStart(ctx context.Context, daemonCfg *configStore
}
}()

if err := daemon.initializeCreatedTask(ctx, tsk, container, spec); err != nil {
return err
}

if err := tsk.Start(context.TODO()); err != nil { // passing ctx caused integration tests to be stuck in the cleanup phase
return setExitCodeFromError(container.SetExitCode, err)
}
Expand Down
31 changes: 31 additions & 0 deletions daemon/start_linux.go
@@ -0,0 +1,31 @@
package daemon // import "github.com/docker/docker/daemon"

import (
"context"
"fmt"

specs "github.com/opencontainers/runtime-spec/specs-go"

"github.com/docker/docker/container"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/libcontainerd/types"
"github.com/docker/docker/oci"
)

// initializeCreatedTask performs any initialization that needs to be done to
// prepare a freshly-created task to be started.
func (daemon *Daemon) initializeCreatedTask(ctx context.Context, tsk types.Task, container *container.Container, spec *specs.Spec) error {
if !container.Config.NetworkDisabled {
nspath, ok := oci.NamespacePath(spec, specs.NetworkNamespace)
if ok && nspath == "" { // the runtime has been instructed to create a new network namespace for tsk.
sb, err := daemon.netController.GetSandbox(container.ID)
if err != nil {
return errdefs.System(err)
}
if err := sb.SetKey(fmt.Sprintf("/proc/%d/ns/net", tsk.Pid())); err != nil {
return errdefs.System(err)
}
}
}
return nil
}
17 changes: 17 additions & 0 deletions daemon/start_notlinux.go
@@ -0,0 +1,17 @@
//go:build !linux

package daemon // import "github.com/docker/docker/daemon"

import (
"context"

"github.com/docker/docker/container"
"github.com/docker/docker/libcontainerd/types"
specs "github.com/opencontainers/runtime-spec/specs-go"
)

// initializeCreatedTask performs any initialization that needs to be done to
// prepare a freshly-created task to be started.
func (daemon *Daemon) initializeCreatedTask(ctx context.Context, tsk types.Task, container *container.Container, spec *specs.Spec) error {
return nil
}
8 changes: 8 additions & 0 deletions integration/internal/container/ops.go
@@ -1,6 +1,7 @@
package container

import (
"maps"
"strings"

"github.com/docker/docker/api/types/container"
Expand Down Expand Up @@ -46,6 +47,13 @@ func WithNetworkMode(mode string) func(*TestContainerConfig) {
}
}

// WithSysctls sets sysctl options for the container
func WithSysctls(sysctls map[string]string) func(*TestContainerConfig) {
return func(c *TestContainerConfig) {
c.HostConfig.Sysctls = maps.Clone(sysctls)
robmry marked this conversation as resolved.
Show resolved Hide resolved
}
}

// WithExposedPorts sets the exposed ports of the container
func WithExposedPorts(ports ...string) func(*TestContainerConfig) {
return func(c *TestContainerConfig) {
Expand Down
107 changes: 107 additions & 0 deletions integration/networking/etchosts_test.go
@@ -0,0 +1,107 @@
package networking

import (
"context"
"testing"
"time"

containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/testutil"
"github.com/docker/docker/testutil/daemon"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)

// Check that the '/etc/hosts' file in a container is created according to
// whether the container supports IPv6.
// Regression test for https://github.com/moby/moby/issues/35954
func TestEtcHostsIpv6(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType == "windows")

ctx := setupTest(t)
d := daemon.New(t)
d.StartWithBusybox(ctx, t,
"--ipv6",
"--ip6tables",
"--experimental",
"--fixed-cidr-v6=fdc8:ffe2:d8d7:1234::/64")
defer d.Stop(t)

c := d.NewClientT(t)
defer c.Close()

testcases := []struct {
name string
sysctls map[string]string
expIPv6Enabled bool
expEtcHosts string
}{
{
// Create a container with no overrides, on the IPv6-enabled default bridge.
// Expect the container to have a working '::1' address, on the assumption
// the test host's kernel supports IPv6 - and for its '/etc/hosts' file to
// include IPv6 addresses.
name: "IPv6 enabled",
expIPv6Enabled: true,
expEtcHosts: `127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
`,
},
{
// Create a container in the same network, with IPv6 disabled. Expect '::1'
// not to be pingable, and no IPv6 addresses in its '/etc/hosts'.
name: "IPv6 disabled",
sysctls: map[string]string{"net.ipv6.conf.all.disable_ipv6": "1"},
expIPv6Enabled: false,
expEtcHosts: "127.0.0.1\tlocalhost\n",
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
ctrId := container.Run(ctx, t, c,
container.WithName("etchosts_"+sanitizeCtrName(t.Name())),
container.WithImage("busybox:latest"),
container.WithCmd("top"),
container.WithSysctls(tc.sysctls),
)
defer func() {
c.ContainerRemove(ctx, ctrId, containertypes.RemoveOptions{Force: true})
}()

runCmd := func(ctrId string, cmd []string, expExitCode int) string {
t.Helper()
execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
res, err := container.Exec(execCtx, c, ctrId, cmd)
assert.Check(t, is.Nil(err))
assert.Check(t, is.Equal(res.ExitCode, expExitCode))
return res.Stdout()
}

// Check that IPv6 is/isn't enabled, as expected.
var expPingExitStatus int
if !tc.expIPv6Enabled {
expPingExitStatus = 1
}
runCmd(ctrId, []string{"ping", "-6", "-c1", "-W3", "::1"}, expPingExitStatus)

// Check the contents of /etc/hosts.
stdout := runCmd(ctrId, []string{"cat", "/etc/hosts"}, 0)
// Append the container's own addresses/name to the expected hosts file content.
inspect := container.Inspect(ctx, t, c, ctrId)
exp := tc.expEtcHosts + inspect.NetworkSettings.IPAddress + "\t" + inspect.Config.Hostname + "\n"
if tc.expIPv6Enabled {
exp += inspect.NetworkSettings.GlobalIPv6Address + "\t" + inspect.Config.Hostname + "\n"
}
assert.Check(t, is.Equal(stdout, exp))
})
}
}
27 changes: 2 additions & 25 deletions libnetwork/drivers/bridge/port_mapping_linux.go
Expand Up @@ -6,9 +6,9 @@ import (
"errors"
"fmt"
"net"
"sync"

"github.com/containerd/log"
"github.com/docker/docker/libnetwork/netutils"
"github.com/docker/docker/libnetwork/types"
"github.com/ishidawataru/sctp"
)
Expand Down Expand Up @@ -55,7 +55,7 @@ func (n *bridgeNetwork) allocatePortsInternal(bindings []types.PortBinding, cont
// skip adding implicit v6 addr, when the kernel was booted with `ipv6.disable=1`
// https://github.com/moby/moby/issues/42288
isV6Binding := c.HostIP != nil && c.HostIP.To4() == nil
if !isV6Binding && !IsV6Listenable() {
if !isV6Binding && !netutils.IsV6Listenable() {
continue
}

Expand Down Expand Up @@ -219,26 +219,3 @@ func (n *bridgeNetwork) releasePort(bnd types.PortBinding) error {

return portmapper.Unmap(host)
}

var (
v6ListenableCached bool
v6ListenableOnce sync.Once
)

// IsV6Listenable returns true when `[::1]:0` is listenable.
// IsV6Listenable returns false mostly when the kernel was booted with `ipv6.disable=1` option.
func IsV6Listenable() bool {
v6ListenableOnce.Do(func() {
ln, err := net.Listen("tcp6", "[::1]:0")
if err != nil {
// When the kernel was booted with `ipv6.disable=1`,
// we get err "listen tcp6 [::1]:0: socket: address family not supported by protocol"
// https://github.com/moby/moby/issues/42288
log.G(context.TODO()).Debugf("port_mapping: v6Listenable=false (%v)", err)
} else {
v6ListenableCached = true
ln.Close()
}
})
return v6ListenableCached
}
38 changes: 13 additions & 25 deletions libnetwork/endpoint.go
Expand Up @@ -478,18 +478,8 @@ func (ep *Endpoint) sbJoin(sb *Sandbox, options ...EndpointOption) (err error) {
}
}

// Do not update hosts file with internal networks endpoint IP
if !n.ingress && n.Name() != libnGWNetwork {
var addresses []string
if ip := ep.getFirstInterfaceIPv4Address(); ip != nil {
addresses = append(addresses, ip.String())
}
if ip := ep.getFirstInterfaceIPv6Address(); ip != nil {
addresses = append(addresses, ip.String())
}
if err = sb.updateHostsFile(addresses); err != nil {
return err
}
if err := sb.updateHostsFile(ep.getEtcHostsAddrs()); err != nil {
return err
}
if err = sb.updateDNS(n.enableIPv6); err != nil {
return err
Expand Down Expand Up @@ -860,26 +850,24 @@ func (ep *Endpoint) getSandbox() (*Sandbox, bool) {
return ps, ok
}

func (ep *Endpoint) getFirstInterfaceIPv4Address() net.IP {
// Return a list of this endpoint's addresses to add to '/etc/hosts'.
func (ep *Endpoint) getEtcHostsAddrs() []string {
ep.mu.Lock()
defer ep.mu.Unlock()

if ep.iface.addr != nil {
return ep.iface.addr.IP
// Do not update hosts file with internal network's endpoint IP
if n := ep.network; n == nil || n.ingress || n.Name() == libnGWNetwork {
return nil
}

return nil
}

func (ep *Endpoint) getFirstInterfaceIPv6Address() net.IP {
ep.mu.Lock()
defer ep.mu.Unlock()

var addresses []string
if ep.iface.addr != nil {
addresses = append(addresses, ep.iface.addr.IP.String())
}
if ep.iface.addrv6 != nil {
return ep.iface.addrv6.IP
addresses = append(addresses, ep.iface.addrv6.IP.String())
}

return nil
return addresses
}

// EndpointOptionGeneric function returns an option setter for a Generic option defined
Expand Down