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
107 changes: 107 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,113 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## [0.9.27] — 2026-05-09

**Rootless track, per-user lease file path.** Twenty-eighth
0.9.x release. The IP-lease file (`network-leases.txt`)
moves from a single shared `/var/run/crate/` location to a
per-user `/var/run/crate/<uid>/` subtree when crated's
privops socket is detected.

### What lands

`lib/network_lease.cpp::effectivePath()` — lazily resolves
the lease file path on first use:

```cpp
const std::string &effectivePath() {
static std::string cached;
static bool computed = false;
if (!computed) {
if (g_pathOverridden) {
cached = g_path; // honour test override
} else if (!PrivOpsClient::detectSocketPath().empty()) {
cached = RuntimePathsPure::perUserRoot((uint32_t)::getuid())
+ "/network-leases.txt";
} else {
cached = g_path; // legacy single-tenant
}
computed = true;
}
return cached;
}
```

All call sites (`openLocked`, `readAll`, `writeAllAtomic`,
`leasePath()`) replaced `g_path.c_str()` references with
`effectivePath().c_str()`. Same path for the entire process
lifetime; cache eliminates per-call detection overhead.

### Behavior

| Mode | Lease file path |
|------|-----------------|
| Legacy (no privops socket) | `/var/run/crate/network-leases.txt` |
| Rootless (socket detected) | `/var/run/crate/<uid>/network-leases.txt` |
| Test override (`setPathForTesting`) | the supplied path |

The 0.9.10 sub-CIDR allocator already gives each operator a
disjoint IP range. Combined with this per-user lease file,
**alice's `crate run` never reads or writes bob's leases**
— two operators can both run a jail named `web` simultaneously
without IP collision.

### Trade-offs

- **Lease file location lock-in at process start.** The path
is computed once on first call to `effectivePath()` and
cached for the lifetime of the process. If the operator
starts/stops crated mid-`crate run`, the lease file
remains where it was first resolved. Acceptable: `crate
run` invocations are short-lived.
- **Migration of existing lease entries.** Operators with
pre-0.9.27 deployments switching to rootless mode will
see an empty per-user lease file at first; existing leases
in the legacy path are not auto-imported. Documented in
the migration doc; manual `crate clean` + `crate run`
rebuilds them per-user.

### Series state

CLI call-sites wired:
- `crate retune` (0.9.15)
- `crate stop` (0.9.17)
- `crate run` ZFS attach + detach (0.9.18)
- `crate run` nullfs mounts 8 sites (0.9.19)
- `crate run` vnet moveToVnet 4 sites (0.9.20)
- `crate run` removeJail teardown (0.9.21)
- `crate run` createJail (0.9.22)
- `crate run` setUp + disableOffload 5 sites (0.9.23)
- `crate run` bridge add + del 2 sites (0.9.24)
- `crate run` setInetAddr (0.9.25)
- `crate run` createEpair 2 sites (0.9.26)
- **`crate run` lease file path → per-user under
/var/run/crate/<uid>/ ← this release**

Remaining:
- 0.9.28 — RCTL umbrella application (uses 0.9.11
loginclass to apply `loginclass:crate-<uid>:KEY:deny=...`
rules at jail-create time)
- 0.9.29 — default flip (`rootless_per_user: true`
becomes default in `crated.conf.sample`)
- 1.0.0 — setuid bit removed from `Makefile install`

### Tests

No new tests added — existing `network_lease`-using tests
continue to pass via the `g_pathOverridden` test-override
path (set by `setPathForTesting`). Suite stays at 1301.

### Files

- `lib/network_lease.cpp` — `#include "privops_client.h"` +
`#include "runtime_paths_pure.h"` + `effectivePath()`
helper + `g_pathOverridden` flag + 7 call-site updates
- `cli/args.cpp` — version `crate 0.9.27`
- `CHANGELOG.md` — entry

---

## [0.9.26] — 2026-05-09

**Rootless track, `create_epair` verb — first response-data
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 0.9.26" << std::endl;
std::cout << "crate 0.9.27" << 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 0.9.26" << std::endl;
std::cout << "crate 0.9.27" << std::endl;
exit(0);
default:
err("unsupported short option '%s'", argv[a]);
Expand Down
58 changes: 48 additions & 10 deletions lib/network_lease.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (C) 2026 by Vladyslav V. Prodan <github.com/click0>. All rights reserved.

#include "network_lease.h"
#include "privops_client.h"
#include "runtime_paths_pure.h"
#include "err.h"

#include <fcntl.h>
Expand All @@ -20,20 +22,47 @@ namespace NetworkLease {

namespace {

// 0.9.27: legacy single-tenant default. The actual path used at
// runtime comes from `effectivePath()` below — when crated's
// privops socket is detected, the lease file moves under
// /var/run/crate/<uid>/network-leases.txt so each operator's
// IP allocations stay isolated. Tests + the explicit
// setPathForTesting() override route still use g_path directly.
std::string g_path = "/var/run/crate/network-leases.txt";
bool g_pathOverridden = false; // set by setPathForTesting

// Resolve the lease file path. Lazily caches the per-uid path
// when rootless mode is active. Same path for the entire
// process lifetime.
const std::string &effectivePath() {
static std::string cached;
static bool computed = false;
if (!computed) {
if (g_pathOverridden) {
cached = g_path; // honour test override
} else if (!PrivOpsClient::detectSocketPath().empty()) {
cached = RuntimePathsPure::perUserRoot((uint32_t)::getuid())
+ "/network-leases.txt";
} else {
cached = g_path; // legacy single-tenant
}
computed = true;
}
return cached;
}

// Open (creating) the lease file and acquire an exclusive lock.
// Returns the fd. Caller must ::close() it.
int openLocked() {
int fd = ::open(g_path.c_str(),
int fd = ::open(effectivePath().c_str(),
O_RDWR | O_CREAT | O_CLOEXEC,
0640);
if (fd < 0)
ERR("cannot open " << g_path << ": " << std::strerror(errno))
ERR("cannot open " << effectivePath() << ": " << std::strerror(errno))
if (::flock(fd, LOCK_EX) != 0) {
int e = errno;
::close(fd);
ERR("flock " << g_path << ": " << std::strerror(e))
ERR("flock " << effectivePath() << ": " << std::strerror(e))
}
return fd;
}
Expand Down Expand Up @@ -78,7 +107,7 @@ std::vector<IpAllocPure::Lease> parseAll(const std::string &buf) {
// crash mid-write doesn't leave a corrupt half-written file.
void writeAllAtomic(int /*lockedFd*/,
const std::vector<IpAllocPure::Lease> &leases) {
std::string tmp = g_path + ".tmp";
std::string tmp = effectivePath() + ".tmp";
int fd = ::open(tmp.c_str(),
O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
0640);
Expand All @@ -102,25 +131,34 @@ void writeAllAtomic(int /*lockedFd*/,
}
::fsync(fd);
::close(fd);
if (::rename(tmp.c_str(), g_path.c_str()) != 0) {
if (::rename(tmp.c_str(), effectivePath().c_str()) != 0) {
int e = errno;
::unlink(tmp.c_str());
ERR("rename " << tmp << " -> " << g_path << ": " << std::strerror(e))
ERR("rename " << tmp << " -> " << effectivePath() << ": " << std::strerror(e))
}
}

} // anon

const std::string &leasePath() { return g_path; }
void setPathForTesting(const std::string &path) { g_path = path; }
const std::string &leasePath() { return effectivePath(); }
void setPathForTesting(const std::string &path) {
g_path = path;
g_pathOverridden = true;
// Note: effectivePath()'s static cache is computed on first
// call, so for tests that exercise setPathForTesting after
// effectivePath() has already cached, the override won't take
// effect. Tests that need to swap paths mid-run should fork
// (ATF tests already run in subprocesses), giving each test
// its own first-call cache.
}

std::vector<IpAllocPure::Lease> readAll() {
// Open without lock for read-only callers (e.g. `crate doctor`,
// future). Tolerate ENOENT.
int fd = ::open(g_path.c_str(), O_RDONLY | O_CLOEXEC);
int fd = ::open(effectivePath().c_str(), O_RDONLY | O_CLOEXEC);
if (fd < 0) {
if (errno == ENOENT) return {};
ERR("open " << g_path << ": " << std::strerror(errno))
ERR("open " << effectivePath() << ": " << std::strerror(errno))
}
auto buf = readAllFd(fd);
::close(fd);
Expand Down
Loading