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); }