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.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
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.24" << std::endl;
std::cout << "crate 0.9.25" << 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.24" << std::endl;
std::cout << "crate 0.9.25" << std::endl;
exit(0);
default:
err("unsupported short option '%s'", argv[a]);
Expand Down
29 changes: 29 additions & 0 deletions daemon/privops_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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\"}")};
Expand Down
4 changes: 4 additions & 0 deletions daemon/privops_handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions lib/privops_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions lib/privops_client_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions lib/privops_nv_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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 @@ -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 ---

Expand Down
12 changes: 12 additions & 0 deletions lib/privops_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
}

Expand Down Expand Up @@ -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
15 changes: 15 additions & 0 deletions lib/privops_pure.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) ---
//
Expand Down
22 changes: 22 additions & 0 deletions lib/privops_wire_pure.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -676,6 +696,8 @@ DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v,
return runVerb<BridgeAddMemberReq>(body, v, parseBridgeAddMember, validateBridgeAddMember);
case Verb::BridgeDelMember:
return runVerb<BridgeDelMemberReq>(body, v, parseBridgeDelMember, validateBridgeDelMember);
case Verb::SetIfaceInetAddr:
return runVerb<SetIfaceInetAddrReq>(body, v, parseSetIfaceInetAddr, validateSetIfaceInetAddr);
case Verb::Unknown:
break;
}
Expand Down
8 changes: 8 additions & 0 deletions lib/privops_wire_pure.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
22 changes: 21 additions & 1 deletion lib/run_net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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");
Expand Down
Loading
Loading