From 58b667eafe87fdcd2a551863e7ee4842a4b4658d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 19:52:22 +0000 Subject: [PATCH] =?UTF-8?q?1.0.3=20=E2=80=94=20stack=20DNS=20dirs=20per-us?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/stack.cpp's per-stack unbound config + pidfile directory now resolves to /var/run/crate//dns-/ when the privops socket is detected. Same lazy-resolve pattern as network_lease.cpp (0.9.27). Before this fix, two operators bringing up stacks with the same network name clobbered each other's unbound.conf, pidfile, and SIGTERM target. After: each operator's DNS state lives in their own per-uid subtree. Wire/format/signatures unchanged. Suite stays at 1303. --- CHANGELOG.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ cli/args.cpp | 4 +-- lib/stack.cpp | 28 +++++++++++++++--- 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d317de..fee9bec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,84 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [1.0.3] — 2026-05-12 + +**Stack DNS dirs per-user.** Third patch release of the 1.x +line. The per-stack unbound config + pidfile directory now +resolves to `/var/run/crate//dns-/` when the +privops socket is detected. + +### What changes + +`lib/stack.cpp` gains a `dnsBaseDir()` static helper that +lazy-resolves the base directory for per-stack DNS state: + +| Mode | Path | +|-------------------------------|--------------------------------------| +| Legacy (no `crated`) | `/var/run/crate/dns-/` | +| Rootless (crated + privops) | `/var/run/crate//dns-/`| + +Four sites updated: + +- `generateUnboundConf()` — pidfile path inside the rendered + unbound config (named + default cases) +- `startStackDns()` — `mkdir` target + path passed to + `unbound -c ` +- `stopStackDns()` — `remove_all` cleanup path + +Net effect: when alice and bob each run a stack with a +network named `db`, their unbound instances no longer fight +over `/var/run/crate/dns-db/unbound.pid`. + +### Why this matters + +Before this release, every operator on a rootless host shared +the same DNS config directory. Two operators bringing up +stacks with the same network name would clobber each other's +unbound.conf and pidfile. The unbound process started first +held the pidfile; the second `crate stack up` would either +silently overwrite the conf and not restart unbound, or +deliver SIGTERM to the wrong process at teardown. + +### 1.x backlog + +Remaining latent per-user path leaks: + +- `lib/vm_run.cpp` VM + cloud-init paths hardcoded +- `lib/run_net.cpp:446` direct `ifconfig -vnet` (should use + existing `SetIfaceUp` privops verb) + +Reclassified out of "latent path leak" into a separate 1.1.0 +work item (real bug, but bigger fix): + +- **PfctlOps privops-wiring**: `crate(1)` calls + `PfctlOps::addRules` / `loadContainerPolicy` / `flushRules` + directly from `lib/run.cpp`. Without setuid, the + non-root operator cannot open `/dev/pf` nor the host-wide + `/var/run/crate/pfctl.lock`. The original audit suggested + per-user lock but that's incorrect — pf is host-wide, the + lock must serialize across operators. The right fix is to + route the three call sites through the existing `AddPfRule` + privops verb (and add `FlushPfAnchor` / `LoadPfPolicy` + verbs if needed). 1.1.0 will cover this. + +### Tests + +No new tests — the change is a single helper + 4 string +substitutions. The `dnsBaseDir()` cache uses the same lazy- +resolve pattern proven in `network_lease.cpp`. Suite stays +at 1303. + +### Files + +- `lib/stack.cpp` — `dnsBaseDir()` helper, 4 call sites + routed through it; new includes for `privops_client.h` and + `runtime_paths_pure.h` +- `cli/args.cpp` — version `crate 1.0.3` +- `CHANGELOG.md` — this entry + +--- + ## [1.0.2] — 2026-05-12 **Spec registry per-user + restart wires through it.** Second diff --git a/cli/args.cpp b/cli/args.cpp index ddb7e75..d99346e 100644 --- a/cli/args.cpp +++ b/cli/args.cpp @@ -753,7 +753,7 @@ Args parseArguments(int argc, char** argv, unsigned &processed) { args.noColor = true; break; } else if (strEq(argv[a], "--version")) { - std::cout << "crate 1.0.2" << std::endl; + std::cout << "crate 1.0.3" << std::endl; exit(0); } else if (auto argShort = isShort(argv[a])) { switch (argShort) { @@ -764,7 +764,7 @@ Args parseArguments(int argc, char** argv, unsigned &processed) { args.logProgress = true; break; case 'V': - std::cout << "crate 1.0.2" << std::endl; + std::cout << "crate 1.0.3" << std::endl; exit(0); default: err("unsupported short option '%s'", argv[a]); diff --git a/lib/stack.cpp b/lib/stack.cpp index 0694f58..3faddab 100644 --- a/lib/stack.cpp +++ b/lib/stack.cpp @@ -9,6 +9,8 @@ #include "ipfw_ops.h" #include "net.h" #include "pathnames.h" +#include "privops_client.h" +#include "runtime_paths_pure.h" #include "stack_pure.h" #include @@ -183,6 +185,24 @@ static std::map allocateIpPool( // --- Per-Stack DNS Service (§27.1) --- +// Resolve the base directory for per-stack unbound config + pidfile. +// Legacy: /var/run/crate (shared across operators). +// Rootless (1.0.3+): /var/run/crate/ when crated's privops socket +// is detected, so two operators on the same host don't fight over +// the same dns- subdir or pidfile path. +static const std::string &dnsBaseDir() { + static std::string cached; + static bool computed = false; + if (!computed) { + if (!PrivOpsClient::detectSocketPath().empty()) + cached = RuntimePathsPure::perUserRoot((uint32_t)::getuid()); + else + cached = "/var/run/crate"; + computed = true; + } + return cached; +} + // Generate unbound configuration for a stack's DNS service static std::string generateUnboundConf( const std::string &listenIp, @@ -198,9 +218,9 @@ static std::string generateUnboundConf( conf << " do-daemonize: yes\n"; // Use per-stack pidfile to avoid clashes when multiple stacks run DNS if (!networkName.empty()) - conf << " pidfile: \"/var/run/crate/dns-" << networkName << "/unbound.pid\"\n"; + conf << " pidfile: \"" << dnsBaseDir() << "/dns-" << networkName << "/unbound.pid\"\n"; else - conf << " pidfile: \"/var/run/crate/dns-default/unbound.pid\"\n"; + conf << " pidfile: \"" << dnsBaseDir() << "/dns-default/unbound.pid\"\n"; conf << " access-control: 127.0.0.0/8 allow\n"; if (!subnet.empty()) conf << " access-control: " << subnet << " allow\n"; @@ -236,7 +256,7 @@ static pid_t startStackDns( // Get upstream DNS for forwarding auto upstreamDns = Net::getNameserverIp(); - auto confDir = STR("/var/run/crate/dns-" << network.name); + auto confDir = STR(dnsBaseDir() << "/dns-" << network.name); std::filesystem::create_directories(confDir); auto confPath = STR(confDir << "/unbound.conf"); @@ -278,7 +298,7 @@ static void stopStackDns(const std::string &networkName, pid_t unboundPid) { ::waitpid(unboundPid, &status, 0); } // Clean up config directory - auto confDir = STR("/var/run/crate/dns-" << networkName); + auto confDir = STR(dnsBaseDir() << "/dns-" << networkName); std::filesystem::remove_all(confDir); }