From cbb6ba33791477cd33088a9731aa9a060d43f3c8 Mon Sep 17 00:00:00 2001 From: Andreas Trawoeger Date: Thu, 22 Oct 2026 22:30:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(rootless):=20the=20CONTAINERD.md=20=C2=A74?= =?UTF-8?q?=20checklist,=20the=20rootless=20units,=20and=20a=20fatal=20che?= =?UTF-8?q?cksum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The candy installed the rootless binaries but carried none of the CONTAINERD.md §2/§4 checklist, promised rootless containerd/buildkit units that no plan step wrote, and made the nerdctl-full checksum advisory: the release publishes one SHA256SUMS file, so the per-asset `.sha256sum` fetch 404'd and `|| true` hid it. - per-distro uidmap helpers (shadow/uidmap/shadow-utils), the user D-Bus, containerd explicitly, `user:100000:65536` in /etc/subuid and /etc/subgid, the user@.service delegation drop-in, and slirp4netns pinned as the network driver; - the rootless containerd/buildkit user units written from the upstream setuptool templates (ExecStart via /usr/bin/env, so one unit serves both install prefixes); - ONE written checksum verifier (no --ignore-missing, no || true) plus a negative check that proves a corrupted archive is rejected; - the CNI plugins linked at the ONE cni_path the toml declares (/opt/cni/bin): the tarball extracts to /usr/local/libexec/cni, Arch's package installs to /opt/cni/bin, and no per-distro cni_path is needed; - `namespace = "default"` replaces the retired `charly` value; - check-nerdctl-rootless: a disposable Fedora VM bed in the absence/perform/presence shape (linger, daemon-reload, the setuptool over the written units, `nerdctl info` rootless, the helpers, /dev/fuse, the units active), with the daemon-safety proof that the daemon's namespace socket is the USER socket by device:inode and that the host system socket's inode/mtime is unchanged; - the bed image declares its fedora distro chain: without it img.DistroDef is nil and no declarative package install is emitted at all. Closes #10 Closes #11 Agent: containerd-crabbox-program/w4c3 · session 8b1c01cf-7f79-4a75-bff8-240415a06208 Assisted-by: Claude Code deepseek-v4.1-flash:cloud (fully tested and validated) --- .agents/skills/layer-nerdctl/SKILL.md | 95 +++++++- .gitignore | 2 + AGENTS.md | 11 +- README.md | 39 ++- box/check-nerdctl-app/charly.yml | 4 + candy/layer-nerdctl/charly.yml | 336 +++++++++++++++++++++++--- charly.yml | 250 +++++++++++++++++++ 7 files changed, 682 insertions(+), 55 deletions(-) diff --git a/.agents/skills/layer-nerdctl/SKILL.md b/.agents/skills/layer-nerdctl/SKILL.md index 3228fe2..3d99f9a 100644 --- a/.agents/skills/layer-nerdctl/SKILL.md +++ b/.agents/skills/layer-nerdctl/SKILL.md @@ -1,9 +1,12 @@ --- name: layer-nerdctl description: 'nerdctl (containerd CLI) + the rootless containerd/buildkit/CNI stack - — native Arch/Alpine packages, the pinned sha256 nerdctl-full tarball on Fedora/Debian/Ubuntu, - nerdctl.toml + the charly CNI conflist + the nested-pod posture. Use when working - with the nerdctl engine, the layer-nerdctl candy, or engine: nerdctl deploys.' + — native Arch/Alpine packages, the pinned FATALLY sha256-verified nerdctl-full tarball + on Fedora/Debian/Ubuntu, nerdctl.toml with namespace=default, the charly CNI conflist, + the written rootless containerd/buildkit user units, and the CONTAINERD.md §2/§4 + checklist (uidmap helpers, subuid/subgid, delegation, /dev/fuse, user D-Bus, + slirp4netns). Use when working with the nerdctl engine, the layer-nerdctl candy, or + engine: nerdctl deploys.' --- # layer-nerdctl @@ -14,18 +17,72 @@ engine. ## What it installs -- **Native packages** on Arch/CachyOS/Omarchy and Alpine: `nerdctl`, +- **Native packages** on Arch/CachyOS/Omarchy and Alpine: `nerdctl`, `containerd`, `cni-plugins`, `rootlesskit`, `slirp4netns`, `buildkit`, `fuse-overlayfs`, - `crun`, `iptables-nft` (arch) / `iptables` (alpine). + `crun`, `iptables-nft` (arch) / `iptables` (alpine), plus `shadow` + (`newuidmap`/`newgidmap`) and `dbus` (the user `dbus.socket`). - **Non-native distros** (Fedora/Debian/Ubuntu) install the pinned, - sha256-verified `nerdctl-full` tarball (step `nerdctl-full-tarball`), which - supplies nerdctl + containerd + `containerd-fuse-overlayfs-grpc` + CNI + - buildkit + rootlesskit in one archive. -- `/etc/nerdctl/nerdctl.toml` (charly namespace + CNI path) and - the `charly` CNI network conflist at `/etc/cni/net.d/charly.conflist`. + **fatally** sha256-verified `nerdctl-full` tarball (step + `nerdctl-full-tarball`), which supplies nerdctl + containerd + + `containerd-fuse-overlayfs-grpc` + CNI + buildkit + rootlesskit in one archive. + The helpers come from `shadow-utils` (Fedora) or `uidmap` + (Debian/Ubuntu); the user bus from `dbus-daemon` or `dbus-user-session`. +- `/etc/nerdctl/nerdctl.toml` with `namespace = "default"` (the ONE spelling + the programme uses) and `cni_path = "/opt/cni/bin"`. The BuildKit socket is + deliberately NOT pinned: it is a runtime value resolved through the ONE roots + resolver (`$XDG_RUNTIME_DIR/charly/buildkitd.sock`). +- The `charly` CNI network conflist at `/etc/cni/net.d/charly.conflist`, and the CNI + plugins at the ONE `cni_path` the toml declares (`/opt/cni/bin`): Arch's package + installs there, the nerdctl-full tarball extracts to `/usr/local/libexec/cni`, and + a step links every real plugin to the declared path (no per-distro `cni_path`). +- The rootless **containerd and buildkit user units** + (`~/.config/systemd/user/{containerd,buildkit}.service`), written from the + upstream setuptool templates. `ExecStart` goes through `/usr/bin/env` so one + unit works for both the `/usr/local/bin` (tarball) and `/usr/bin` (package) + install locations. - The **nested-pod posture**: userns-scoped `cap_add: ALL`, `/dev/fuse`, `/dev/net/tun`, `unmask=/proc/*`. +## The rootless checklist (CONTAINERD.md §2 and §4) + +Every item below is a real plan step with a `check:`; the runtime half is +asserted by the `check-nerdctl-rootless` disposable-VM bed. + +| Item | Where | Why it exists | +|---|---|---| +| `newuidmap` / `newgidmap` | distro packages (`shadow`, `uidmap`, `shadow-utils`) | rootlesskit execs them to write the UID/GID map; the setuptool checks only the binaries **it** installs, never these two | +| `user:100000:65536` in `/etc/subuid` and `/etc/subgid` | plan step `rootless-subuid` | the ranges the helpers consume | +| `user@.service.d/delegate.conf` with `Delegate=cpu cpuset io memory pids` | plan step `rootless-delegate-controllers` | memory/cpuset/io limits and `--cpuset-cpus`; a new login picks it up | +| linger | bed, PERFORM (`loginctl enable-linger`) | the user manager and its units must outlive the login session | +| `/dev/fuse` | bed, PRESENCE (opens the node) | the fuse-overlayfs fallback is a libfuse3 client; the setuptool checks the binaries and never the node | +| user D-Bus (`dbus-user-session` / `dbus-daemon` / `dbus`) | package + bed (`dbus.socket`) | the rootless unit has `Requires=dbus.socket` | +| `slirp4netns` as the network driver | the containerd unit sets `CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns` | the documented default | +| the setuptool run | bed, PERFORM (`install` + `install-buildkit`) | starts and enables the written units | +| `namespace = "default"` | `nerdctl.toml` | one namespace spelling, no per-layer name | + +### Network drivers — the measured state + +- **`slirp4netns` is the default** and is pinned in the unit. +- **`pasta` is an option, not a recommendation.** Measured on rootlesskit 3.3.0 + with pasta `2026_10_02.cba3570`: `rootlesskit --net=pasta true` returns rc 0. + The deployment's networking properties (published ports bind `0.0.0.0`, + container→host loopback is blocked, no ephemeral host port) are measured on + slirp4netns, so pasta is not recommended. +- **`bypass4netns` is an option only where rootlesskit serves it.** The setuptool + can install `bypass4netnsd`, and nerdctl annotates a container with + `nerdctl/bypass4netns=true`. Measured on rootlesskit 3.3.0, both + `--net=bypass4netns` and `--port-driver=bypass4netns` are refused + (`unknown network mode` / `unknown port driver`), so this layer does not set it. + +### Low ports — a documented option, never forced + +Do **not** lower `net.ipv4.ip_unprivileged_port_start` for this layer. A rootless +**publish** of a port below 1024 fails `bind: permission denied` until the host +lowers it; the **container-side** threshold is the CNI netns's own `0`, so a +process inside a rootless lease binds a low port with no sysctl at all. If a +specific deploy needs the publish side, set it deliberately in +`/etc/sysctl.d/` on that host — never here, and never as a blanket default. + ## Engine capability The `engine: nerdctl` word is served by `opencharly/plugin-nerdctl` @@ -35,7 +92,21 @@ The `engine: nerdctl` word is served by `opencharly/plugin-nerdctl` host user under rootless), guaranteeing uid-identical sharing; a non-zero uid needs the subuid caveat. +## Checks and the R10 bed + +- The candy's `check:` steps prove the static half on every distro arm: the + helpers, the subuid ranges, the delegation drop-in, D-Bus, slirp4netns, the + written units (marker + `systemd-analyze --user verify`), `namespace = + "default"`, and a **negative** test that the checksum verifier rejects a + corrupted archive. +- `check-nerdctl-rootless` (root `charly.yml`, disposable VM) is the runtime + gate: ABSENCE (no rootless daemon) → PERFORM (daemon-reload, linger, the + setuptool) → PRESENCE (`nerdctl info` reports `name=rootless`, the helpers + resolve, `/dev/fuse` opens, the units are active). It records the rootful + system socket's inode/mtime before the daemon starts and asserts it is + unchanged after, and that the rootless daemon listens on the user socket. + ## Use when -Working with the nerdctl engine, the layer-nerdctl candy, or an -`engine: nerdctl` deploy. +Working with the nerdctl engine, the layer-nerdctl candy, the rootless +containerd/buildkit checklist, or an `engine: nerdctl` deploy. diff --git a/.gitignore b/.gitignore index 8eaede2..3721795 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Build artefacts .build/ +# The VM bed's rendered disk + seed (charly vm build) — never authored, never tracked. +/image/ # Editor / OS .DS_Store diff --git a/AGENTS.md b/AGENTS.md index 8a22a17..1a2cc8a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,11 +10,15 @@ bed image in `box/check-nerdctl-app/charly.yml`. The candy carries the embedded Canonical files: -- `charly.yml` — the project root: `discover:`, `defaults:`, and the - `check-nerdctl-nested:` disposable `pod:` R10 bed. +- `charly.yml` — the project root: `discover:`, `defaults:`, the + `check-nerdctl-nested:` disposable `pod:` bed (nested-pod posture), and the + `check-nerdctl-rootless:` disposable `vm:` bed (the CONTAINERD.md §2/§4 + rootless checklist, whose runtime half needs a real systemd). - `candy/layer-nerdctl/charly.yml` — the `layer-nerdctl:` candy entity (the `NERDCTL_FULL_VERSION` var, the `security:` posture, the plan steps, the `check:` assertions) and the `layer-nerdctl-skill:` skill entity. +- `.agents/skills/layer-nerdctl/SKILL.md` — the skill body (authored content, + discovered from the repo root). - `box/check-nerdctl-app/charly.yml` — the disposable bed image composing the candy. - `README.md` — user overview only; never agent guidance. @@ -44,6 +48,9 @@ Canonical files: `pod:` bed in the root `charly.yml` — a FRESH deploy asserting `/dev/fuse`, `/dev/net/tun`, the full `CapEff` mask, the userns uid map, and `nerdctl` on PATH. It fails without the `security:` block. +- The R10 for the rootless checklist is `check-nerdctl-rootless:` — a disposable + `vm:` bed, because the checklist's runtime half (`systemctl --user`, linger, + the units) cannot be proven in a supervisord pod. ## Modify this repo diff --git a/README.md b/README.md index d0ce213..558a507 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,39 @@ stack, as a standalone OpenCharly layer repo. Composed by a box that wants the opt-in `engine: nerdctl` engine. On Arch/CachyOS/Omarchy and Alpine the candy uses native distro packages; on Fedora, -Debian, and Ubuntu it installs the pinned, sha256-verified `nerdctl-full` tarball -(which supplies nerdctl + containerd + `containerd-fuse-overlayfs-grpc` + CNI + -buildkit + rootlesskit in one archive). It writes `/etc/nerdctl/nerdctl.toml` -(the `charly` namespace + rootless buildkit host) and the `charly` CNI network -conflist at `/etc/cni/net.d/charly.conflist`. +Debian, and Ubuntu it installs the pinned, **fatally** sha256-verified +`nerdctl-full` tarball (which supplies nerdctl + containerd + +`containerd-fuse-overlayfs-grpc` + CNI + buildkit + rootlesskit in one archive). +It writes `/etc/nerdctl/nerdctl.toml` (`namespace = "default"`), the `charly` CNI +network conflist at `/etc/cni/net.d/charly.conflist`, and the rootless +containerd/buildkit user units. ## What it provides | Property | Value | |---|---| | Layer / candy | `layer-nerdctl` | -| Pinned version | `NERDCTL_FULL_VERSION` `2.1.2` | -| Config | `/etc/nerdctl/nerdctl.toml`, `/etc/cni/net.d/charly.conflist` | +| Pinned version | `NERDCTL_FULL_VERSION` `2.1.2`, sha256-verified fatally | +| Config | `/etc/nerdctl/nerdctl.toml` (`namespace = "default"`), `/etc/cni/net.d/charly.conflist`, CNI plugins at `/opt/cni/bin` | +| Rootless units | `~/.config/systemd/user/containerd.service`, `~/.config/systemd/user/buildkit.service` | +| Rootless checklist | uidmap helpers, `/etc/subuid` + `/etc/subgid` (`user:100000:65536`), `user@.service.d/delegate.conf`, linger, `/dev/fuse`, user D-Bus, slirp4netns (CONTAINERD.md §2, §4) | | Nested-pod posture | userns-scoped `cap_add: ALL`, `/dev/fuse`, `/dev/net/tun`, `unmask=/proc/*` | | Service / port | none | The engine word is served by `opencharly/plugin-nerdctl` (out-of-process) or compiled in; every engine op delegates to `container.InvokeEngineOp("nerdctl", …)`. +## Rootless networking + +`slirp4netns` is the default and is pinned in the containerd unit. `pasta` and +`bypass4netns` are documented options, not recommendations: on rootlesskit 3.3.0 +pasta starts (`rootlesskit --net=pasta true`, rc 0) but the deployment's +networking properties are measured on slirp4netns, and bypass4netns is refused by +that rootlesskit (`unknown network mode` / `unknown port driver`). Publishing a +port below 1024 is governed by the host's +`net.ipv4.ip_unprivileged_port_start`; this layer never sets it — see the skill +for when to do it deliberately. + ## How to use it This repo uses the multi-directory layout: the root `charly.yml` is the project @@ -41,17 +55,20 @@ my-nested-box: ## Layout -- `charly.yml` — the project root: `discover:`, `defaults:`, and the - `check-nerdctl-nested:` disposable `pod:` R10 bed. +- `charly.yml` — the project root: `discover:`, `defaults:`, the + `check-nerdctl-nested:` disposable `pod:` bed (nested-pod posture), and the + `check-nerdctl-rootless:` disposable VM bed (the §4 rootless checklist). - `candy/layer-nerdctl/charly.yml` — the `layer-nerdctl:` candy entity, its - `security:` posture, and the embedded `layer-nerdctl-skill:` skill entity. + `security:` posture, the plan steps/checks, and the `layer-nerdctl-skill:` + skill entity. +- `.agents/skills/layer-nerdctl/SKILL.md` — the skill body. - `box/check-nerdctl-app/charly.yml` — the disposable bed image composing the candy. - `README.md` — this user overview. ## Related - Owning skill: `/charly-distros:layer-nerdctl` — the nerdctl engine, the - `layer-nerdctl` candy, and `engine: nerdctl` deploys. + `layer-nerdctl` candy, the rootless checklist, and `engine: nerdctl` deploys. - `/charly-internals:plugin-nerdctl` — the out-of-tree engine plugin. - `/charly-image:layer` — candy authoring reference. - [`opencharly/charly`](https://github.com/opencharly/charly) — the charly CLI and image builder diff --git a/box/check-nerdctl-app/charly.yml b/box/check-nerdctl-app/charly.yml index 221b992..5a768c8 100644 --- a/box/check-nerdctl-app/charly.yml +++ b/box/check-nerdctl-app/charly.yml @@ -7,6 +7,10 @@ check-nerdctl-app: Disposable bed image for the layer-nerdctl R10 — a Fedora base composing layer-nerdctl, so the nested-pod posture is emitted and can be asserted. base: quay.io/fedora/fedora:43 + # The image's distro tag chain. Without it `img.DistroDef` is nil and the generator + # emits NO declarative package installs, so a candy's `distro.fedora.package:` list + # silently does nothing (the tarball still installs, which is how this went unseen). + distro: ["fedora:43", fedora] candy: - layer-nerdctl plan: diff --git a/candy/layer-nerdctl/charly.yml b/candy/layer-nerdctl/charly.yml index 434fa32..150e272 100644 --- a/candy/layer-nerdctl/charly.yml +++ b/candy/layer-nerdctl/charly.yml @@ -1,26 +1,33 @@ # layer-nerdctl — nerdctl (containerd CLI) + the rootless containerd/buildkit/CNI stack. # -# Native on Arcfamily distros (pacman: nerdctl/cni-plugins/rootlesskit/slirp4netns/ -# buildkit/fuse-overlayfs/crun); on non-native distros the pinned, sha256-verified -# nerdctl-full tarball supplies nerdctl + containerd + containerd-fuse-overlayfs-grpc -# + CNI + buildkit + rootlesskit in one archive. +# Native on Arcfamily distros (pacman: nerdctl/containerd/cni-plugins/rootlesskit/ +# slirp4netns/buildkit/fuse-overlayfs/crun/shadow/dbus); on non-native distros the +# pinned, FATALLY sha256-verified nerdctl-full tarball supplies nerdctl + containerd + +# containerd-fuse-overlayfs-grpc + CNI + buildkit + rootlesskit in one archive. # -# Rootless-first: the charly-side user runs containerd under its own user namespace -# with the fuse-overlayfs snapshotter, and a `charly` CNI network is written so a -# nested pod's workloads share the host user's identity (uid-identical sharing). +# Rootless-first: the charly-side user runs containerd under its own user namespace, and +# a `charly` CNI network is written so a nested pod's workloads share the host user's +# identity (uid-identical sharing). The static half of CONTAINERD.md §2/§4 lives here as +# plan steps + checks; the runtime half (linger, the setuptool install, the first start) +# is the `check-nerdctl-rootless` bed's three-step shape. layer-nerdctl: candy: description: | nerdctl (containerd CLI) with the full rootless containerd + buildkit + - CNI stack. Native distro packages on Arch/CachyOS/Omarchy; on Fedora, - Debian and Ubuntu the pinned nerdctl-full tarball (sha256-verified) - supplies nerdctl + containerd + containerd-fuse-overlayfs-grpc + CNI + - buildkit + rootlesskit. Writes /etc/nerdctl/nerdctl.toml, the rootless - containerd/buildkit units, the CNI `charly` network and the cgroup2-rw + - iptables-nft posture for nested use. The BuildKit socket is deliberately NOT + CNI stack. Native distro packages on Arch/CachyOS/Omarchy and Alpine; on + Fedora, Debian and Ubuntu the pinned nerdctl-full tarball — FATALLY + sha256-verified, never an advisory check — supplies nerdctl + containerd + + containerd-fuse-overlayfs-grpc + CNI + buildkit + rootlesskit. Writes + /etc/nerdctl/nerdctl.toml (containerd `namespace = "default"`), the CNI + `charly` network, the rootless containerd/buildkit USER UNITS, and the static + half of the CONTAINERD.md §2/§4 checklist: the newuidmap/newgidmap helpers, + the /etc/subuid + /etc/subgid ranges, the user@.service delegation drop-in, + slirp4netns as the network driver, and the user D-Bus. Linger, the + setuptool run and the units' first start are the runtime half, asserted by + the check-nerdctl-rootless bed. The BuildKit socket is deliberately NOT pinned here: it is a runtime value resolved through the ONE roots resolver - ($XDG_RUNTIME_DIR/charly/buildkitd.sock). ADE checks: nerdctl version, nerdctl - info, the rootless probe, and the fuse-overlayfs snapshotter. + ($XDG_RUNTIME_DIR/charly/buildkitd.sock). ADE checks: nerdctl version, the + fatal checksum, the helpers, the subuid ranges, the units, and the toml. var: NERDCTL_FULL_VERSION: "2.1.2" security: @@ -39,7 +46,11 @@ layer-nerdctl: distro: arch: package: + # nerdctl depends on containerd on Arch, but the units below name + # containerd-rootless.sh's own package contract, so it is declared: + # a distro whose nerdctl does not pull containerd must still get it. - nerdctl + - containerd - cni-plugins - rootlesskit - slirp4netns @@ -47,9 +58,15 @@ layer-nerdctl: - fuse-overlayfs - crun - iptables-nft + # newuidmap/newgidmap: rootlesskit execs them for the UID/GID map + # (CONTAINERD.md §2). The setuptool never checks these two. + - shadow + # The user D-Bus socket the rootless unit requires (Requires=dbus.socket). + - dbus alpine: package: - nerdctl + - containerd - cni-plugins - rootlesskit - slirp4netns @@ -57,8 +74,66 @@ layer-nerdctl: - fuse-overlayfs - crun - iptables + - shadow + - dbus + fedora: + package: + # shadow-utils carries newuidmap/newgidmap; dbus-daemon carries the + # user dbus.socket the rootless unit requires. + - shadow-utils + - dbus-daemon + - slirp4netns + - fuse-overlayfs + debian: + package: + - uidmap + - dbus-user-session + - slirp4netns + - fuse-overlayfs + ubuntu: + package: + - uidmap + - dbus-user-session + - slirp4netns + - fuse-overlayfs plan: - - run: install the pinned nerdctl-full tarball on non-native distros + - run: write the nerdctl-full checksum verifier (one implementation, fatal by construction) + id: nerdctl-full-verify-script + write: /usr/local/libexec/charly/verify-nerdctl-full.sh + run_as: root + mode: "0755" + content: | + #!/bin/sh + # Verify one nerdctl-full archive against a sha256sum file (the release's + # SHA256SUMS, or the one line extracted from it for this archive). + # + # FATAL by construction: no `--ignore-missing` (which makes a filename + # mismatch exit 0) and no `|| true` (which makes a mismatch advisory). + # The archive and the checksum file must share a directory, because the + # checksum line names the archive it was published for. + set -eu + archive="$1" + sums="$2" + if [ ! -s "$sums" ]; then + echo "verify-nerdctl-full: empty or missing checksum file: $sums" >&2 + exit 1 + fi + if [ ! -f "$archive" ]; then + echo "verify-nerdctl-full: missing archive: $archive" >&2 + exit 1 + fi + archive_dir=$(CDPATH= cd -- "$(dirname -- "$archive")" && pwd) + archive_name=$(basename -- "$archive") + sums_name=$(basename -- "$sums") + cd "$archive_dir" + # Require that the checksum file actually names THIS archive: a checksum + # file for a different asset must not pass by naming nothing. + if ! grep -Eq "[[:space:]]\*?${archive_name}$" "$sums_name"; then + echo "verify-nerdctl-full: $sums_name does not name $archive_name" >&2 + exit 1 + fi + exec sha256sum -c "$sums_name" + - run: install the pinned nerdctl-full tarball on non-native distros, with a fatal checksum id: nerdctl-full-tarball command: | set -euo pipefail @@ -67,17 +142,72 @@ layer-nerdctl: esac ver="${NERDCTL_FULL_VERSION}" arch="${ARCH}" - url="https://github.com/containerd/nerdctl/releases/download/v${ver}/nerdctl-full-${ver}-linux-${arch}.tar.gz" - curl -fsSL "${url}" -o /tmp/nerdctl-full.tar.gz - curl -fsSL "${url}.sha256sum" -o /tmp/nerdctl-full.sha256 || true - if [ -s /tmp/nerdctl-full.sha256 ]; then - ( cd /tmp && sha256sum -c nerdctl-full.sha256 --ignore-missing 2>/dev/null || true ) + tarball="nerdctl-full-${ver}-linux-${arch}.tar.gz" + release="https://github.com/containerd/nerdctl/releases/download/v${ver}" + url="${release}/${tarball}" + work="$(mktemp -d /tmp/nerdctl-full.XXXXXX)" + # The release publishes ONE SHA256SUMS file, not per-asset .sha256sum files + # (a `${url}.sha256sum` fetch is a 404, which is why the old step's `|| true` + # made the whole check vacuous). Extract the ONE line for this archive and + # verify against it; an absent line leaves an empty file, which the verifier + # rejects. + curl -fsSL "${release}/SHA256SUMS" -o "${work}/SHA256SUMS" + grep -E "[[:space:]]${tarball}$" "${work}/SHA256SUMS" > "${work}/${tarball}.sha256sum" + curl -fsSL "${url}" -o "${work}/${tarball}" + /usr/local/libexec/charly/verify-nerdctl-full.sh "${work}/${tarball}" "${work}/${tarball}.sha256sum" + tar -C /usr/local -xzf "${work}/${tarball}" + rm -rf "${work}" + run_as: root + - run: delegate the uid/gid ranges rootlesskit maps through newuidmap/newgidmap + id: rootless-subuid + run_as: root + command: | + set -eu + # CONTAINERD.md §2: `user:100000:65536` in /etc/subuid AND /etc/subgid. + # Resolve the image user from its UID rather than hardcoding a name: the + # user is `user` on some distros and `arch` on the cloud images. + u="$(getent passwd 1000 | cut -d: -f1)" + if [ -z "$u" ]; then + echo "layer-nerdctl: no uid-1000 user to delegate the rootless ranges to" >&2 + exit 1 fi - tar -C /usr/local -xzf /tmp/nerdctl-full.tar.gz - rm -f /tmp/nerdctl-full.tar.gz /tmp/nerdctl-full.sha256 + for f in /etc/subuid /etc/subgid; do + touch "$f" + if ! grep -qx "${u}:100000:65536" "$f"; then + printf '%s:100000:65536\n' "$u" >> "$f" + fi + done + - run: delegate the cgroup controllers resource limits need to the user manager + id: rootless-delegate-controllers + write: /etc/systemd/system/user@.service.d/delegate.conf run_as: root - cache: - - /var/cache/charly-nerdctl-full + mode: "0644" + content: | + # Managed by the opencharly layer-nerdctl candy (CONTAINERD.md §2). + [Service] + Delegate=cpu cpuset io memory pids + - run: expose the CNI plugins at the ONE cni_path the toml declares + id: rootless-cni-path + run_as: root + command: | + set -eu + # nerdctl.toml names /opt/cni/bin. Arch's cni-plugins package already + # installs there; the nerdctl-full tarball extracts to + # /usr/local/libexec/cni, and a distro package may use /usr/lib/cni or + # /usr/libexec/cni. Link every real plugin to the ONE declared path + # instead of writing a distro-specific cni_path (one spelling). + mkdir -p /opt/cni/bin + for d in /usr/local/libexec/cni /usr/lib/cni /usr/libexec/cni; do + [ -d "$d" ] || continue + for f in "$d"/*; do + [ -f "$f" ] || continue + b=$(basename "$f") + [ -e "/opt/cni/bin/$b" ] || ln -s "$f" "/opt/cni/bin/$b" + done + done + for p in bridge portmap firewall tuning host-local; do + [ -x "/opt/cni/bin/$p" ] || { echo "missing CNI plugin $p at /opt/cni/bin" >&2; exit 1; } + done - run: create the nerdctl + containerd config directories mkdir: /etc/nerdctl run_as: root @@ -86,13 +216,16 @@ layer-nerdctl: mkdir: /etc/cni/net.d run_as: root mode: "0755" - - run: write the nerdctl.toml (rootless namespace + CNI path) + - run: write the nerdctl.toml (default containerd namespace + CNI path) write: /etc/nerdctl/nerdctl.toml run_as: root mode: "0644" content: | # Managed by the opencharly layer-nerdctl candy. # + # `namespace = "default"` is the ONE spelling the programme uses. The + # retired `charly` value is gone: one namespace, no per-layer name. + # # No `buildkit_host` here, deliberately. This file is a STATIC image-build # artefact, and the BuildKit socket is a RUNTIME value: charly resolves it # through the ONE roots resolver — buildkitdrive.SocketPath() over @@ -100,7 +233,7 @@ layer-nerdctl: # A literal here could only hardcode one uid, and the deviation this drops is # the exact one buildkitdrive/paths.go names: "it breaks on any uid other than # 1000, and two conventions for one socket is one too many." - namespace = "charly" + namespace = "default" cni_path = "/opt/cni/bin" - run: write the charly CNI network conflist write: /etc/cni/net.d/charly.conflist @@ -117,14 +250,85 @@ layer-nerdctl: { "type": "tuning" } ] } + - run: write the rootless containerd user unit + id: rootless-containerd-unit + write: ${HOME}/.config/systemd/user/containerd.service + run_as: "${USER}" + mode: "0644" + content: | + # Managed by the opencharly layer-nerdctl candy. + # + # The rootless containerd unit, from containerd-rootless-setuptool.sh + # install's own template, with one deliberate difference: ExecStart goes + # through /usr/bin/env so the SAME unit works whether the wrapper is at + # /usr/local/bin (nerdctl-full on Fedora/Debian/Ubuntu) or /usr/bin (the + # Arch/Alpine packages). + # + # CONTAINERD_ROOTLESS_ROOTLESSKIT_NET pins slirp4netns, the documented + # default (CONTAINERD.md §4.3). bypass4netns and pasta are OPTIONS + # documented in the skill; they are not set here. + [Unit] + Description=containerd (Rootless) + Requires=dbus.socket + + [Service] + Environment=PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin + Environment=CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns + Environment=CONTAINERD_ROOTLESS_ROOTLESSKIT_FLAGS= + ExecStart=/usr/bin/env containerd-rootless.sh + ExecReload=/bin/kill -s HUP $MAINPID + TimeoutSec=0 + RestartSec=2 + Restart=always + StartLimitBurst=3 + StartLimitInterval=60s + LimitNOFILE=infinity + LimitNPROC=infinity + LimitCORE=infinity + TasksMax=infinity + Delegate=yes + Type=simple + KillMode=mixed + + [Install] + WantedBy=default.target + - run: write the rootless buildkit user unit + id: rootless-buildkit-unit + write: ${HOME}/.config/systemd/user/buildkit.service + run_as: "${USER}" + mode: "0644" + content: | + # Managed by the opencharly layer-nerdctl candy. + # + # The rootless BuildKit unit, from install-buildkit's own template: it + # enters containerd's rootless user/mount/net namespace through the + # setuptool's nsenter verb. + [Unit] + Description=BuildKit (Rootless) + PartOf=containerd.service + + [Service] + Environment=PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin + ExecStart=/usr/bin/env containerd-rootless-setuptool.sh nsenter -- buildkitd --oci-worker=true --oci-worker-rootless=true --containerd-worker=false --oci-worker-net=bridge + ExecReload=/bin/kill -s HUP $MAINPID + RestartSec=2 + Restart=always + Type=simple + KillMode=mixed + + [Install] + WantedBy=default.target - check: nerdctl version exits 0 command: nerdctl version exit_status: 0 - check: the nerdctl binary is present on PATH command: command -v nerdctl exit_status: 0 - - check: the nerdctl.toml declares the charly namespace - command: grep -q 'namespace = "charly"' /etc/nerdctl/nerdctl.toml + - check: the nerdctl.toml declares the default containerd namespace + command: grep -qx 'namespace = "default"' /etc/nerdctl/nerdctl.toml + exit_status: 0 + - check: the retired charly namespace is gone from nerdctl.toml (one spelling only) + command: '! grep -qE "^[[:space:]]*namespace[[:space:]]*=[[:space:]]*\"charly\"" /etc/nerdctl/nerdctl.toml' exit_status: 0 - check: the toml carries NO uid-pinned buildkit_host KEY (the resolver owns that slot) # The pattern is ANCHORED to a key assignment, not the bare substring: the file @@ -134,6 +338,78 @@ layer-nerdctl: exit_status: 0 - check: the charly CNI network conflist is present file: /etc/cni/net.d/charly.conflist + - check: the CNI plugins the conflist names resolve at the toml's cni_path + command: | + for p in bridge portmap firewall tuning host-local; do + [ -x "/opt/cni/bin/$p" ] || { echo "missing CNI plugin $p at /opt/cni/bin" >&2; exit 1; } + done + exit_status: 0 + - check: the uidmap helpers rootlesskit execs are on PATH (the setuptool never checks them) + command: | + p=$(command -v newuidmap) && q=$(command -v newgidmap) + [ -n "$p" ] && [ -n "$q" ] + exit_status: 0 + - check: /etc/subuid and /etc/subgid delegate the uid-1000 user's rootless range + command: | + u=$(getent passwd 1000 | cut -d: -f1) + grep -qx "${u}:100000:65536" /etc/subuid + grep -qx "${u}:100000:65536" /etc/subgid + exit_status: 0 + - check: the user manager carries the delegation drop-in for cpuset and io + command: grep -qx 'Delegate=cpu cpuset io memory pids' /etc/systemd/system/user@.service.d/delegate.conf + exit_status: 0 + - check: the user D-Bus the rootless unit requires is installed + command: command -v dbus-daemon + exit_status: 0 + - check: slirp4netns, the rootless network driver, is installed + command: command -v slirp4netns + exit_status: 0 + - check: the rootless wrapper and setuptool are installed + command: | + a=$(command -v containerd-rootless.sh) && b=$(command -v containerd-rootless-setuptool.sh) + [ -n "$a" ] && [ -n "$b" ] + exit_status: 0 + - check: the candy wrote the rootless containerd and buildkit user units + command: | + home=$(getent passwd 1000 | cut -d: -f6) + for u in containerd.service buildkit.service; do + f="${home}/.config/systemd/user/${u}" + [ -f "$f" ] || { echo "missing ${f}" >&2; exit 1; } + grep -qx '# Managed by the opencharly layer-nerdctl candy.' "$f" || { + echo "${f} is not the candy-written unit" >&2; exit 1; } + done + exit_status: 0 + - check: the written user units parse as systemd units (N/A on a non-systemd distro) + command: | + if ! command -v systemd-analyze >/dev/null 2>&1; then + echo "N/A: this distro has no systemd, so the systemd-only rootless instance does not apply" + exit 0 + fi + home=$(getent passwd 1000 | cut -d: -f6) + systemd-analyze --user verify "${home}/.config/systemd/user/containerd.service" "${home}/.config/systemd/user/buildkit.service" + exit_status: 0 + - check: the checksum verifier REJECTS a corrupted archive (a test that can fail) + command: | + set -eu + d=$(mktemp -d) + trap 'rm -rf "$d"' EXIT + printf 'payload' > "$d/asset.tar.gz" + ( cd "$d" && sha256sum asset.tar.gz > asset.tar.gz.sha256sum ) + /usr/local/libexec/charly/verify-nerdctl-full.sh "$d/asset.tar.gz" "$d/asset.tar.gz.sha256sum" >/dev/null + printf 'corrupt' > "$d/asset.tar.gz" + if /usr/local/libexec/charly/verify-nerdctl-full.sh "$d/asset.tar.gz" "$d/asset.tar.gz.sha256sum" >/dev/null 2>&1; then + echo "a corrupted archive passed the checksum verification" >&2 + exit 1 + fi + exit_status: 0 + - check: /dev/fuse is a character device that opens for the fuse-overlayfs fallback + context: [runtime] + command: | + set -eu + [ -c /dev/fuse ] || { echo "/dev/fuse is not a character device" >&2; exit 1; } + exec 3<>/dev/fuse + exec 3>&- + exit_status: 0 # ---- HARNESS SKILL ---- layer-nerdctl-skill: @@ -141,4 +417,4 @@ layer-nerdctl-skill: name: layer-nerdctl family: distros owner: layer-nerdctl - description: "nerdctl (containerd CLI) + the rootless containerd/buildkit/CNI stack — native Arch/Alpine packages, the pinned sha256 nerdctl-full tarball on Fedora/Debian/Ubuntu, nerdctl.toml + the charly CNI conflist + the nested-pod posture. Use when working with the nerdctl engine, the layer-nerdctl candy, or engine: nerdctl deploys." + description: "nerdctl (containerd CLI) + the rootless containerd/buildkit/CNI stack — native Arch/Alpine packages, the pinned FATALLY sha256-verified nerdctl-full tarball on Fedora/Debian/Ubuntu, nerdctl.toml with namespace=default, the charly CNI conflist, the written rootless containerd/buildkit user units, the CONTAINERD.md §2/§4 checklist (uidmap helpers, subuid/subgid, delegation, /dev/fuse, user D-Bus, slirp4netns), and the nested-pod posture. Use when working with the nerdctl engine, the layer-nerdctl candy, or engine: nerdctl deploys." diff --git a/charly.yml b/charly.yml index 88e0375..d5dfedd 100644 --- a/charly.yml +++ b/charly.yml @@ -53,3 +53,253 @@ check-nerdctl-nested: command: command -v nerdctl exit_status: 0 +# ===== The rootless checklist bed (CONTAINERD-IMAGES.md §1.2; CONTAINERD.md §2 + §4) ===== +# +# The runtime half of the §4 checklist needs a real systemd. A pod runs supervisord, so a +# `systemctl --user` assertion there is meaningless: this bed is a DISPOSABLE VM, the +# substrate a rootless host instance actually belongs to. The candy supplies the static +# half (helpers, subuid/subgid, delegation drop-in, D-Bus, slirp4netns, the units); the +# plan runs the upstream setuptool over those units and asserts the running result. +# +# The candy is applied by the VM deploy's own `add_candy:`, NOT a nested `local:` member: +# a non-namespaced bed's nested member ref is mis-converted by the plugin-check pin this +# charly carries (`bed.key` -> `bed/key`), and `add_candy` is the substrate's own path. +nerdctl-rootless-vm-base: + vm: + source: + # A Fedora cloud guest, not Arch: this layer's non-native arm installs the + # pinned nerdctl-full tarball (and the CNI plugins from /usr/local/libexec/cni), + # so the bed proves that path end to end. A dnf install also keeps the running + # kernel's module tree, so no pacman IgnorePkg hold (and its "ignoring package + # upgrade" warning) is needed. + kind: cloud_image + url: https://download.fedoraproject.org/pub/fedora/linux/releases/43/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2 + base_user: fedora + distro: fedora + backend: libvirt + disk_size: 20G + ram: 4G + cpu: 2 + machine: q35 + firmware: bios + ssh: + port_auto: true + key_source: generate + cloud_init: + timezone: UTC + package: + - sudo + +check-nerdctl-rootless: + vm: + from: nerdctl-rootless-vm-base + disposable: true + lifecycle: test + # The candy is applied by the deploy substrate itself, so the units it writes are + # in place before the plan's ABSENCE step runs and nothing is started yet. + add_candy: + - layer-nerdctl + description: | + The §4 rootless checklist on a disposable guest, in the three-step shape: + ABSENCE (no rootless containerd is running and the units are inactive), + PERFORM (daemon-reload, linger, and the upstream setuptool install over the + candy-written units), PRESENCE (`nerdctl info` names rootless, the uidmap + helpers resolve, /dev/fuse opens, and the units are active). The system + socket's inode/mtime is recorded before the daemon starts and compared after, + and the rootless daemon's real listen address is asserted to be the user + socket, never the system one. + plan: + # ===== STEP 1: ABSENCE — the control the presence step needs to mean anything ===== + # The VM deploy's add_candy applied the candy but did not start anything: this + # asserts the pre-setup state, and it is idempotent because it stops the units + # first (the plan runs at deploy-add AND at check-live). + - check: no rootless containerd is active before the setup, and the system socket is recorded + id: rootless-absent-before-setup + context: [runtime] + command: | + set -eu + systemctl --user stop buildkit.service containerd.service + if systemctl --user is-active --quiet containerd.service; then + echo "containerd.service is already active before the setup step" >&2; exit 1 + fi + if systemctl --user is-active --quiet buildkit.service; then + echo "buildkit.service is already active before the setup step" >&2; exit 1 + fi + # DAEMON SAFETY, recorded BEFORE any daemon starts: the venue's ROOTFUL + # system listeners. A test daemon that binds the default socket replaces + # them; the presence step compares these exact values. `sudo` is used + # because /run/containerd is root-only: an unreadable file must not read + # as "absent". + sys=/run/containerd/containerd.sock + { + sudo -n stat -Lc '%d:%i %Y' "$sys" 2>/dev/null || echo absent + sudo -n stat -Lc '%d:%i %Y' "$sys.ttrpc" 2>/dev/null || echo absent + } > /tmp/nerdctl-rootless-system-socket.before + cat /tmp/nerdctl-rootless-system-socket.before + if nerdctl info >/dev/null 2>&1; then + echo "a containerd daemon already answers before the setup step" >&2; exit 1 + fi + echo "absence confirmed: no rootless containerd, no answering client" + # ===== STEP 2: PERFORM — the settings the setuptool needs, then the setuptool ===== + - check: the setup performs — daemon-reload, linger, and the upstream setuptool install + id: rootless-perform-setup + context: [runtime] + command: | + set -u + fail() { echo "perform FAILED: $*" >&2; exit 1; } + # Bounded readiness, with a deadline and a real failure: never a blind + # retry. The setuptool's own install gives a unit three seconds, which a + # cold guest can exceed (measured on the Fedora venue: the unit came up + # after the setuptool had already given up). The unit is the candy's, so + # start it through systemd and wait for the CLIENT to answer. + wait_active() { + unit="$1" + i=0 + while ! systemctl --user is-active --quiet "$unit"; do + i=$((i+1)) + [ "$i" -le 150 ] || { + systemctl --user status "$unit" --no-pager 2>&1 | head -20 >&2 + journalctl --user -u "$unit" -n 30 --no-pager 2>&1 | tail -15 >&2 + fail "$unit did not become active"; } + sleep 0.2 + done + } + # 1. Apply the candy-written user@.service delegation drop-in (CONTAINERD.md §2). + sudo -n systemctl daemon-reload || fail "system daemon-reload failed" + # 2. Linger, so the user manager and the units it owns survive the session + # that installs them (CONTAINERD.md §4.1). + sudo -n loginctl enable-linger "$(id -un)" || fail "loginctl enable-linger failed" + systemctl --user daemon-reload || fail "user daemon-reload failed" + # 3. Start the candy-written units and wait for the daemon to ANSWER. + systemctl --user start containerd.service || fail "could not start containerd.service" + wait_active containerd.service + i=0 + while ! nerdctl info >/dev/null 2>&1; do + i=$((i+1)) + [ "$i" -le 150 ] || fail "containerd is active but nerdctl info does not answer" + sleep 0.2 + done + systemctl --user start buildkit.service || fail "could not start buildkit.service" + wait_active buildkit.service + # 4. The upstream installer, over the units it would otherwise write. It + # finds them and skips writing ("File already exists, skipping"), sees + # them active, and enables them; install-buildkit is the second half. + containerd-rootless-setuptool.sh install || fail "containerd-rootless-setuptool.sh install failed" + containerd-rootless-setuptool.sh install-buildkit || fail "install-buildkit failed" + echo "perform complete" + # ===== STEP 3: PRESENCE — assertions only the change can produce ===== + - check: nerdctl reports the rootless security option, the units are active, and the helpers resolve + id: rootless-presence + context: [runtime] + command: | + set -u + # Every assertion below names itself on failure: a bare exit status is + # not a diagnosis (the first run's failure was one line of stderr). + fail() { echo "presence FAILED: $*" >&2; exit 1; } + d=$(mktemp -d) + trap 'rm -rf "$d"' EXIT + + say() { echo "presence: $*"; } + + say "units are active" + # Bounded readiness with a deadline, not a blind retry: the fresh-update + # phase re-extracts the tarball over a RUNNING buildkitd, so the unit can + # be mid-restart for a moment. A unit that never comes back is reported + # with its status and journal. + wait_unit() { + unit="$1" + i=0 + while ! systemctl --user is-active --quiet "$unit"; do + i=$((i+1)) + [ "$i" -le 100 ] || { + systemctl --user status "$unit" --no-pager 2>&1 | head -25 >&2 + journalctl --user -u "$unit" -n 40 --no-pager 2>&1 | tail -20 >&2 + fail "$unit did not become active"; } + sleep 0.2 + done + } + wait_unit containerd.service + wait_unit buildkit.service + # The candy wrote these units and the setuptool skipped them; the marker + # distinguishes the layer's units from a setuptool-written pair. + for u in containerd.service buildkit.service; do + grep -qx '# Managed by the opencharly layer-nerdctl candy.' "$HOME/.config/systemd/user/$u" \ + || fail "$u is not the candy-written unit" + done + + say "nerdctl reports the rootless security option" + # nerdctl 2.4 prints a bare `rootless` line in the Security Options block; + # older builds print `name=rootless`. Accept both — the assertion is the + # option, not one release's spelling. + nerdctl info > "$d/info.txt" 2>&1 || { cat "$d/info.txt" >&2; fail "nerdctl info failed"; } + sed -n '/Security Options:/,/^[A-Z]/p' "$d/info.txt" | grep -Eq '^[[:space:]]+(name=)?rootless$' \ + || { cat "$d/info.txt" >&2; fail "SecurityOptions does not report rootless"; } + nerdctl info | grep -i 'storage driver' | grep -qi overlay \ + || fail "the storage driver is not overlay" + + say "the uidmap helpers resolve and a UID/GID map succeeds" + command -v newuidmap >/dev/null || fail "newuidmap is not on PATH" + command -v newgidmap >/dev/null || fail "newgidmap is not on PATH" + # The exact A/B CONTAINERD.md §2 measured: rootlesskit execs the helpers. + if ! out=$(rootlesskit --state-dir="$d/rl" true 2>&1); then + echo "$out" >&2; fail "rootlesskit could not set up the UID/GID map" + fi + + say "/dev/fuse opens" + [ -c /dev/fuse ] || fail "/dev/fuse is not a character device" + exec 3<>/dev/fuse || fail "cannot open /dev/fuse" + exec 3>&- + + say "user D-Bus and the slirp4netns driver" + systemctl --user is-active dbus.socket >/dev/null || { + systemctl --user status dbus.socket --no-pager 2>&1 | head -20 >&2 + fail "the user dbus.socket is not active"; } + envline=$(systemctl --user show containerd.service -p Environment 2>&1) \ + || fail "systemctl show containerd.service failed" + printf '%s\n' "$envline" | grep -q 'CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns' \ + || { printf '%s\n' "$envline" >&2; fail "the unit does not pin slirp4netns"; } + ps -ww -eo args | grep -q 'rootlesskit .*--net=slirp4netns' \ + || { ps -ww -eo args | grep rootlesskit >&2; fail "no rootlesskit process uses slirp4netns"; } + + say "daemon safety: the real listen address is the USER socket" + # The rootless containerd binds /run/containerd/containerd.sock INSIDE its + # mount namespace, where /run/containerd is the user's directory; `ss -lx` + # therefore shows that namespace path. Prove the object it names is the + # USER socket, not the host's rootful one, by device:inode across + # /proc//root. + [ -S "$XDG_RUNTIME_DIR/containerd/containerd.sock" ] \ + || fail "no user containerd socket at $XDG_RUNTIME_DIR/containerd/containerd.sock" + pid=$(pgrep -u "$(id -u)" -x containerd | head -1) + [ -n "$pid" ] || { ps -ww -eo pid,user,args | grep containerd >&2; fail "no containerd process for uid $(id -u)"; } + user_obj=$(stat -Lc '%d:%i' "$XDG_RUNTIME_DIR/containerd/containerd.sock") \ + || fail "cannot stat the user containerd socket" + ns_obj=$(stat -Lc '%d:%i' "/proc/$pid/root/run/containerd/containerd.sock") \ + || fail "cannot stat the daemon's namespace socket" + [ "$user_obj" = "$ns_obj" ] \ + || fail "the daemon's namespace socket ($ns_obj) is not the user socket ($user_obj)" + ss -lx | grep -F '/run/containerd/containerd.sock' >/dev/null \ + || { ss -lx >&2; fail "the rootless daemon has no listen entry"; } + sys=/run/containerd/containerd.sock + if sudo -n test -e "$sys"; then + sys_obj=$(sudo -n stat -Lc '%d:%i' "$sys") + [ "$sys_obj" != "$user_obj" ] \ + || fail "the rootless daemon owns the HOST system socket ($sys_obj)" + fi + after="$( { sudo -n stat -Lc '%d:%i %Y' "$sys" 2>/dev/null || echo absent; sudo -n stat -Lc '%d:%i %Y' "$sys.ttrpc" 2>/dev/null || echo absent; } )" + before="$(cat /tmp/nerdctl-rootless-system-socket.before)" + [ "$before" = "$after" ] \ + || fail "the system socket changed while the rootless daemon ran: before=$before after=$after" + if [ "$after" = "absent absent" ]; then + echo "system socket: absent before and after (the rootless daemon never bound it)" + else + echo "system socket inode/mtime unchanged: $after" + fi + + say "a real container runs under the delegated manager" + # --net=host is the ROOTLESSKIT netns (slirp4netns): the engine's runtime + # path is what this assertion proves, and it keeps the probe independent of + # the guest kernel's netfilter module tree (the CNI path is asserted where + # it belongs: the conflist check and the plugins-at-cni_path check). + runout=$(timeout 300 nerdctl run --rm --net=host --memory 64m alpine echo ok 2>&1) || { + echo "$runout" >&2; fail "nerdctl run failed"; } + printf '%s\n' "$runout" | grep -qx ok || { printf '%s\n' "$runout" >&2; fail "nerdctl run did not print ok"; }