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:
20 files changed
+518
-41
No files matched your search
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user