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/).

---

## [0.9.24] — 2026-05-09

**Rootless track, bridge membership verbs.** Twenty-fifth
0.9.x release. Symmetric pair around `IfconfigOps::bridgeAddMember`
and `bridgeDelMember`.

### What lands

#### Two new privops verbs

- **`bridge_add_member`** — wraps `IfconfigOps::bridgeAddMember(bridge, member)`.
Fields: `bridge`, `member` (both validated as iface names).
- **`bridge_del_member`** — symmetric remove.

Different shape from 0.9.23's verbs (2 args vs 1) but same
overall pattern. Validators delegate to existing
`validateIfaceName` for both fields.

#### Wire-up across the stack

Same files as 0.9.23 — `privops_pure`, `privops_wire_pure`,
`privops_nv_pure`, `privops_client`, `privops_handlers`. Each
gained two new functions / cases.

#### CLI wiring

`lib/run_net.cpp` gets two more file-static helpers
(`bridgeAddMemberPrivopsOrLocal`, `bridgeDelMemberPrivopsOrLocal`)
and 2 call-sites migrate:

| Site | Op |
|------|-----|
| `setupBridgeEpair` line 481 | `bridgeAddMember(bridgeIface, ifaceA)` |
| `destroyBridgeEpair` line 491 | `bridgeDelMember(bridgeIface, ifaceA)` |

`bridgeAddMember` is hard-fail (matches existing exception
behaviour). `bridgeDelMember` is soft-fail (matches RunAtEnd
teardown pattern from earlier releases — warn on error,
continue cleanup).

### 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 → bridge_add_member / bridge_del_member ← this release**

Remaining iface verbs:
- 0.9.25 — `set_iface_inet_addr` (3-arg verb: iface + addr + prefix_len)
- 0.9.26 — `create_epair` (first response-data verb — returns the epair pair names)
- 0.9.27 — `network_lease.cpp` per-user paths + RCTL umbrella
- 0.9.28 — default flip
- 1.0.0 — setuid removed

### Tests

2 new ATF tests (`bridge_add_member_minimal`,
`bridge_del_member_minimal`) in `privops_pure_test`.
`verb_token_roundtrips_for_every_verb` updated to include
both new verbs. Suite: 1296 → **1298**.

### Files

Same set as 0.9.23: `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`, `cli/args.cpp`,
`CHANGELOG.md`.

---

## [0.9.23] — 2026-05-09

**Rootless track, atomic single-iface verbs.** Twenty-fourth
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.23" << std::endl;
std::cout << "crate 0.9.24" << 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.23" << std::endl;
std::cout << "crate 0.9.24" << 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 @@ -394,6 +394,28 @@ DispatchResult handleDisableIfaceOffload(const PrivOpsPure::DisableIfaceOffloadR
return {200, PrivOpsWirePure::formatDisableIfaceOffloadSuccess(r.ifname)};
}

// --- handleBridgeAddMember / handleBridgeDelMember (0.9.24) ---

DispatchResult handleBridgeAddMember(const PrivOpsPure::BridgeAddMemberReq &r) {
try {
IfconfigOps::bridgeAddMember(r.bridge, r.member);
} catch (const std::exception &e) {
return {500, PrivOpsWirePure::formatHandlerError("ifconfig_failed",
e.what())};
}
return {200, PrivOpsWirePure::formatBridgeAddMemberSuccess(r.bridge, r.member)};
}

DispatchResult handleBridgeDelMember(const PrivOpsPure::BridgeDelMemberReq &r) {
try {
IfconfigOps::bridgeDelMember(r.bridge, r.member);
} catch (const std::exception &e) {
return {500, PrivOpsWirePure::formatHandlerError("ifconfig_failed",
e.what())};
}
return {200, PrivOpsWirePure::formatBridgeDelMemberSuccess(r.bridge, r.member)};
}

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

namespace {
Expand Down Expand Up @@ -546,6 +568,22 @@ DispatchResult dispatchPrivOp(Verb v, const std::string &body,
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleDisableIfaceOffload(r);
}
case Verb::BridgeAddMember: {
PrivOpsPure::BridgeAddMemberReq r;
if (auto e = PrivOpsWirePure::parseBridgeAddMember(body, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateBridgeAddMember(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleBridgeAddMember(r);
}
case Verb::BridgeDelMember: {
PrivOpsPure::BridgeDelMemberReq r;
if (auto e = PrivOpsWirePure::parseBridgeDelMember(body, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateBridgeDelMember(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleBridgeDelMember(r);
}
default:
return PrivOpsWirePure::parseValidateAndDispatch(v, body);
}
Expand Down Expand Up @@ -692,6 +730,22 @@ DispatchResult dispatchPrivOpFromMap(const PrivOpsNvPure::FieldMap &m,
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleDisableIfaceOffload(r);
}
case Verb::BridgeAddMember: {
PrivOpsPure::BridgeAddMemberReq r;
if (auto e = PrivOpsNvPure::parseBridgeAddMember(m, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateBridgeAddMember(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleBridgeAddMember(r);
}
case Verb::BridgeDelMember: {
PrivOpsPure::BridgeDelMemberReq r;
if (auto e = PrivOpsNvPure::parseBridgeDelMember(m, r); !e.empty())
return {400, PrivOpsWirePure::formatParseError(e)};
if (auto e = PrivOpsPure::validateBridgeDelMember(r); !e.empty())
return {400, PrivOpsWirePure::formatValidateError(e)};
return handleBridgeDelMember(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 @@ -152,4 +152,9 @@ PrivOpsWirePure::DispatchResult handleDestroyJail(const PrivOpsPure::DestroyJail
PrivOpsWirePure::DispatchResult handleSetIfaceUp(const PrivOpsPure::SetIfaceUpReq &r);
PrivOpsWirePure::DispatchResult handleDisableIfaceOffload(const PrivOpsPure::DisableIfaceOffloadReq &r);

// 0.9.24: bridge membership ops. Wraps
// IfconfigOps::bridgeAddMember / bridgeDelMember.
PrivOpsWirePure::DispatchResult handleBridgeAddMember(const PrivOpsPure::BridgeAddMemberReq &r);
PrivOpsWirePure::DispatchResult handleBridgeDelMember(const PrivOpsPure::BridgeDelMemberReq &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 @@ -112,6 +112,12 @@ PrivOpsNvPure::FieldMap buildDestroyJail(const std::string &name,
PrivOpsNvPure::FieldMap buildSetIfaceUp(const std::string &ifname);
PrivOpsNvPure::FieldMap buildDisableIfaceOffload(const std::string &ifname);

// 0.9.24: bridge membership ops.
PrivOpsNvPure::FieldMap buildBridgeAddMember(const std::string &bridge,
const std::string &member);
PrivOpsNvPure::FieldMap buildBridgeDelMember(const std::string &bridge,
const std::string &member);

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

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

PrivOpsNvPure::FieldMap buildBridgeAddMember(const std::string &bridge,
const std::string &member) {
return {
{"verb", "bridge_add_member"},
{"bridge", bridge},
{"member", member},
};
}

PrivOpsNvPure::FieldMap buildBridgeDelMember(const std::string &bridge,
const std::string &member) {
return {
{"verb", "bridge_del_member"},
{"bridge", bridge},
{"member", member},
};
}

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

std::string parseBridgeAddMember(const FieldMap &m,
PrivOpsPure::BridgeAddMemberReq &out) {
if (auto e = requireString(m, "bridge", out.bridge); !e.empty()) return e;
if (auto e = requireString(m, "member", out.member); !e.empty()) return e;
return "";
}

std::string parseBridgeDelMember(const FieldMap &m,
PrivOpsPure::BridgeDelMemberReq &out) {
if (auto e = requireString(m, "bridge", out.bridge); !e.empty()) return e;
if (auto e = requireString(m, "member", out.member); !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 @@ -123,6 +123,10 @@ std::string parseSetIfaceUp(const FieldMap &m,
PrivOpsPure::SetIfaceUpReq &out);
std::string parseDisableIfaceOffload(const FieldMap &m,
PrivOpsPure::DisableIfaceOffloadReq &out);
std::string parseBridgeAddMember(const FieldMap &m,
PrivOpsPure::BridgeAddMemberReq &out);
std::string parseBridgeDelMember(const FieldMap &m,
PrivOpsPure::BridgeDelMemberReq &out);

// --- Verb routing ---

Expand Down
16 changes: 16 additions & 0 deletions lib/privops_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ const char *verbName(Verb v) {
case Verb::RemoveIpfwRule: return "remove_ipfw_rule";
case Verb::SetIfaceUp: return "set_iface_up";
case Verb::DisableIfaceOffload: return "disable_iface_offload";
case Verb::BridgeAddMember: return "bridge_add_member";
case Verb::BridgeDelMember: return "bridge_del_member";
case Verb::Unknown: return "unknown";
}
return "unknown";
Expand All @@ -109,6 +111,8 @@ Verb parseVerb(const std::string &name) {
if (name == "remove_ipfw_rule") return Verb::RemoveIpfwRule;
if (name == "set_iface_up") return Verb::SetIfaceUp;
if (name == "disable_iface_offload") return Verb::DisableIfaceOffload;
if (name == "bridge_add_member") return Verb::BridgeAddMember;
if (name == "bridge_del_member") return Verb::BridgeDelMember;
return Verb::Unknown;
}

Expand Down Expand Up @@ -477,4 +481,16 @@ std::string validateDisableIfaceOffload(const DisableIfaceOffloadReq &r) {
return validateIfaceName(r.ifname);
}

std::string validateBridgeAddMember(const BridgeAddMemberReq &r) {
if (auto e = validateIfaceName(r.bridge); !e.empty()) return "bridge: " + e;
if (auto e = validateIfaceName(r.member); !e.empty()) return "member: " + e;
return "";
}

std::string validateBridgeDelMember(const BridgeDelMemberReq &r) {
if (auto e = validateIfaceName(r.bridge); !e.empty()) return "bridge: " + e;
if (auto e = validateIfaceName(r.member); !e.empty()) return "member: " + e;
return "";
}

} // namespace PrivOpsPure
25 changes: 25 additions & 0 deletions lib/privops_pure.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ enum class Verb {
// and `IfconfigOps::disableOffload` call sites in lib/run_net.cpp.
SetIfaceUp,
DisableIfaceOffload,

// 0.9.24: bridge membership ops. Symmetric pair around
// IfconfigOps::bridgeAddMember / bridgeDelMember. The 0.9.6
// composite ConfigureIface verb embeds bridgeAddMember when
// its `bridge` field is non-empty, but only for the
// computed-pair-A pattern (epair host-side). These atomic
// verbs target run_net.cpp's setupBridgeEpair (add) and
// destroyBridgeEpair (del) where the iface to attach is
// operator-supplied directly.
BridgeAddMember,
BridgeDelMember,
};

// Returns the verb's canonical wire-format token (lowercase, no
Expand Down Expand Up @@ -211,6 +222,18 @@ struct DisableIfaceOffloadReq {
std::string ifname;
};

// 0.9.24: bridge membership ops. Both validate via existing
// validateIfaceName for both `bridge` and `member` fields.
struct BridgeAddMemberReq {
std::string bridge; // bridge interface name (e.g. "bridge0")
std::string member; // member interface name (e.g. "epair0a")
};

struct BridgeDelMemberReq {
std::string bridge;
std::string member;
};

// --- Per-verb validators ---
//
// Each `validate*(req)` returns "" on success, otherwise a one-line
Expand All @@ -235,6 +258,8 @@ std::string validateAddIpfwRule(const AddIpfwRuleReq &r);
std::string validateRemoveIpfwRule(const RemoveIpfwRuleReq &r);
std::string validateSetIfaceUp(const SetIfaceUpReq &r);
std::string validateDisableIfaceOffload(const DisableIfaceOffloadReq &r);
std::string validateBridgeAddMember(const BridgeAddMemberReq &r);
std::string validateBridgeDelMember(const BridgeDelMemberReq &r);

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

std::string parseBridgeAddMember(const std::string &body,
PrivOpsPure::BridgeAddMemberReq &out) {
if (auto e = requireStringField(body, "bridge", out.bridge); !e.empty()) return e;
if (auto e = requireStringField(body, "member", out.member); !e.empty()) return e;
return "";
}

std::string parseBridgeDelMember(const std::string &body,
PrivOpsPure::BridgeDelMemberReq &out) {
if (auto e = requireStringField(body, "bridge", out.bridge); !e.empty()) return e;
if (auto e = requireStringField(body, "member", out.member); !e.empty()) return e;
return "";
}

// --- Verb routing helper ---

PrivOpsPure::Verb parseVerbFromPath(const std::string &path) {
Expand Down Expand Up @@ -602,6 +616,26 @@ std::string formatDisableIfaceOffloadSuccess(const std::string &ifname) {
return o.str();
}

std::string formatBridgeAddMemberSuccess(const std::string &bridge,
const std::string &member) {
std::ostringstream o;
o << "{\"added\":true"
<< ",\"bridge\":\"" << escape(bridge) << "\""
<< ",\"member\":\"" << escape(member) << "\""
<< "}";
return o.str();
}

std::string formatBridgeDelMemberSuccess(const std::string &bridge,
const std::string &member) {
std::ostringstream o;
o << "{\"removed\":true"
<< ",\"bridge\":\"" << escape(bridge) << "\""
<< ",\"member\":\"" << escape(member) << "\""
<< "}";
return o.str();
}

DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v,
const std::string &body) {
using namespace PrivOpsPure;
Expand Down Expand Up @@ -638,6 +672,10 @@ DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v,
return runVerb<SetIfaceUpReq>(body, v, parseSetIfaceUp, validateSetIfaceUp);
case Verb::DisableIfaceOffload:
return runVerb<DisableIfaceOffloadReq>(body, v, parseDisableIfaceOffload, validateDisableIfaceOffload);
case Verb::BridgeAddMember:
return runVerb<BridgeAddMemberReq>(body, v, parseBridgeAddMember, validateBridgeAddMember);
case Verb::BridgeDelMember:
return runVerb<BridgeDelMemberReq>(body, v, parseBridgeDelMember, validateBridgeDelMember);
case Verb::Unknown:
break;
}
Expand Down
Loading
Loading