rootless: support detach-netns mode

Now `dockerd-rootless.sh` launches RootlessKit with `--detach-netns`
so as to run the daemon in the host network namespace.

The libnetwork namespaces are allocated inside the "detached" netns
(`$ROOTLESSKIT_STATE_DIR/netns`) that is associated with slirp4netns,
vpnkit, pasta, etc., as the rootless daemon has no `CAP_NET_ADMIN` for
the host network namespace.

This will enable:
- Accelerated (and deflaked) `docker pull`, `docker push`, `docker build`, etc
- Proper support for `docker pull 127.0.0.1:.../...`
- Proper support for `dockern run --net=host`

See also:
- rootless-containers/rootlesskit PR 379
- containerd/nerdctl PR 2723

NOTE: libnetwork contains code generated by Claude Code

Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
This commit is contained in:
Akihiro Suda committed 2026-04-10 02:02:51 +09:00
1 parent 994a849220
commit 84aedb8055
20 files changed
+518 -41

No files matched your search

+6 -1
View File
@@ -313,7 +313,12 @@ cmd_entrypoint_check() {
cmd_entrypoint_nsenter() {
# No need to call init()
pid=$(cat "$XDG_RUNTIME_DIR/dockerd-rootless/child_pid")
exec nsenter --no-fork --wd="$(pwd)" --preserve-credentials -m -n -U -t "$pid" -- "$@"
n=""
# If RootlessKit is running with `--detach-netns` mode, we do NOT enter the detached netns here
if [ ! -e "$XDG_RUNTIME_DIR/dockerd-rootless/netns" ]; then
n="-n"
fi
exec nsenter --no-fork --wd="$(pwd)" --preserve-credentials -m $n -U -t "$pid" -- "$@"
}
show_systemd_error() {
+26 -2
View File
@@ -23,6 +23,9 @@
# * Defaults to "auto".
# * DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=(true|false): prohibit connections to 127.0.0.1 on the host (including via 10.0.2.2, in the case of slirp4netns).
# * Defaults to "true".
# * DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=(true|false): whether to launch rootlesskit with the "detach-netns" mode.
# The "detached-netns" mode accelerates `docker (pull|push|build)` and enables `docker run --net=host`
# Defaults to "true". Set this to false only when facing a compatibility issue.
# To apply an environment variable via systemd, create ~/.config/systemd/user/docker.service.d/override.conf as follows,
# and run `systemctl --user daemon-reload && systemctl --user restart docker`:
@@ -111,6 +114,7 @@ fi
: "${DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX:=auto}"
: "${DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP:=auto}"
: "${DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK:=}"
: "${DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS:=true}"
net=$DOCKERD_ROOTLESS_ROOTLESSKIT_NET
port_driver=$DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER
mtu=$DOCKERD_ROOTLESS_ROOTLESSKIT_MTU
@@ -177,6 +181,20 @@ if [ -z "$_DOCKERD_ROOTLESS_CHILD" ]; then
_DOCKERD_ROOTLESS_SELINUX=1
export _DOCKERD_ROOTLESS_SELINUX
fi
case "$DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS" in
1 | true)
DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="--detach-netns $DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS"
;;
0 | false)
# NOP
;;
*)
echo "Unknown DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS value: $DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS"
exit 1
;;
esac
# Re-exec the script via RootlessKit, so as to create unprivileged {user,mount,network} namespaces.
#
# --copy-up allows removing/creating files in the directories by creating tmpfs and symlinks
@@ -214,13 +232,19 @@ else
mount_directory /etc/ssl "--rbind"
fi
netns="/proc/self/ns/net"
case "$DOCKERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS" in
1 | true)
netns="$ROOTLESSKIT_STATE_DIR/netns"
;;
esac
# When running with --firewall-backend=nftables, IP forwarding needs to be enabled
# because the daemon won't enable it. IP forwarding is harmless in the rootless
# netns, there's only a single external interface and only Docker uses the netns.
# So, always enable IPv4 and IPv6 forwarding. But ignore failure to enable IPv6
# forwarding, for hosts with IPv6 disabled.
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1 || true
nsenter -n"$netns" sysctl -w net.ipv4.ip_forward=1
nsenter -n"$netns" sysctl -w net.ipv6.conf.all.forwarding=1 || true
exec "$dockerd" "$@"
fi
+14 -1
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"github.com/containerd/containerd/v2/pkg/apparmor"
"github.com/moby/moby/v2/daemon/internal/rootless"
aaprofile "github.com/moby/profiles/apparmor"
)
@@ -18,13 +19,25 @@ const (
// DefaultApparmorProfile returns the name of the default apparmor profile
func DefaultApparmorProfile() string {
if apparmor.HostSupports() {
if detachedNetNS, _ := rootless.DetachedNetNS(); detachedNetNS != "" {
// AppArmor is inaccessible with detached-netns because sysfs is netns-scoped.
return ""
}
return defaultAppArmorProfile
}
return ""
}
func ensureDefaultAppArmorProfile() error {
if apparmor.HostSupports() {
hostSupports := apparmor.HostSupports()
if hostSupports {
if detachedNetNS, _ := rootless.DetachedNetNS(); detachedNetNS != "" {
// "open /sys/kernel/security/apparmor/profiles: permission denied"
// (because sysfs is netns-scoped)
hostSupports = false
}
}
if hostSupports {
loaded, err := aaprofile.IsLoaded(defaultAppArmorProfile)
if err != nil {
return fmt.Errorf("Could not check if %s AppArmor profile was loaded: %s", defaultAppArmorProfile, err)
+18 -6
View File
@@ -453,7 +453,9 @@ func (daemon *Daemon) initializeNetworking(ctx context.Context, cfg *config.Conf
}
// Cleanup any stale sandbox left over due to ungraceful daemon shutdown
if err := daemon.netController.SandboxDestroy(ctx, ctr.ID); err != nil {
if err := daemon.runInNetNS(func() error {
return daemon.netController.SandboxDestroy(ctx, ctr.ID)
}); err != nil {
log.G(ctx).WithError(err).Errorf("failed to cleanup up stale network sandbox for container %s", ctr.ID)
}
@@ -497,7 +499,9 @@ func (daemon *Daemon) initializeNetworking(ctx context.Context, cfg *config.Conf
defer func() {
if retErr != nil {
if err := sb.Delete(ctx); err != nil {
if err := daemon.runInNetNS(func() error {
return sb.Delete(ctx)
}); err != nil {
log.G(ctx).WithFields(log.Fields{
"error": err,
"container": ctr.ID,
@@ -1027,7 +1031,9 @@ func (daemon *Daemon) releaseNetwork(ctx context.Context, ctr *container.Contain
return
}
if err := sb.Delete(ctx); err != nil {
if err := daemon.runInNetNS(func() error {
return sb.Delete(ctx)
}); err != nil {
log.G(ctx).Errorf("Error deleting sandbox id %s for container %s: %v", sid, ctr.ID, err)
}
@@ -1056,7 +1062,9 @@ func (daemon *Daemon) ConnectToNetwork(ctx context.Context, ctr *container.Conta
n, err := daemon.FindNetwork(idOrName)
if err == nil && n != nil {
if err := daemon.updateNetworkConfig(ctr, n, endpointConfig); err != nil {
if err := daemon.runInNetNS(func() error {
return daemon.updateNetworkConfig(ctr, n, endpointConfig)
}); err != nil {
return err
}
} else {
@@ -1070,7 +1078,9 @@ func (daemon *Daemon) ConnectToNetwork(ctx context.Context, ctr *container.Conta
EndpointSettings: endpointConfig,
DesiredMacAddress: endpointConfig.MacAddress,
}
if err := daemon.connectToNetwork(ctx, &daemon.config().Config, ctr, idOrName, epc); err != nil {
if err := daemon.runInNetNS(func() error {
return daemon.connectToNetwork(ctx, &daemon.config().Config, ctr, idOrName, epc)
}); err != nil {
return err
}
}
@@ -1102,7 +1112,9 @@ func (daemon *Daemon) DisconnectFromNetwork(ctx context.Context, ctr *container.
return cerrdefs.ErrInvalidArgument.WithMessage("cannot disconnect container from host network - container was created in host network mode")
}
if err := daemon.disconnectFromNetwork(ctx, ctr, n, false); err != nil {
if err := daemon.runInNetNS(func() error {
return daemon.disconnectFromNetwork(ctx, ctr, n, false)
}); err != nil {
return err
}
} else {
+12
View File
@@ -13,6 +13,7 @@ import (
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/config"
"github.com/moby/moby/v2/daemon/internal/rootless"
"github.com/moby/moby/v2/daemon/libnetwork/ns"
"github.com/moby/moby/v2/daemon/libnetwork/resolvconf"
"github.com/moby/sys/mount"
@@ -237,3 +238,14 @@ func supportsRecursivelyReadOnly(cfg *configStore, runtime string) error {
}
return fmt.Errorf("rro is not supported by runtime %q", runtime)
}
func (daemon *Daemon) runInNetNS(f func() error) error {
if rootless.RunningWithRootlessKit() {
if detachedNetNS, err := rootless.DetachedNetNS(); err != nil {
return err
} else if detachedNetNS != "" {
return rootless.RunInNetNS(detachedNetNS, f)
}
}
return f()
}
+3 -1
View File
@@ -837,7 +837,9 @@ func (daemon *Daemon) initNetworkController(cfg *config.Config, activeSandboxes
if len(activeSandboxes) > 0 {
log.G(ctx).Info("there are running containers, updated network configuration will not take affect")
} else if err := configureNetworking(ctx, daemon.netController, cfg); err != nil {
} else if err := daemon.runInNetNS(func() error {
return configureNetworking(ctx, daemon.netController, cfg)
}); err != nil {
return err
}
+4
View File
@@ -17,3 +17,7 @@ func setupResolvConf(_ *any) {}
func getSysInfo(_ *Daemon) *sysinfo.SysInfo {
return sysinfo.New()
}
func (daemon *Daemon) runInNetNS(f func() error) error {
return f()
}
+4
View File
@@ -570,3 +570,7 @@ func setupResolvConf(config *config.Config) {}
func getSysInfo(*config.Config) *sysinfo.SysInfo {
return sysinfo.New()
}
func (daemon *Daemon) runInNetNS(f func() error) error {
return f()
}
+127
View File
@@ -0,0 +1,127 @@
// Portions from https://github.com/containerd/nerdctl/pull/2723
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rootless
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"syscall"
"github.com/vishvananda/netns"
)
// DetachedNetNS returns non-empty netns path if RootlessKit is running with --detach-netns mode.
// Otherwise returns "" without an error.
var DetachedNetNS = sync.OnceValues(detachedNetNS)
func detachedNetNS() (string, error) {
stateDir := os.Getenv("ROOTLESSKIT_STATE_DIR")
if stateDir == "" {
return "", nil
}
p := filepath.Join(stateDir, "netns")
if _, err := os.Stat(p); err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", nil
}
return "", err
}
return p, nil
}
// RunInNetNS runs f in the detached network namespace if one is
// configured, otherwise runs f directly. The function is executed on a
// dedicated OS thread that is discarded after use, because setns back to the
// host network namespace fails with EPERM in rootless mode (the host netns is
// owned by the initial user namespace where the daemon lacks CAP_SYS_ADMIN).
func RunInNetNS(nsPath string, f func() error) error {
if nsPath == "" {
return f()
}
ch := make(chan error, 1)
go func() {
runtime.LockOSThread()
ns, err := netns.GetFromPath(nsPath)
if err != nil {
runtime.UnlockOSThread()
ch <- fmt.Errorf("failed to open detached network namespace %s: %w", nsPath, err)
return
}
defer ns.Close()
origNS, err := netns.Get()
if err != nil {
runtime.UnlockOSThread()
ch <- fmt.Errorf("failed to get current network namespace: %w", err)
return
}
defer origNS.Close()
if err := netns.Set(ns); err != nil {
runtime.UnlockOSThread()
ch <- fmt.Errorf("failed to enter detached network namespace: %w", err)
return
}
ch <- f()
if err := netns.Set(origNS); err != nil {
// Cannot restore the thread's network namespace. Keep the
// goroutine locked so the Go runtime terminates the thread
// instead of returning a tainted thread to the pool.
return
}
runtime.UnlockOSThread()
}()
return <-ch
}
// sandboxNSThreads tracks OS threads that are currently executing in a
// container sandbox network namespace (via InvokeFunc). This is used to
// prevent iptables/nftables wrappers from adding nsenter to the detached
// netns when they are already running in the correct (container) namespace.
var sandboxNSThreads sync.Map // key: int (tid)
// MarkInSandboxNS marks the current OS thread as being inside a container
// sandbox network namespace. The caller must have locked the OS thread with
// runtime.LockOSThread before calling this function.
func MarkInSandboxNS() {
sandboxNSThreads.Store(syscall.Gettid(), struct{}{})
}
// UnmarkInSandboxNS removes the sandbox namespace mark from the current OS
// thread. Must be called on the same locked thread that called MarkInSandboxNS.
func UnmarkInSandboxNS() {
sandboxNSThreads.Delete(syscall.Gettid())
}
// InSandboxNS reports whether the current OS thread has been marked as
// executing inside a container sandbox network namespace. When true,
// iptables/nftables commands should NOT be wrapped with nsenter to the
// detached netns, because the thread is already in the target (container)
// namespace.
func InSandboxNS() bool {
_, ok := sandboxNSThreads.Load(syscall.Gettid())
return ok
}
@@ -10,6 +10,7 @@ import (
"strings"
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/internal/rootless"
"github.com/opencontainers/runtime-spec/specs-go"
)
@@ -32,6 +33,7 @@ func ToRootfulInRootless(spec *specs.Spec) {
// * Fix up OOMScoreAdj
// * Fix up /proc if --pid=host
// * Fix up /dev/shm and /dev/mqueue if --ipc=host
// * Fix up /sys if --net=host (with detach-netns)
//
// v2Controllers should be non-nil only if running with v2 and systemd.
func ToRootless(spec *specs.Spec, v2Controllers []string) error {
@@ -120,6 +122,31 @@ func toRootless(spec *specs.Spec, v2Controllers []string, currentOOMScoreAdj int
}
}
// Fix up /sys if --net=host (with detach-netns)
detachedNetNS, err := rootless.DetachedNetNS()
if err != nil {
return err
}
if detachedNetNS != "" {
netHost, err := isHostNS(spec, specs.NetworkNamespace)
if err != nil {
return err
}
if netHost {
// For rootless + host netns, we can't mount sysfs.
// We can't (non-recursively) bind mount /sys, either.
//
// TODO: consider to just rbind /sys from the host with rro,
// when rro is available (kernel >= 5.12, runc >= 1.1).
//
// Relevant: https://github.com/moby/buildkit/blob/v0.12.4/util/rootless/specconv/specconv_linux.go#L15-L34
// https://github.com/containerd/nerdctl/pull/2723
if err := removeSysfs(spec); err != nil {
return err
}
}
}
return nil
}
@@ -139,7 +166,11 @@ func isHostNS(spec *specs.Spec, nsType specs.LinuxNamespaceType) (bool, error) {
if err != nil {
return false, err
}
selfNS, err := os.Readlink(filepath.Join("/proc/self/ns", string(nsType)))
procfsNSType := string(nsType)
if nsType == specs.NetworkNamespace {
procfsNSType = "net"
}
selfNS, err := os.Readlink(filepath.Join("/proc/self/ns", procfsNSType))
if err != nil {
return false, err
}
@@ -197,3 +228,26 @@ func bindMountHostIPC(s *specs.Spec) error {
}
return nil
}
func removeSysfs(s *specs.Spec) error {
var hasSysfs bool
for _, mount := range s.Mounts {
if mount.Type == "sysfs" {
hasSysfs = true
break
}
}
if !hasSysfs {
// NOP, as the user has specified a custom /sys mount
return nil
}
var mounts []specs.Mount // nolint: prealloc
for _, mount := range s.Mounts {
if strings.HasPrefix(mount.Destination, "/sys") {
continue
}
mounts = append(mounts, mount)
}
s.Mounts = mounts
return nil
}
@@ -15,6 +15,7 @@ import (
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/internal/netiputil"
"github.com/moby/moby/v2/daemon/internal/otelutil"
"github.com/moby/moby/v2/daemon/internal/rootless"
"github.com/moby/moby/v2/daemon/internal/stringid"
"github.com/moby/moby/v2/daemon/libnetwork/datastore"
"github.com/moby/moby/v2/daemon/libnetwork/driverapi"
@@ -210,10 +211,25 @@ func newDriver(store *datastore.Store, config Configuration, pms *drvregistry.Po
return d, nil
}
func runInNetNS(f func() error) error {
if rootless.RunningWithRootlessKit() {
if detachedNetNS, err := rootless.DetachedNetNS(); err != nil {
return err
} else if detachedNetNS != "" {
return rootless.RunInNetNS(detachedNetNS, f)
}
}
return f()
}
// Register registers a new instance of bridge driver.
func Register(r driverapi.Registerer, store *datastore.Store, pms *drvregistry.PortMappers, config Configuration) error {
d, err := newDriver(store, config, pms)
if err != nil {
var d *driver
if err := runInNetNS(func() error {
var err error
d, err = newDriver(store, config, pms)
return err
}); err != nil {
return err
}
return r.RegisterDriver(NetworkType, d, driverapi.Capability{
@@ -8,18 +8,38 @@ import (
"io"
"os/exec"
"strings"
"sync"
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/internal/rootless"
"go.opentelemetry.io/otel"
)
type nftHandle = struct{}
var lookPathNSEnter = sync.OnceValues(func() (string, error) {
return exec.LookPath("nsenter")
})
func (t *table) nftApply(ctx context.Context, nftCmd []byte) error {
ctx, span := otel.Tracer("").Start(ctx, spanPrefix+".nftApply.exec")
defer span.End()
cmd := exec.Command(nftPath, "-f", "-")
cmdPath := nftPath
cmdArgs := []string{nftPath, "-f", "-"}
detachedNetNS, err := rootless.DetachedNetNS()
if err != nil {
return fmt.Errorf("could not check for detached netns: %w", err)
}
if detachedNetNS != "" && !rootless.InSandboxNS() {
nsenterPath, err := lookPathNSEnter()
if err != nil {
return fmt.Errorf("nsenter not found: %w", err)
}
cmdPath = nsenterPath
cmdArgs = append([]string{nsenterPath, "-n" + detachedNetNS, "-F", "--"}, cmdArgs...)
}
cmd := exec.CommandContext(ctx, cmdPath, cmdArgs[1:]...)
stdinPipe, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("getting stdin pipe for nft: %w", err)
+14 -1
View File
@@ -364,7 +364,20 @@ func (iptable IPTable) raw(args ...string) ([]byte, error) {
log.G(context.TODO()).Debugf("%s, %v", path, args)
startTime := time.Now()
output, err := exec.Command(path, args...).CombinedOutput()
cmd := exec.CommandContext(context.TODO(), path, args...)
detachedNetNS, err := rootless.DetachedNetNS()
if err != nil {
return nil, fmt.Errorf("could not check for detached netns: %w", err)
}
if detachedNetNS != "" && !rootless.InSandboxNS() {
nsenterPath, err := exec.LookPath("nsenter")
if err != nil {
return nil, fmt.Errorf("nsenter not found: %w", err)
}
cmd.Args = append([]string{nsenterPath, "-n" + detachedNetNS, "-F", "--"}, cmd.Args...)
cmd.Path = nsenterPath
}
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("iptables failed: %s %v: %s (%s)", commandName, strings.Join(args, " "), output, err)
}
+114 -7
View File
@@ -20,6 +20,8 @@ package nlwrap
import (
"context"
"fmt"
"runtime"
"github.com/containerd/log"
"github.com/pkg/errors"
@@ -42,12 +44,71 @@ func NewHandle(nlFamilies ...int) (Handle, error) {
return Handle{nlh}, nil
}
// NewHandleAt creates a new netlink handle in the specified network namespace.
//
// Unlike netlink.NewHandleAt, this function properly manages thread lifecycle
// when the calling thread's network namespace cannot be restored after switching
// (e.g. in rootless mode where setns back to the host netns fails with EPERM).
// The upstream netlink library silently ignores setns restoration errors and
// returns the tainted thread to the Go runtime's thread pool, which causes
// goroutines scheduled on those threads to operate in the wrong network
// namespace.
func NewHandleAt(ns netns.NsHandle, nlFamilies ...int) (Handle, error) {
nlh, err := netlink.NewHandleAt(ns, nlFamilies...)
if err != nil {
return Handle{}, err
if !ns.IsOpen() {
// No target namespace; same as NewHandle.
return NewHandle(nlFamilies...)
}
return Handle{nlh}, nil
type result struct {
handle *netlink.Handle
err error
}
ch := make(chan result, 1)
go func() {
runtime.LockOSThread()
origNS, err := netns.Get()
if err != nil {
runtime.UnlockOSThread()
ch <- result{err: fmt.Errorf("could not get current network namespace: %w", err)}
return
}
defer origNS.Close()
if err := netns.Set(ns); err != nil {
runtime.UnlockOSThread()
ch <- result{err: fmt.Errorf("failed to enter network namespace: %w", err)}
return
}
// Create netlink sockets in the target namespace.
// NewHandle with no ns args does not do any namespace switching.
nlh, err := netlink.NewHandle(nlFamilies...)
if err != nil {
// Best-effort restore before reporting the error.
netns.Set(origNS) //nolint:errcheck
runtime.UnlockOSThread()
ch <- result{err: err}
return
}
if err := netns.Set(origNS); err != nil {
// Cannot restore the thread's network namespace. Keep the
// goroutine locked to this thread so the Go runtime terminates
// it instead of returning a tainted thread to the pool.
ch <- result{handle: nlh}
return
}
runtime.UnlockOSThread()
ch <- result{handle: nlh}
}()
r := <-ch
if r.err != nil {
return Handle{}, r.err
}
return Handle{r.handle}, nil
}
func (nlh Handle) Close() {
@@ -160,10 +221,56 @@ func LinkList() (links []netlink.Link, err error) {
return links, discardErrDumpInterrupted(err)
}
// LinkSubscribeWithOptions calls netlink.LinkSubscribeWithOptions, retrying if necessary.
// Close the done channel when done (rather than just sending on it), so that goroutines
// started by the netlink package are all stopped.
// LinkSubscribeWithOptions calls netlink.LinkSubscribeWithOptions, retrying if
// necessary. Close the done channel when done (rather than just sending on it),
// so that goroutines started by the netlink package are all stopped.
//
// When a target namespace is specified, the subscribe socket is created on a
// dedicated OS thread to avoid the same executeInNetns thread contamination
// issue described in [NewHandleAt].
func LinkSubscribeWithOptions(ch chan<- netlink.LinkUpdate, done <-chan struct{}, options netlink.LinkSubscribeOptions) (err error) {
if options.Namespace != nil && options.Namespace.IsOpen() {
ns := *options.Namespace
// Clear the namespace option so the netlink library does not do
// its own namespace switching (via executeInNetns). We handle it.
options.Namespace = nil
errCh := make(chan error, 1)
go func() {
runtime.LockOSThread()
origNS, nserr := netns.Get()
if nserr != nil {
runtime.UnlockOSThread()
errCh <- fmt.Errorf("could not get current network namespace: %w", nserr)
return
}
defer origNS.Close()
if nserr := netns.Set(ns); nserr != nil {
runtime.UnlockOSThread()
errCh <- fmt.Errorf("failed to enter network namespace: %w", nserr)
return
}
// Create the subscribe socket in the target namespace.
// With Namespace cleared, the netlink library will not
// attempt any namespace switching internally.
retryOnIntr(func() error {
err = netlink.LinkSubscribeWithOptions(ch, done, options) //nolint:forbidigo
return err
})
errCh <- err
if nserr := netns.Set(origNS); nserr != nil {
// Cannot restore: keep locked so the runtime kills
// this thread instead of returning it to the pool.
return
}
runtime.UnlockOSThread()
}()
return <-errCh
}
retryOnIntr(func() error {
err = netlink.LinkSubscribeWithOptions(ch, done, options) //nolint:forbidigo
return err
+23 -4
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/internal/rootless"
"github.com/moby/moby/v2/daemon/libnetwork/internal/modprobe"
"github.com/moby/moby/v2/daemon/libnetwork/nlwrap"
"github.com/vishvananda/netns"
@@ -22,12 +23,30 @@ var initNamespace = sync.OnceValues(initHandles)
// initHandles initializes a new network namespace
func initHandles() (netns.NsHandle, nlwrap.Handle) {
initNs, err := netns.Get()
var (
initNs netns.NsHandle
initNl nlwrap.Handle
err error
)
detachedNetNS, err := rootless.DetachedNetNS()
if err != nil {
log.G(context.Background()).WithError(err).Error("could not get initial namespace: falling back to using netns.None")
initNs = netns.None()
log.G(context.Background()).WithError(err).Error("could not check for detached netns")
}
if detachedNetNS != "" {
initNs, err = netns.GetFromPath(detachedNetNS)
if err != nil {
log.G(context.Background()).WithError(err).Errorf("could not get detached network namespace %s", detachedNetNS)
return initNs, initNl
}
initNl, err = nlwrap.NewHandleAt(initNs, getSupportedNlFamilies()...)
} else {
initNs, err = netns.Get()
if err != nil {
log.G(context.Background()).WithError(err).Error("could not get initial namespace: falling back to using netns.None")
initNs = netns.None()
}
initNl, err = nlwrap.NewHandle(getSupportedNlFamilies()...)
}
initNl, err := nlwrap.NewHandle(getSupportedNlFamilies()...)
if err != nil {
// Fail fast to keep the invariant: NlHandle must be a valid handle
panic(fmt.Errorf("could not create netlink handle on initial (host) namespace: %w", err))
+13
View File
@@ -14,6 +14,7 @@ import (
"syscall"
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/internal/rootless"
"github.com/moby/moby/v2/daemon/internal/unshare"
"github.com/moby/moby/v2/daemon/libnetwork/nlwrap"
"github.com/moby/moby/v2/daemon/libnetwork/ns"
@@ -115,6 +116,15 @@ func NewSandbox(key string, osCreate, isRestore bool) (*Namespace, error) {
n := &Namespace{path: key, isDefault: !osCreate}
detachedNetNS, err := rootless.DetachedNetNS()
if err != nil {
return nil, err
}
if detachedNetNS != "" && !osCreate {
// n refers to the host netns and we do not have a permission to do the netlink stuff
return n, nil
}
sboxNs, err := netns.GetFromPath(n.path)
if err != nil {
return nil, fmt.Errorf("failed get network namespace %q: %v", n.path, err)
@@ -193,6 +203,7 @@ func createNetworkNamespace(path string, osCreate bool) error {
if osCreate {
return unshare.Go(unix.CLONE_NEWNET, do, nil)
}
// use host netns
return do()
}
@@ -355,6 +366,7 @@ func (n *Namespace) InvokeFunc(f func()) error {
}
defer func() {
close(done)
rootless.UnmarkInSandboxNS()
if err := netns.Set(origNS); err != nil {
log.G(context.TODO()).WithError(err).Warn("failed to restore thread's network namespace")
// Recover from the error by leaving this goroutine locked to
@@ -364,6 +376,7 @@ func (n *Namespace) InvokeFunc(f func()) error {
runtime.UnlockOSThread()
}
}()
rootless.MarkInSandboxNS()
f()
}()
return <-done
@@ -14,6 +14,7 @@ import (
"time"
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/internal/rootless"
"github.com/moby/moby/v2/daemon/libnetwork/types"
)
@@ -58,6 +59,18 @@ func StartProxy(pb types.PortBinding,
cmd.ExtraFiles = append(cmd.ExtraFiles, listenSock)
}
detachedNetNS, err := rootless.DetachedNetNS()
if err != nil {
return nil, err
}
if detachedNetNS != "" {
cmd.Path, err = exec.LookPath("nsenter")
if err != nil {
return nil, err
}
cmd.Args = append([]string{cmd.Path, "-n" + detachedNetNS, "-F", "--"}, cmd.Args...)
}
wait := make(chan error, 1)
// As p.cmd.SysProcAttr.Pdeathsig is set, the signal will be sent to the
+25 -13
View File
@@ -280,13 +280,21 @@ func (daemon *Daemon) WaitForDetachment(ctx context.Context, networkName, networ
// CreateManagedNetwork creates an agent network.
func (daemon *Daemon) CreateManagedNetwork(create clustertypes.NetworkCreateRequest) error {
_, err := daemon.createNetwork(context.TODO(), &daemon.config().Config, create.CreateRequest, create.ID, true)
return err
return daemon.runInNetNS(func() error {
_, err := daemon.createNetwork(context.TODO(), &daemon.config().Config, create.CreateRequest, create.ID, true)
return err
})
}
// CreateNetwork creates a network with the given name, driver and other optional parameters
func (daemon *Daemon) CreateNetwork(ctx context.Context, create networktypes.CreateRequest) (*networktypes.CreateResponse, error) {
return daemon.createNetwork(ctx, &daemon.config().Config, create, "", false)
var resp *networktypes.CreateResponse
err := daemon.runInNetNS(func() error {
var err error
resp, err = daemon.createNetwork(ctx, &daemon.config().Config, create, "", false)
return err
})
return resp, err
}
func (daemon *Daemon) createNetwork(ctx context.Context, cfg *config.Config, create networktypes.CreateRequest, id string, agent bool) (*networktypes.CreateResponse, error) {
@@ -623,20 +631,24 @@ func (daemon *Daemon) GetNetworkDriverList(ctx context.Context) []string {
// DeleteManagedNetwork deletes an agent network.
// The requirement of networkID is enforced.
func (daemon *Daemon) DeleteManagedNetwork(networkID string) error {
n, err := daemon.GetNetworkByID(networkID)
if err != nil {
return err
}
return daemon.deleteNetwork(n, true)
return daemon.runInNetNS(func() error {
n, err := daemon.GetNetworkByID(networkID)
if err != nil {
return err
}
return daemon.deleteNetwork(n, true)
})
}
// DeleteNetwork destroys a network unless it's one of docker's predefined networks.
func (daemon *Daemon) DeleteNetwork(networkID string) error {
n, err := daemon.GetNetworkByID(networkID)
if err != nil {
return fmt.Errorf("could not find network by ID: %w", err)
}
return daemon.deleteNetwork(n, false)
return daemon.runInNetNS(func() error {
n, err := daemon.GetNetworkByID(networkID)
if err != nil {
return fmt.Errorf("could not find network by ID: %w", err)
}
return daemon.deleteNetwork(n, false)
})
}
func (daemon *Daemon) deleteNetwork(nw *libnetwork.Network, dynamic bool) error {
+5
View File
@@ -18,6 +18,7 @@ import (
containertypes "github.com/moby/moby/api/types/container"
dconfig "github.com/moby/moby/v2/daemon/config"
"github.com/moby/moby/v2/daemon/container"
"github.com/moby/moby/v2/daemon/internal/rootless"
"github.com/moby/moby/v2/daemon/internal/rootless/mountopts"
"github.com/moby/moby/v2/daemon/internal/rootless/specconv"
"github.com/moby/moby/v2/daemon/pkg/oci"
@@ -126,6 +127,10 @@ func WithSelinux(c *container.Container) coci.SpecOpts {
func WithApparmor(c *container.Container) coci.SpecOpts {
return func(ctx context.Context, _ coci.Client, _ *containers.Container, s *coci.Spec) error {
if apparmor.HostSupports() {
// AppArmor is inaccessible with detached-netns because sysfs is netns-scoped.
if detachedNetNS, _ := rootless.DetachedNetNS(); detachedNetNS != "" {
return nil
}
var appArmorProfile string
if c.AppArmorProfile != "" {
appArmorProfile = c.AppArmorProfile
+3 -1
View File
@@ -34,7 +34,9 @@ func (daemon *Daemon) initializeCreatedTask(
return errdefs.System(err)
}
}
if err := daemon.allocateNetwork(ctx, cfg, ctr); err != nil {
if err := daemon.runInNetNS(func() error {
return daemon.allocateNetwork(ctx, cfg, ctr)
}); err != nil {
return fmt.Errorf("%s: %w", errSetupNetworking, err)
}
return nil