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

---

## [0.9.23] — 2026-05-09

**Rootless track, atomic single-iface verbs.** Twenty-fourth
0.9.x release. **First verb-set expansion since 0.9.0** —
previously the 14-verb taxonomy was treated as frozen, but
the `crate run` host-side bridge plumbing has primitives
(`setUp`, `disableOffload`) that don't compose well with
the existing `configure_iface` composite verb.

### What lands

#### Two new privops verbs

- **`set_iface_up`** — wraps `IfconfigOps::setUp(ifname)`.
Single string arg; idempotent (already-up succeeds).
- **`disable_iface_offload`** — wraps
`IfconfigOps::disableOffload(ifname)`. Same shape; the
FreeBSD 15 checksum-offload workaround.

Why these two together: same shape (1 ifname, no
response data), called as a sibling pair in
`run_net.cpp::setupBridgeEpair`, and small enough that
splitting into separate releases would be unhelpful.

#### Wire-up across the stack

- `lib/privops_pure.{h,cpp}` — `Verb::SetIfaceUp`,
`Verb::DisableIfaceOffload` + request structs +
validators (delegate to existing `validateIfaceName`)
- `lib/privops_wire_pure.{h,cpp}` — JSON parsers +
`formatSetIfaceUpSuccess` / `formatDisableIfaceOffloadSuccess`
builders + dispatcher cases
- `lib/privops_nv_pure.{h,cpp}` — nv parsers (FieldMap)
- `lib/privops_client.h` + `lib/privops_client_pure.cpp` —
`buildSetIfaceUp` / `buildDisableIfaceOffload`
- `daemon/privops_handlers.{h,cpp}` —
`handleSetIfaceUp` / `handleDisableIfaceOffload` +
dispatcher cases (HTTP + libnv transports)

#### CLI wiring

`lib/run_net.cpp` gets two new file-static helpers
(`setUpPrivopsOrLocal`, `disableOffloadPrivopsOrLocal`)
mirroring the 0.9.20 `moveToVnetPrivopsOrLocal` pattern.
Five call-sites are migrated:

| Site | Op |
|------|-----|
| `createEpair` line 160 | disableOffload(ifaceA) |
| `createEpair` line 161 | disableOffload(ifaceB) |
| `setupBridgeEpair` line 436 | disableOffload(ifaceA) |
| `setupBridgeEpair` line 437 | disableOffload(ifaceB) |
| `setupBridgeEpair` line 440 | setUp(ifaceA) |

### Why expand the verb set

The `configure_iface` composite verb (0.9.6) bundles
move + IP/MAC config + bridge attach. It assumes a
spec-driven "give me everything at once" usage. The
`run_net.cpp` orchestration is streaming — interleave
IfconfigOps calls with state-tracking and other ops.
For that pattern, atomic verbs match better. The
0.9.0 taxonomy contract isn't broken; new verbs append
to the closed set.

### Trade-offs

- **No rollback** of either op. The handler succeeds or
fails the entire op atomically. setUp + disableOffload
are themselves idempotent, so retry-on-failure works.
- **Other host-side IfconfigOps still need root.**
`bridgeAddMember`, `setInetAddr`, `createEpair` haven't
got matching verbs yet. 0.9.24 plan: `bridge_add_member`
+ `set_iface_inet_addr`. 0.9.25: `create_epair` (returns
pair names — first verb with non-trivial response data).

### Series state

CLI call-sites wired:
- `crate retune` → set_rctl / clear_rctl (0.9.15)
- `crate stop` → destroy_jail (0.9.17)
- `crate run` ZFS attach + detach → attach_zfs / detach_zfs (0.9.18)
- `crate run` nullfs mounts (8 sites) → mount_nullfs (0.9.19)
- `crate run` vnet moveToVnet (4 sites) → configure_iface move-only (0.9.20)
- `crate run` removeJail teardown → destroy_jail (0.9.21)
- `crate run` createJail → create_jail (0.9.22)
- **`crate run` setUp + disableOffload (5 sites) → set_iface_up / disable_iface_offload ← this release**

Remaining:
- 0.9.24 — `bridge_add_member` + `set_iface_inet_addr` verbs
- 0.9.25 — `create_epair` (first response-data verb)
- 0.9.26 — `network_lease.cpp` per-user paths + RCTL umbrella
- 0.9.27 — default flip
- 1.0.0 — setuid removed

### Tests

2 new ATF tests (`set_iface_up_minimal`,
`disable_iface_offload_minimal`) in privops_pure_test.
`verb_token_roundtrips_for_every_verb` updated to include
the 2 new verbs (catches future enum-add-without-mapping).
Suite: 1294 → **1296**.

### Files

- `lib/privops_pure.{h,cpp}` — verb enum + structs + validators
- `lib/privops_wire_pure.{h,cpp}` — JSON parsers + format builders
- `lib/privops_nv_pure.{h,cpp}` — nv parsers
- `lib/privops_client.h` + `lib/privops_client_pure.cpp` — builders
- `daemon/privops_handlers.{h,cpp}` — handlers + dispatcher cases
- `lib/run_net.cpp` — 2 new helpers + 5 call-site replacements
- `tests/unit/privops_pure_test.cpp` — 2 new tests
- `cli/args.cpp` — version `crate 0.9.23`
- `CHANGELOG.md` — entry

---

## [0.9.22] — 2026-05-09

**Rootless track, `createJail` via privops.** Twenty-third
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.22" << std::endl;
std::cout << "crate 0.9.23" << 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.22" << std::endl;
std::cout << "crate 0.9.23" << std::endl;
exit(0);
default:
err("unsupported short option '%s'", argv[a]);
Expand Down
54 changes: 54 additions & 0 deletions daemon/privops_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,28 @@ DispatchResult handleDestroyJail(const PrivOpsPure::DestroyJailReq &r) {
return {200, PrivOpsWirePure::formatDestroyJailSuccess(r.name)};
}

// --- handleSetIfaceUp / handleDisableIfaceOffload (0.9.23) ---

DispatchResult handleSetIfaceUp(const PrivOpsPure::SetIfaceUpReq &r) {
try {
IfconfigOps::setUp(r.ifname);
} catch (const std::exception &e) {
return {500, PrivOpsWirePure::formatHandlerError("ifconfig_failed",
e.what())};
}
return {200, PrivOpsWirePure::formatSetIfaceUpSuccess(r.ifname)};
}

DispatchResult handleDisableIfaceOffload(const PrivOpsPure::DisableIfaceOffloadReq &r) {
try {
IfconfigOps::disableOffload(r.ifname);
} catch (const std::exception &e) {
return {500, PrivOpsWirePure::formatHandlerError("ifconfig_failed",
e.what())};
}
return {200, PrivOpsWirePure::formatDisableIfaceOffloadSuccess(r.ifname)};
}

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

namespace {
Expand Down Expand Up @@ -508,6 +530,22 @@ DispatchResult dispatchPrivOp(Verb v, const std::string &body,
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleDestroyJail(r);
}
case Verb::SetIfaceUp: {
PrivOpsPure::SetIfaceUpReq r;
if (auto e = PrivOpsWirePure::parseSetIfaceUp(body, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateSetIfaceUp(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleSetIfaceUp(r);
}
case Verb::DisableIfaceOffload: {
PrivOpsPure::DisableIfaceOffloadReq r;
if (auto e = PrivOpsWirePure::parseDisableIfaceOffload(body, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateDisableIfaceOffload(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleDisableIfaceOffload(r);
}
default:
return PrivOpsWirePure::parseValidateAndDispatch(v, body);
}
Expand Down Expand Up @@ -638,6 +676,22 @@ DispatchResult dispatchPrivOpFromMap(const PrivOpsNvPure::FieldMap &m,
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleDestroyJail(r);
}
case Verb::SetIfaceUp: {
PrivOpsPure::SetIfaceUpReq r;
if (auto e = PrivOpsNvPure::parseSetIfaceUp(m, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateSetIfaceUp(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleSetIfaceUp(r);
}
case Verb::DisableIfaceOffload: {
PrivOpsPure::DisableIfaceOffloadReq r;
if (auto e = PrivOpsNvPure::parseDisableIfaceOffload(m, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateDisableIfaceOffload(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleDisableIfaceOffload(r);
}
case Verb::Unknown:
return {404,
std::string("{\"error\":\"unknown or missing 'verb' field\"}")};
Expand Down
7 changes: 7 additions & 0 deletions daemon/privops_handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,11 @@ PrivOpsWirePure::DispatchResult handleRemoveIpfwRule(const PrivOpsPure::RemoveIp
PrivOpsWirePure::DispatchResult handleCreateJail(const PrivOpsPure::CreateJailReq &r);
PrivOpsWirePure::DispatchResult handleDestroyJail(const PrivOpsPure::DestroyJailReq &r);

// 0.9.23: atomic single-iface ops needed by setupBridgeEpair flow.
// Wrap IfconfigOps::setUp / disableOffload (which themselves use
// libifconfig with shell fallback). Both succeed silently on
// idempotent calls (already up / already disabled).
PrivOpsWirePure::DispatchResult handleSetIfaceUp(const PrivOpsPure::SetIfaceUpReq &r);
PrivOpsWirePure::DispatchResult handleDisableIfaceOffload(const PrivOpsPure::DisableIfaceOffloadReq &r);

} // namespace Crated
4 changes: 4 additions & 0 deletions lib/privops_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ PrivOpsNvPure::FieldMap buildCreateJail(const std::string &name,
PrivOpsNvPure::FieldMap buildDestroyJail(const std::string &name,
bool force);

// 0.9.23: atomic single-iface ops.
PrivOpsNvPure::FieldMap buildSetIfaceUp(const std::string &ifname);
PrivOpsNvPure::FieldMap buildDisableIfaceOffload(const std::string &ifname);

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

struct Response {
Expand Down
14 changes: 14 additions & 0 deletions lib/privops_client_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,18 @@ PrivOpsNvPure::FieldMap buildDestroyJail(const std::string &name, bool force) {
};
}

PrivOpsNvPure::FieldMap buildSetIfaceUp(const std::string &ifname) {
return {
{"verb", "set_iface_up"},
{"ifname", ifname},
};
}

PrivOpsNvPure::FieldMap buildDisableIfaceOffload(const std::string &ifname) {
return {
{"verb", "disable_iface_offload"},
{"ifname", ifname},
};
}

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

std::string parseSetIfaceUp(const FieldMap &m,
PrivOpsPure::SetIfaceUpReq &out) {
if (auto e = requireString(m, "ifname", out.ifname); !e.empty()) return e;
return "";
}

std::string parseDisableIfaceOffload(const FieldMap &m,
PrivOpsPure::DisableIfaceOffloadReq &out) {
if (auto e = requireString(m, "ifname", out.ifname); !e.empty()) return e;
return "";
}

// --- Verb routing ---

PrivOpsPure::Verb extractVerb(const FieldMap &m) {
Expand Down
4 changes: 4 additions & 0 deletions lib/privops_nv_pure.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ std::string parseAddIpfwRule(const FieldMap &m,
PrivOpsPure::AddIpfwRuleReq &out);
std::string parseRemoveIpfwRule(const FieldMap &m,
PrivOpsPure::RemoveIpfwRuleReq &out);
std::string parseSetIfaceUp(const FieldMap &m,
PrivOpsPure::SetIfaceUpReq &out);
std::string parseDisableIfaceOffload(const FieldMap &m,
PrivOpsPure::DisableIfaceOffloadReq &out);

// --- Verb routing ---

Expand Down
12 changes: 12 additions & 0 deletions lib/privops_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ const char *verbName(Verb v) {
case Verb::RemovePfRule: return "remove_pf_rule";
case Verb::AddIpfwRule: return "add_ipfw_rule";
case Verb::RemoveIpfwRule: return "remove_ipfw_rule";
case Verb::SetIfaceUp: return "set_iface_up";
case Verb::DisableIfaceOffload: return "disable_iface_offload";
case Verb::Unknown: return "unknown";
}
return "unknown";
Expand All @@ -105,6 +107,8 @@ Verb parseVerb(const std::string &name) {
if (name == "remove_pf_rule") return Verb::RemovePfRule;
if (name == "add_ipfw_rule") return Verb::AddIpfwRule;
if (name == "remove_ipfw_rule") return Verb::RemoveIpfwRule;
if (name == "set_iface_up") return Verb::SetIfaceUp;
if (name == "disable_iface_offload") return Verb::DisableIfaceOffload;
return Verb::Unknown;
}

Expand Down Expand Up @@ -465,4 +469,12 @@ std::string validateRemoveIpfwRule(const RemoveIpfwRuleReq &r) {
return "";
}

std::string validateSetIfaceUp(const SetIfaceUpReq &r) {
return validateIfaceName(r.ifname);
}

std::string validateDisableIfaceOffload(const DisableIfaceOffloadReq &r) {
return validateIfaceName(r.ifname);
}

} // namespace PrivOpsPure
20 changes: 20 additions & 0 deletions lib/privops_pure.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ enum class Verb {
RemovePfRule,
AddIpfwRule,
RemoveIpfwRule,

// 0.9.23: atomic host-side iface ops needed by `crate run`'s
// setupBridgeEpair flow. The 0.9.0 ConfigureIface verb is the
// composite "move + config + bridge attach" path; these atomic
// verbs are the host-side primitives operators chain manually
// when they need finer control. Targets the `IfconfigOps::setUp`
// and `IfconfigOps::disableOffload` call sites in lib/run_net.cpp.
SetIfaceUp,
DisableIfaceOffload,
};

// Returns the verb's canonical wire-format token (lowercase, no
Expand Down Expand Up @@ -193,6 +202,15 @@ struct RemoveIpfwRuleReq {
unsigned number = 0;
};

// 0.9.23: atomic single-iface ops.
struct SetIfaceUpReq {
std::string ifname;
};

struct DisableIfaceOffloadReq {
std::string ifname;
};

// --- Per-verb validators ---
//
// Each `validate*(req)` returns "" on success, otherwise a one-line
Expand All @@ -215,6 +233,8 @@ std::string validateAddPfRule(const AddPfRuleReq &r);
std::string validateRemovePfRule(const RemovePfRuleReq &r);
std::string validateAddIpfwRule(const AddIpfwRuleReq &r);
std::string validateRemoveIpfwRule(const RemoveIpfwRuleReq &r);
std::string validateSetIfaceUp(const SetIfaceUpReq &r);
std::string validateDisableIfaceOffload(const DisableIfaceOffloadReq &r);

// --- Field-level validators (exposed for tests + reuse) ---
//
Expand Down
Loading
Loading