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

---

## [0.9.26] — 2026-05-09

**Rootless track, `create_epair` verb — first response-data
verb.** Twenty-seventh 0.9.x release. Wraps
`IfconfigOps::createEpair()`, which the kernel auto-assigns
the next free epair unit number for. The verb returns the
A/B iface names so the client can plumb them downstream.

### What lands

#### New privops verb

`create_epair` — no request fields. Response body shape:

```
{"created":true,"a":"epair17a","b":"epair17b"}
```

This is the first verb whose response carries non-trivial
data. Previously all verbs returned status + a confirmation
body the client could ignore. `create_epair` is different
because the operator NEEDS the assigned names to do the
next steps (move B-half into vnet, attach A-half to bridge).

#### Wire protocol

- Daemon handler: builds the JSON response via
`formatCreateEpairSuccess(a, b)` →
`{"created":true,"a":"<A>","b":"<B>"}`.
- Client (run_net.cpp): receives `Response.body` as JSON
string, calls existing `PrivOpsWirePure::extractStringField`
twice (once for `a`, once for `b`). No new JSON parser
needed — extractStringField already handles top-level
string fields (used by daemon-side parsers).

The libnv transport wraps the same JSON body in an nvlist
field as before — clients on either transport use the same
extraction code.

#### CLI wiring

`lib/run_net.cpp` gets `createEpairPrivopsOrLocal()` returning
`std::pair<std::string, std::string>`. Two call-sites
migrate (lines 239 / 518) — both `auto epairPair =
IfconfigOps::createEpair();` patterns.

### Trade-offs

- **JSON body extraction at the client.** Native nvlist
responses (per-verb structured fields) would be cleaner
for libnv transport but require refactoring 14 handlers
to expose typed responses. Current JSON-in-nvlist shape
ships today; future optimisation if motivated.
- **No retry on transient failures.** If `create_epair`
succeeds on the daemon but the response is dropped
(network issue, timeout), an orphan epair leaks. Same
constraint as direct `IfconfigOps::createEpair()` — that
pattern is unchanged.

### 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 (host-side epair-A) (0.9.25)
- **`crate run` createEpair (2 sites) → create_epair ← this release**

**Full host-side iface plumbing now coverable via privops.**
The 5 `IfconfigOps::*` ops `crate run` uses
(createEpair / disableOffload / setUp / setInetAddr /
bridgeAddMember) all have privops paths. With 0.9.22's
createJail and 0.9.21's removeJail, **`crate run` and
`crate stop` no longer need root for any privileged step
when the privops socket is detected** — the legacy setuid
fallback is now optional.

### Remaining

- 0.9.27 — `network_lease.cpp` per-user paths + RCTL
umbrella application (uses 0.9.10 sub-CIDR + 0.9.11
loginclass)
- 0.9.28 — default flip
- 1.0.0 — setuid removed

### Tests

- 1 new ATF test in `privops_pure_test`
(`create_epair_no_fields_required`)
- 1 new ATF test in `privops_wire_pure_test`
(`format_create_epair_response_extracts`) that locks down
the response shape AND verifies it round-trips through the
client's `extractStringField` extraction path
- `verb_token_roundtrips_for_every_verb` updated
- Suite: 1299 → **1301**

### Files

Same set as 0.9.23/0.9.24/0.9.25 plus
`tests/unit/privops_wire_pure_test.cpp`:
`privops_pure.{h,cpp}`, `privops_wire_pure.{h,cpp}`,
`privops_nv_pure.{h,cpp}`, `privops_client.h`,
`privops_client_pure.cpp`, `privops_handlers.{h,cpp}`,
`run_net.cpp`, `tests/unit/privops_pure_test.cpp`,
`tests/unit/privops_wire_pure_test.cpp`, `cli/args.cpp`,
`CHANGELOG.md`.

---

## [0.9.25] — 2026-05-09

**Rootless track, `set_iface_inet_addr` verb.** Twenty-sixth
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.25" << std::endl;
std::cout << "crate 0.9.26" << 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.25" << std::endl;
std::cout << "crate 0.9.26" << std::endl;
exit(0);
default:
err("unsupported short option '%s'", argv[a]);
Expand Down
30 changes: 30 additions & 0 deletions daemon/privops_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,20 @@ DispatchResult handleSetIfaceInetAddr(const PrivOpsPure::SetIfaceInetAddrReq &r)
r.ifname, r.addr, r.prefixLen)};
}

// --- handleCreateEpair (0.9.26) ---

DispatchResult handleCreateEpair(const PrivOpsPure::CreateEpairReq &/*r*/) {
std::pair<std::string, std::string> pair;
try {
pair = IfconfigOps::createEpair();
} catch (const std::exception &e) {
return {500, PrivOpsWirePure::formatHandlerError("ifconfig_failed",
e.what())};
}
return {200, PrivOpsWirePure::formatCreateEpairSuccess(pair.first,
pair.second)};
}

// --- Top-level dispatcher ---

namespace {
Expand Down Expand Up @@ -605,6 +619,14 @@ DispatchResult dispatchPrivOp(Verb v, const std::string &body,
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleSetIfaceInetAddr(r);
}
case Verb::CreateEpair: {
PrivOpsPure::CreateEpairReq r;
if (auto e = PrivOpsWirePure::parseCreateEpair(body, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateCreateEpair(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleCreateEpair(r);
}
default:
return PrivOpsWirePure::parseValidateAndDispatch(v, body);
}
Expand Down Expand Up @@ -775,6 +797,14 @@ DispatchResult dispatchPrivOpFromMap(const PrivOpsNvPure::FieldMap &m,
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleSetIfaceInetAddr(r);
}
case Verb::CreateEpair: {
PrivOpsPure::CreateEpairReq r;
if (auto e = PrivOpsNvPure::parseCreateEpair(m, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateCreateEpair(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleCreateEpair(r);
}
case Verb::Unknown:
return {404,
std::string("{\"error\":\"unknown or missing 'verb' field\"}")};
Expand Down
5 changes: 5 additions & 0 deletions daemon/privops_handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,9 @@ PrivOpsWirePure::DispatchResult handleBridgeDelMember(const PrivOpsPure::BridgeD
// IfconfigOps::setInetAddr (3-arg primitive: iface + addr + prefixLen).
PrivOpsWirePure::DispatchResult handleSetIfaceInetAddr(const PrivOpsPure::SetIfaceInetAddrReq &r);

// 0.9.26: create an epair pair via libifconfig (with shell
// fallback). The kernel assigns the next free unit number;
// the response body carries the assigned A/B iface names.
PrivOpsWirePure::DispatchResult handleCreateEpair(const PrivOpsPure::CreateEpairReq &r);

} // namespace Crated
6 changes: 6 additions & 0 deletions lib/privops_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ PrivOpsNvPure::FieldMap buildSetIfaceInetAddr(const std::string &ifname,
const std::string &addr,
unsigned prefixLen);

// 0.9.26: create epair pair. No request fields. Response body
// (in Response.body) is a JSON object with `a` and `b` fields
// holding the kernel-assigned A/B iface names; clients parse
// them via PrivOpsWirePure::extractStringField.
PrivOpsNvPure::FieldMap buildCreateEpair();

// --- Wire transport (FreeBSD-only) ---

struct Response {
Expand Down
6 changes: 6 additions & 0 deletions lib/privops_client_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,10 @@ PrivOpsNvPure::FieldMap buildSetIfaceInetAddr(const std::string &ifname,
};
}

PrivOpsNvPure::FieldMap buildCreateEpair() {
return {
{"verb", "create_epair"},
};
}

} // namespace PrivOpsClient
5 changes: 5 additions & 0 deletions lib/privops_nv_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ std::string parseSetIfaceInetAddr(const FieldMap &m,
return "";
}

std::string parseCreateEpair(const FieldMap &/*m*/,
PrivOpsPure::CreateEpairReq &/*out*/) {
return "";
}

// --- Verb routing ---

PrivOpsPure::Verb extractVerb(const FieldMap &m) {
Expand Down
2 changes: 2 additions & 0 deletions lib/privops_nv_pure.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ std::string parseBridgeDelMember(const FieldMap &m,
PrivOpsPure::BridgeDelMemberReq &out);
std::string parseSetIfaceInetAddr(const FieldMap &m,
PrivOpsPure::SetIfaceInetAddrReq &out);
std::string parseCreateEpair(const FieldMap &m,
PrivOpsPure::CreateEpairReq &out);

// --- Verb routing ---

Expand Down
8 changes: 8 additions & 0 deletions lib/privops_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const char *verbName(Verb v) {
case Verb::BridgeAddMember: return "bridge_add_member";
case Verb::BridgeDelMember: return "bridge_del_member";
case Verb::SetIfaceInetAddr: return "set_iface_inet_addr";
case Verb::CreateEpair: return "create_epair";
case Verb::Unknown: return "unknown";
}
return "unknown";
Expand All @@ -115,6 +116,7 @@ Verb parseVerb(const std::string &name) {
if (name == "bridge_add_member") return Verb::BridgeAddMember;
if (name == "bridge_del_member") return Verb::BridgeDelMember;
if (name == "set_iface_inet_addr") return Verb::SetIfaceInetAddr;
if (name == "create_epair") return Verb::CreateEpair;
return Verb::Unknown;
}

Expand Down Expand Up @@ -505,4 +507,10 @@ std::string validateSetIfaceInetAddr(const SetIfaceInetAddrReq &r) {
return "";
}

std::string validateCreateEpair(const CreateEpairReq &) {
// No fields — kernel picks the unit number. Validator is here
// for symmetry with the rest of the verb set; always succeeds.
return "";
}

} // namespace PrivOpsPure
14 changes: 14 additions & 0 deletions lib/privops_pure.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ enum class Verb {
// createEpair flow where the host-side epair-A end gets a
// /31 IP after the jail-side epair-B is moved into the jail.
SetIfaceInetAddr,

// 0.9.26: create epair pair. First response-data verb —
// returns the kernel-assigned A/B iface names. Wraps
// IfconfigOps::createEpair() (no inputs; output is a pair
// of strings). Targets run_net.cpp::createEpair (line 117)
// and setupBridgeEpair (line 396) where the existing code
// unpacks `auto epairPair = IfconfigOps::createEpair();`.
CreateEpair,
};

// Returns the verb's canonical wire-format token (lowercase, no
Expand Down Expand Up @@ -248,6 +256,11 @@ struct SetIfaceInetAddrReq {
unsigned prefixLen = 32; // 0..32
};

// 0.9.26: create epair pair. No request fields — kernel picks
// the next free unit number (epair<N>a / epair<N>b).
struct CreateEpairReq {
};

// --- Per-verb validators ---
//
// Each `validate*(req)` returns "" on success, otherwise a one-line
Expand Down Expand Up @@ -275,6 +288,7 @@ std::string validateDisableIfaceOffload(const DisableIfaceOffloadReq &r);
std::string validateBridgeAddMember(const BridgeAddMemberReq &r);
std::string validateBridgeDelMember(const BridgeDelMemberReq &r);
std::string validateSetIfaceInetAddr(const SetIfaceInetAddrReq &r);
std::string validateCreateEpair(const CreateEpairReq &r);

// --- Field-level validators (exposed for tests + reuse) ---
//
Expand Down
18 changes: 18 additions & 0 deletions lib/privops_wire_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,12 @@ std::string parseSetIfaceInetAddr(const std::string &body,
return "";
}

std::string parseCreateEpair(const std::string &/*body*/,
PrivOpsPure::CreateEpairReq &/*out*/) {
// No required fields. Body content (if any) is ignored.
return "";
}

// --- Verb routing helper ---

PrivOpsPure::Verb parseVerbFromPath(const std::string &path) {
Expand Down Expand Up @@ -656,6 +662,16 @@ std::string formatSetIfaceInetAddrSuccess(const std::string &ifname,
return o.str();
}

std::string formatCreateEpairSuccess(const std::string &ifaceA,
const std::string &ifaceB) {
std::ostringstream o;
o << "{\"created\":true"
<< ",\"a\":\"" << escape(ifaceA) << "\""
<< ",\"b\":\"" << escape(ifaceB) << "\""
<< "}";
return o.str();
}

DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v,
const std::string &body) {
using namespace PrivOpsPure;
Expand Down Expand Up @@ -698,6 +714,8 @@ DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v,
return runVerb<BridgeDelMemberReq>(body, v, parseBridgeDelMember, validateBridgeDelMember);
case Verb::SetIfaceInetAddr:
return runVerb<SetIfaceInetAddrReq>(body, v, parseSetIfaceInetAddr, validateSetIfaceInetAddr);
case Verb::CreateEpair:
return runVerb<CreateEpairReq>(body, v, parseCreateEpair, validateCreateEpair);
case Verb::Unknown:
break;
}
Expand Down
12 changes: 12 additions & 0 deletions lib/privops_wire_pure.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ std::string parseBridgeDelMember(const std::string &body,
std::string parseSetIfaceInetAddr(const std::string &body,
PrivOpsPure::SetIfaceInetAddrReq &out);

std::string parseCreateEpair(const std::string &body,
PrivOpsPure::CreateEpairReq &out);

// --- Verb routing helper ---
//
// Parse the URL path's verb segment. The route pattern is
Expand Down Expand Up @@ -291,4 +294,13 @@ std::string formatSetIfaceInetAddrSuccess(const std::string &ifname,
const std::string &addr,
unsigned prefixLen);

// 0.9.26: 200 OK body for create_epair. Returns the kernel-
// assigned A/B iface names. First privops verb whose response
// carries non-trivial data; clients parse the body to retrieve
// them (existing extractStringField fits).
//
// Shape: {"created":true,"a":"<ifaceA>","b":"<ifaceB>"}
std::string formatCreateEpairSuccess(const std::string &ifaceA,
const std::string &ifaceB);

} // namespace PrivOpsWirePure
Loading
Loading