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
78 changes: 78 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<uid>/dns-<network>/` 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-<network>/` |
| Rootless (crated + privops) | `/var/run/crate/<uid>/dns-<network>/`|

Four sites updated:

- `generateUnboundConf()` — pidfile path inside the rendered
unbound config (named + default cases)
- `startStackDns()` — `mkdir` target + path passed to
`unbound -c <conf>`
- `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
Expand Down
4 changes: 2 additions & 2 deletions cli/args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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]);
Expand Down
28 changes: 24 additions & 4 deletions lib/stack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <rang.hpp>
Expand Down Expand Up @@ -183,6 +185,24 @@ static std::map<std::string, std::string> 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/<uid> when crated's privops socket
// is detected, so two operators on the same host don't fight over
// the same dns-<network> 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,
Expand All @@ -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";
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
}

Expand Down
Loading