diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fb52bf..f0680eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,84 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## [0.9.25] — 2026-05-09 + +**Rootless track, `set_iface_inet_addr` verb.** Twenty-sixth +0.9.x release. Third atomic iface verb. The host-side IPv4 +assignment primitive used by `run_net.cpp::createEpair` to +configure the host-side epair-A end after the jail-side +epair-B is moved into the jail. + +### What lands + +#### New privops verb + +`set_iface_inet_addr` — wraps `IfconfigOps::setInetAddr(iface, +addr, prefixLen)`. Three-arg shape: + +| Field | Type | Notes | +|-------|------|-------| +| `ifname` | string | Iface name (validated via `validateIfaceName`) | +| `addr` | string | Bare IPv4 (no `/prefix`) | +| `prefix_len` | unsigned | 0..32 | + +Validator reuses `validateIpv4Cidr` by reassembling +`addr + "/" + prefixLen` — cheaper than duplicating IPv4 +octet logic. + +#### Wire-up across the stack + +Same files as 0.9.23 / 0.9.24 — `privops_pure`, +`privops_wire_pure`, `privops_nv_pure`, `privops_client`, +`privops_handlers`. One new function/case in each. + +#### CLI wiring + +`lib/run_net.cpp::createEpair` line 229 (was line 209 +pre-0.9.24) now calls `setInetAddrPrivopsOrLocal(info.ifaceA, +info.ipA, 31)` instead of direct `IfconfigOps::setInetAddr`. +The /31 epair-A side gets its IP via privops when the socket +is detected. + +### 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) → set_iface_inet_addr ← this release** + +Remaining iface verbs: +- 0.9.26 — `create_epair` (first response-data verb — returns + the epair pair names since the kernel auto-assigns them) +- 0.9.27 — `network_lease.cpp` per-user paths + RCTL umbrella +- 0.9.28 — default flip +- 1.0.0 — setuid removed + +### Tests + +1 new ATF test (`set_iface_inet_addr_minimal`) covering happy +path + 3 reject cases (bad iface, bad addr, out-of-range +prefix). `verb_token_roundtrips_for_every_verb` updated. +Suite: 1298 → **1299**. + +### Files + +Same set as 0.9.23/0.9.24: `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.24] — 2026-05-09 **Rootless track, bridge membership verbs.** Twenty-fifth diff --git a/cli/args.cpp b/cli/args.cpp index e113e87..d2e45e5 100644 --- a/cli/args.cpp +++ b/cli/args.cpp @@ -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.24" << std::endl; + std::cout << "crate 0.9.25" << std::endl; exit(0); } else if (auto argShort = isShort(argv[a])) { switch (argShort) { @@ -764,7 +764,7 @@ Args parseArguments(int argc, char** argv, unsigned &processed) { args.logProgress = true; break; case 'V': - std::cout << "crate 0.9.24" << std::endl; + std::cout << "crate 0.9.25" << std::endl; exit(0); default: err("unsupported short option '%s'", argv[a]); diff --git a/daemon/privops_handlers.cpp b/daemon/privops_handlers.cpp index 82ed291..0b9725d 100644 --- a/daemon/privops_handlers.cpp +++ b/daemon/privops_handlers.cpp @@ -416,6 +416,19 @@ DispatchResult handleBridgeDelMember(const PrivOpsPure::BridgeDelMemberReq &r) { return {200, PrivOpsWirePure::formatBridgeDelMemberSuccess(r.bridge, r.member)}; } +// --- handleSetIfaceInetAddr (0.9.25) --- + +DispatchResult handleSetIfaceInetAddr(const PrivOpsPure::SetIfaceInetAddrReq &r) { + try { + IfconfigOps::setInetAddr(r.ifname, r.addr, (int)r.prefixLen); + } catch (const std::exception &e) { + return {500, PrivOpsWirePure::formatHandlerError("ifconfig_failed", + e.what())}; + } + return {200, PrivOpsWirePure::formatSetIfaceInetAddrSuccess( + r.ifname, r.addr, r.prefixLen)}; +} + // --- Top-level dispatcher --- namespace { @@ -584,6 +597,14 @@ DispatchResult dispatchPrivOp(Verb v, const std::string &body, return {400, PrivOpsWirePure::formatValidateError(e)}; return handleBridgeDelMember(r); } + case Verb::SetIfaceInetAddr: { + PrivOpsPure::SetIfaceInetAddrReq r; + if (auto e = PrivOpsWirePure::parseSetIfaceInetAddr(body, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateSetIfaceInetAddr(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleSetIfaceInetAddr(r); + } default: return PrivOpsWirePure::parseValidateAndDispatch(v, body); } @@ -746,6 +767,14 @@ DispatchResult dispatchPrivOpFromMap(const PrivOpsNvPure::FieldMap &m, return {400, PrivOpsWirePure::formatValidateError(e)}; return handleBridgeDelMember(r); } + case Verb::SetIfaceInetAddr: { + PrivOpsPure::SetIfaceInetAddrReq r; + if (auto e = PrivOpsNvPure::parseSetIfaceInetAddr(m, r); !e.empty()) + return {400, PrivOpsWirePure::formatParseError(e)}; + if (auto e = PrivOpsPure::validateSetIfaceInetAddr(r); !e.empty()) + return {400, PrivOpsWirePure::formatValidateError(e)}; + return handleSetIfaceInetAddr(r); + } case Verb::Unknown: return {404, std::string("{\"error\":\"unknown or missing 'verb' field\"}")}; diff --git a/daemon/privops_handlers.h b/daemon/privops_handlers.h index 99d8a31..f18fd6b 100644 --- a/daemon/privops_handlers.h +++ b/daemon/privops_handlers.h @@ -157,4 +157,8 @@ PrivOpsWirePure::DispatchResult handleDisableIfaceOffload(const PrivOpsPure::Dis PrivOpsWirePure::DispatchResult handleBridgeAddMember(const PrivOpsPure::BridgeAddMemberReq &r); PrivOpsWirePure::DispatchResult handleBridgeDelMember(const PrivOpsPure::BridgeDelMemberReq &r); +// 0.9.25: set host-side IPv4 address. Wraps +// IfconfigOps::setInetAddr (3-arg primitive: iface + addr + prefixLen). +PrivOpsWirePure::DispatchResult handleSetIfaceInetAddr(const PrivOpsPure::SetIfaceInetAddrReq &r); + } // namespace Crated diff --git a/lib/privops_client.h b/lib/privops_client.h index 26b64e7..13741af 100644 --- a/lib/privops_client.h +++ b/lib/privops_client.h @@ -118,6 +118,11 @@ PrivOpsNvPure::FieldMap buildBridgeAddMember(const std::string &bridge, PrivOpsNvPure::FieldMap buildBridgeDelMember(const std::string &bridge, const std::string &member); +// 0.9.25: set host-side IPv4 address. +PrivOpsNvPure::FieldMap buildSetIfaceInetAddr(const std::string &ifname, + const std::string &addr, + unsigned prefixLen); + // --- Wire transport (FreeBSD-only) --- struct Response { diff --git a/lib/privops_client_pure.cpp b/lib/privops_client_pure.cpp index ea1353f..7ba68d3 100644 --- a/lib/privops_client_pure.cpp +++ b/lib/privops_client_pure.cpp @@ -251,4 +251,15 @@ PrivOpsNvPure::FieldMap buildBridgeDelMember(const std::string &bridge, }; } +PrivOpsNvPure::FieldMap buildSetIfaceInetAddr(const std::string &ifname, + const std::string &addr, + unsigned prefixLen) { + return { + {"verb", "set_iface_inet_addr"}, + {"ifname", ifname}, + {"addr", addr}, + {"prefix_len", toString(prefixLen)}, + }; +} + } // namespace PrivOpsClient diff --git a/lib/privops_nv_pure.cpp b/lib/privops_nv_pure.cpp index f730587..fe36efe 100644 --- a/lib/privops_nv_pure.cpp +++ b/lib/privops_nv_pure.cpp @@ -226,6 +226,14 @@ std::string parseBridgeDelMember(const FieldMap &m, return ""; } +std::string parseSetIfaceInetAddr(const FieldMap &m, + PrivOpsPure::SetIfaceInetAddrReq &out) { + if (auto e = requireString(m, "ifname", out.ifname); !e.empty()) return e; + if (auto e = requireString(m, "addr", out.addr); !e.empty()) return e; + if (auto e = requireUnsigned(m, "prefix_len", out.prefixLen); !e.empty()) return e; + return ""; +} + // --- Verb routing --- PrivOpsPure::Verb extractVerb(const FieldMap &m) { diff --git a/lib/privops_nv_pure.h b/lib/privops_nv_pure.h index c1c312c..50bf8b1 100644 --- a/lib/privops_nv_pure.h +++ b/lib/privops_nv_pure.h @@ -127,6 +127,8 @@ std::string parseBridgeAddMember(const FieldMap &m, PrivOpsPure::BridgeAddMemberReq &out); std::string parseBridgeDelMember(const FieldMap &m, PrivOpsPure::BridgeDelMemberReq &out); +std::string parseSetIfaceInetAddr(const FieldMap &m, + PrivOpsPure::SetIfaceInetAddrReq &out); // --- Verb routing --- diff --git a/lib/privops_pure.cpp b/lib/privops_pure.cpp index 2579b8f..77afcd7 100644 --- a/lib/privops_pure.cpp +++ b/lib/privops_pure.cpp @@ -89,6 +89,7 @@ const char *verbName(Verb v) { case Verb::DisableIfaceOffload: return "disable_iface_offload"; case Verb::BridgeAddMember: return "bridge_add_member"; case Verb::BridgeDelMember: return "bridge_del_member"; + case Verb::SetIfaceInetAddr: return "set_iface_inet_addr"; case Verb::Unknown: return "unknown"; } return "unknown"; @@ -113,6 +114,7 @@ Verb parseVerb(const std::string &name) { if (name == "disable_iface_offload") return Verb::DisableIfaceOffload; 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; return Verb::Unknown; } @@ -493,4 +495,14 @@ std::string validateBridgeDelMember(const BridgeDelMemberReq &r) { return ""; } +std::string validateSetIfaceInetAddr(const SetIfaceInetAddrReq &r) { + if (auto e = validateIfaceName(r.ifname); !e.empty()) return "ifname: " + e; + if (r.prefixLen > 32) return "prefix_len out of range (0..32)"; + // Reuse validateIpv4Cidr by reassembling addr+prefix; cheaper than + // duplicating the IPv4 octet logic. + std::string cidr = r.addr + "/" + std::to_string(r.prefixLen); + if (auto e = validateIpv4Cidr(cidr); !e.empty()) return "addr: " + e; + return ""; +} + } // namespace PrivOpsPure diff --git a/lib/privops_pure.h b/lib/privops_pure.h index 680e605..d8b0348 100644 --- a/lib/privops_pure.h +++ b/lib/privops_pure.h @@ -118,6 +118,13 @@ enum class Verb { // operator-supplied directly. BridgeAddMember, BridgeDelMember, + + // 0.9.25: set host-side IPv4 address on a non-jail iface. + // Wraps IfconfigOps::setInetAddr (three-arg primitive: + // iface + addr + prefixLen). Targets the run_net.cpp + // 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, }; // Returns the verb's canonical wire-format token (lowercase, no @@ -234,6 +241,13 @@ struct BridgeDelMemberReq { std::string member; }; +// 0.9.25: set host-side IPv4 address on a non-jail iface. +struct SetIfaceInetAddrReq { + std::string ifname; + std::string addr; // IPv4 address (no /prefix here; bare addr) + unsigned prefixLen = 32; // 0..32 +}; + // --- Per-verb validators --- // // Each `validate*(req)` returns "" on success, otherwise a one-line @@ -260,6 +274,7 @@ std::string validateSetIfaceUp(const SetIfaceUpReq &r); std::string validateDisableIfaceOffload(const DisableIfaceOffloadReq &r); std::string validateBridgeAddMember(const BridgeAddMemberReq &r); std::string validateBridgeDelMember(const BridgeDelMemberReq &r); +std::string validateSetIfaceInetAddr(const SetIfaceInetAddrReq &r); // --- Field-level validators (exposed for tests + reuse) --- // diff --git a/lib/privops_wire_pure.cpp b/lib/privops_wire_pure.cpp index 7561abe..b56e891 100644 --- a/lib/privops_wire_pure.cpp +++ b/lib/privops_wire_pure.cpp @@ -362,6 +362,14 @@ std::string parseBridgeDelMember(const std::string &body, return ""; } +std::string parseSetIfaceInetAddr(const std::string &body, + PrivOpsPure::SetIfaceInetAddrReq &out) { + if (auto e = requireStringField(body, "ifname", out.ifname); !e.empty()) return e; + if (auto e = requireStringField(body, "addr", out.addr); !e.empty()) return e; + if (auto e = requireUnsignedField(body, "prefix_len", out.prefixLen); !e.empty()) return e; + return ""; +} + // --- Verb routing helper --- PrivOpsPure::Verb parseVerbFromPath(const std::string &path) { @@ -636,6 +644,18 @@ std::string formatBridgeDelMemberSuccess(const std::string &bridge, return o.str(); } +std::string formatSetIfaceInetAddrSuccess(const std::string &ifname, + const std::string &addr, + unsigned prefixLen) { + std::ostringstream o; + o << "{\"set\":true" + << ",\"ifname\":\"" << escape(ifname) << "\"" + << ",\"addr\":\"" << escape(addr) << "\"" + << ",\"prefix_len\":" << prefixLen + << "}"; + return o.str(); +} + DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v, const std::string &body) { using namespace PrivOpsPure; @@ -676,6 +696,8 @@ DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v, return runVerb(body, v, parseBridgeAddMember, validateBridgeAddMember); case Verb::BridgeDelMember: return runVerb(body, v, parseBridgeDelMember, validateBridgeDelMember); + case Verb::SetIfaceInetAddr: + return runVerb(body, v, parseSetIfaceInetAddr, validateSetIfaceInetAddr); case Verb::Unknown: break; } diff --git a/lib/privops_wire_pure.h b/lib/privops_wire_pure.h index 57fb72e..3133b7d 100644 --- a/lib/privops_wire_pure.h +++ b/lib/privops_wire_pure.h @@ -168,6 +168,9 @@ std::string parseBridgeAddMember(const std::string &body, std::string parseBridgeDelMember(const std::string &body, PrivOpsPure::BridgeDelMemberReq &out); +std::string parseSetIfaceInetAddr(const std::string &body, + PrivOpsPure::SetIfaceInetAddrReq &out); + // --- Verb routing helper --- // // Parse the URL path's verb segment. The route pattern is @@ -283,4 +286,9 @@ std::string formatBridgeAddMemberSuccess(const std::string &bridge, std::string formatBridgeDelMemberSuccess(const std::string &bridge, const std::string &member); +// 0.9.25: 200 OK body for set_iface_inet_addr. +std::string formatSetIfaceInetAddrSuccess(const std::string &ifname, + const std::string &addr, + unsigned prefixLen); + } // namespace PrivOpsWirePure diff --git a/lib/run_net.cpp b/lib/run_net.cpp index 7889cb0..bc9d519 100644 --- a/lib/run_net.cpp +++ b/lib/run_net.cpp @@ -138,6 +138,26 @@ static void bridgeDelMemberPrivopsOrLocal(const std::string &bridge, IfconfigOps::bridgeDelMember(bridge, member); } +// 0.9.25: privops-aware setInetAddr. +static void setInetAddrPrivopsOrLocal(const std::string &iface, + const std::string &addr, + int prefixLen) { + std::string sock = PrivOpsClient::detectSocketPath(); + if (!sock.empty()) { + auto resp = PrivOpsClient::sendRequest(sock, + PrivOpsClient::buildSetIfaceInetAddr(iface, addr, + (unsigned)prefixLen)); + if (!resp.transportError.empty()) + ERR2("run_net", "privops set_iface_inet_addr '" << iface + << "' transport error: " << resp.transportError) + if (resp.status >= 400) + ERR2("run_net", "privops set_iface_inet_addr '" << iface + << "' failed (status " << resp.status << "): " << resp.body) + return; + } + IfconfigOps::setInetAddr(iface, addr, prefixLen); +} + GatewayInfo detectGateway() { GatewayInfo gw; @@ -206,7 +226,7 @@ EpairInfo createEpair(int jid, const std::string &jidStr, // set the IP addresses on the jail epair execInJail({CRATE_PATH_IFCONFIG, info.ifaceB, "inet", info.ipB, "netmask", "0xfffffffe"}, "set up IP jail epair addresses"); - IfconfigOps::setInetAddr(info.ifaceA, info.ipA, 31); + setInetAddrPrivopsOrLocal(info.ifaceA, info.ipA, 31); // set default route in jail execInJail({CRATE_PATH_ROUTE, "add", "default", info.ipA}, "set default route in jail"); diff --git a/tests/unit/privops_pure_test.cpp b/tests/unit/privops_pure_test.cpp index 1070bbf..e226b4f 100644 --- a/tests/unit/privops_pure_test.cpp +++ b/tests/unit/privops_pure_test.cpp @@ -25,6 +25,7 @@ ATF_TEST_CASE_BODY(verb_token_roundtrips_for_every_verb) { Verb::AddIpfwRule, Verb::RemoveIpfwRule, Verb::SetIfaceUp, Verb::DisableIfaceOffload, Verb::BridgeAddMember, Verb::BridgeDelMember, + Verb::SetIfaceInetAddr, }; for (Verb v : verbs) { std::string token = verbName(v); @@ -539,6 +540,29 @@ ATF_TEST_CASE_BODY(bridge_del_member_minimal) { ATF_REQUIRE(!validateBridgeDelMember(r).empty()); } +ATF_TEST_CASE_WITHOUT_HEAD(set_iface_inet_addr_minimal); +ATF_TEST_CASE_BODY(set_iface_inet_addr_minimal) { + SetIfaceInetAddrReq r; + r.ifname = "epair0a"; + r.addr = "10.0.0.1"; + r.prefixLen = 31; + ATF_REQUIRE_EQ(validateSetIfaceInetAddr(r), std::string()); + + // Bad iface + r.ifname = "name;rm"; + ATF_REQUIRE(!validateSetIfaceInetAddr(r).empty()); + + // Bad addr + r.ifname = "epair0a"; + r.addr = "999.999.999.999"; + ATF_REQUIRE(!validateSetIfaceInetAddr(r).empty()); + + // Out-of-range prefix + r.addr = "10.0.0.1"; + r.prefixLen = 33; + ATF_REQUIRE(!validateSetIfaceInetAddr(r).empty()); +} + ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, verb_token_roundtrips_for_every_verb); ATF_ADD_TEST_CASE(tcs, verb_unknown_token_returns_unknown); @@ -600,4 +624,5 @@ ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, disable_iface_offload_minimal); ATF_ADD_TEST_CASE(tcs, bridge_add_member_minimal); ATF_ADD_TEST_CASE(tcs, bridge_del_member_minimal); + ATF_ADD_TEST_CASE(tcs, set_iface_inet_addr_minimal); }