Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libbsd0 libgps28 libasound2 libjack-jackd2-0 libpulse0 \
direwolf \
ca-certificates \
libcap2-bin \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/* /var/log/*

# binaries
Expand All @@ -89,16 +88,15 @@ EXPOSE 8080 8001 8002
# A non-root user for the running process.
RUN useradd --system --no-create-home --shell /usr/sbin/nologin sim

# -rt-priority renices the router and its TNC children, which needs
# CAP_SYS_NICE — but the container runs as the non-root `sim` user, and
# docker's --cap-add only populates the *bounding* set (a non-root process
# doesn't inherit it). File capabilities on the binaries grant it to the
# process directly; still inert unless the container ALSO gets
# --cap-add SYS_NICE (file caps can't exceed the bounding set), and the
# binaries only use it when -rt-priority is passed.
RUN setcap cap_sys_nice+ep /usr/local/bin/sim-web \
&& setcap cap_sys_nice+ep /usr/local/bin/sim-router

# NOTE on -rt-priority: it renices the router + TNC children, which needs
# CAP_SYS_NICE. Grant it at RUNTIME with `--cap-add SYS_NICE` (and run the
# container as root, or with that cap in the user-namespace) — NOT with file
# capabilities on the binaries. A `setcap cap_sys_nice+ep` here makes the
# binary REFUSE TO exec (EPERM "operation not permitted") on any host whose
# bounding set lacks the cap — e.g. an unprivileged LXC — breaking the image
# for everyone who isn't using -rt-priority. -rt-priority already degrades
# gracefully (logs a one-line warning and runs at normal priority) where the
# platform won't grant the cap, so the binary must stay capability-free.
USER sim

ENTRYPOINT ["/usr/local/bin/sim-web", "-addr", ":8080", "-config", "/etc/sim/network.yaml"]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ services:
cap_add: [SYS_NICE]
```

The capability is granted at **runtime** (`--cap-add` / `cap_add`), deliberately — the image carries no file capabilities on its binaries. (An earlier build set `cap_sys_nice+ep` on `sim-web`/`sim-router`; that made them refuse to `exec` with *"operation not permitted"* on any host whose bounding set lacks the cap — e.g. an unprivileged LXC — breaking the image even for runs that never pass `-rt-priority`. Runtime cap-add is the correct mechanism.)

> **Nested-container hosts:** on a host that is itself an unprivileged container (e.g. Docker inside an unprivileged Proxmox LXC), the kernel checks CAP_SYS_NICE against the *init* user namespace, so negative nice is unavailable to anything inside — even container root. The flag then logs its one-line warning and the sim runs at normal priority; everything else is unaffected.
> **Nested-container hosts:** on a host that is itself an unprivileged container (e.g. Docker inside an unprivileged Proxmox LXC), the kernel checks CAP_SYS_NICE against the *init* user namespace, so negative nice is unavailable to anything inside — even container root, even with `cap_add`. The flag then logs its one-line warning and the sim runs at normal priority; everything else is unaffected.

## Quick install (curl | sudo bash)

Expand Down
52 changes: 52 additions & 0 deletions internal/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package dockerfile

// Guards against reintroducing file capabilities on the shipped binaries.
//
// A `setcap cap_sys_nice+ep` on sim-web/sim-router makes them refuse to exec
// ("operation not permitted") on any host whose capability bounding set lacks
// CAP_SYS_NICE — e.g. an unprivileged LXC — which broke the image for every
// run, including those that never pass -rt-priority. The capability must be
// granted at runtime (--cap-add SYS_NICE) instead, so the binaries stay
// capability-free. This caught a real production breakage (packethacking/net-sim
// — the lab net-sim crash-looped after the setcap landed).

import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestDockerfileHasNoSetcapOnBinaries(t *testing.T) {
// Walk up to the repo root (where the Dockerfile lives).
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
var dockerfile string
for d := wd; d != "/" && d != "."; d = filepath.Dir(d) {
p := filepath.Join(d, "Dockerfile")
if _, err := os.Stat(p); err == nil {
dockerfile = p
break
}
}
if dockerfile == "" {
t.Fatal("could not locate Dockerfile from " + wd)
}
b, err := os.ReadFile(dockerfile)
if err != nil {
t.Fatal(err)
}
// Allow the word inside a comment explaining why we don't do it; only fail
// on an actual RUN setcap ... line.
for _, line := range strings.Split(string(b), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") {
continue
}
if strings.Contains(trimmed, "setcap") {
t.Fatalf("Dockerfile reintroduced file capabilities (%q) — these break exec on hosts lacking the cap in their bounding set; grant CAP_SYS_NICE at runtime via --cap-add instead", trimmed)
}
}
}
Loading