From ca6108a4a4a5f7c43729581a4d6cc1b5560dbb5e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 14:01:39 +0000 Subject: [PATCH] =?UTF-8?q?0.9.26=20=E2=80=94=20rootless:=20create=5Fepair?= =?UTF-8?q?=20verb=20(first=20response-data=20verb)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twenty-seventh 0.9.x release. Wraps IfconfigOps::createEpair — kernel auto-assigns next free epair unit number, verb returns the A/B iface names so client can plumb them downstream. create_epair — no request fields. Response body: {"created":true,"a":"epair17a","b":"epair17b"} First verb whose response carries non-trivial data. Previously all verbs returned status + confirmation body the client could ignore. create_epair is different because operator NEEDS the assigned names for next steps (move B to vnet, attach A to bridge). Wire: daemon handler builds JSON via formatCreateEpairSuccess. Client (run_net.cpp) calls existing PrivOpsWirePure:: extractStringField twice (for a + b). No new JSON parser needed. CLI wiring: createEpairPrivopsOrLocal returning std::pair. Two call sites migrate (lines 239 / 518). Full host-side iface plumbing via privops now achieved: createEpair (this) / disableOffload (0.9.23) / setUp (0.9.23) / setInetAddr (0.9.25) / bridgeAddMember (0.9.24) — all 5 IfconfigOps ops crate run uses have privops paths. With 0.9.22 createJail and 0.9.21 removeJail, crate run and crate stop no longer need root for any privileged step when privops socket detected; legacy setuid fallback is now optional. 2 new ATF tests + verb_token_roundtrips updated. Suite: 1299 -> 1301. Remaining: network_lease per-user (0.9.27), default flip (0.9.28), setuid removed (1.0.0). --- CHANGELOG.md | 115 ++++++++++++++++++++++++++ cli/args.cpp | 4 +- daemon/privops_handlers.cpp | 30 +++++++ daemon/privops_handlers.h | 5 ++ lib/privops_client.h | 6 ++ lib/privops_client_pure.cpp | 6 ++ lib/privops_nv_pure.cpp | 5 ++ lib/privops_nv_pure.h | 2 + lib/privops_pure.cpp | 8 ++ lib/privops_pure.h | 14 ++++ lib/privops_wire_pure.cpp | 18 ++++ lib/privops_wire_pure.h | 12 +++ lib/run_net.cpp | 32 ++++++- tests/unit/privops_pure_test.cpp | 10 ++- tests/unit/privops_wire_pure_test.cpp | 16 ++++ 15 files changed, 278 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0680eb..81e50bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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":"","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`. 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 diff --git a/cli/args.cpp b/cli/args.cpp index d2e45e5..157446e 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.25" << std::endl; + std::cout << "crate 0.9.26" << 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.25" << std::endl; + std::cout << "crate 0.9.26" << 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 0b9725d..29d1471 100644 --- a/daemon/privops_handlers.cpp +++ b/daemon/privops_handlers.cpp @@ -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 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 { @@ -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); } @@ -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\"}")}; diff --git a/daemon/privops_handlers.h b/daemon/privops_handlers.h index f18fd6b..7445d23 100644 --- a/daemon/privops_handlers.h +++ b/daemon/privops_handlers.h @@ -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 diff --git a/lib/privops_client.h b/lib/privops_client.h index 13741af..1d3bebf 100644 --- a/lib/privops_client.h +++ b/lib/privops_client.h @@ -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 { diff --git a/lib/privops_client_pure.cpp b/lib/privops_client_pure.cpp index 7ba68d3..2c6a51e 100644 --- a/lib/privops_client_pure.cpp +++ b/lib/privops_client_pure.cpp @@ -262,4 +262,10 @@ PrivOpsNvPure::FieldMap buildSetIfaceInetAddr(const std::string &ifname, }; } +PrivOpsNvPure::FieldMap buildCreateEpair() { + return { + {"verb", "create_epair"}, + }; +} + } // namespace PrivOpsClient diff --git a/lib/privops_nv_pure.cpp b/lib/privops_nv_pure.cpp index fe36efe..b16d8bf 100644 --- a/lib/privops_nv_pure.cpp +++ b/lib/privops_nv_pure.cpp @@ -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) { diff --git a/lib/privops_nv_pure.h b/lib/privops_nv_pure.h index 50bf8b1..774d59a 100644 --- a/lib/privops_nv_pure.h +++ b/lib/privops_nv_pure.h @@ -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 --- diff --git a/lib/privops_pure.cpp b/lib/privops_pure.cpp index 77afcd7..4b67874 100644 --- a/lib/privops_pure.cpp +++ b/lib/privops_pure.cpp @@ -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"; @@ -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; } @@ -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 diff --git a/lib/privops_pure.h b/lib/privops_pure.h index d8b0348..34f2895 100644 --- a/lib/privops_pure.h +++ b/lib/privops_pure.h @@ -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 @@ -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 (epaira / epairb). +struct CreateEpairReq { +}; + // --- Per-verb validators --- // // Each `validate*(req)` returns "" on success, otherwise a one-line @@ -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) --- // diff --git a/lib/privops_wire_pure.cpp b/lib/privops_wire_pure.cpp index b56e891..4927393 100644 --- a/lib/privops_wire_pure.cpp +++ b/lib/privops_wire_pure.cpp @@ -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) { @@ -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; @@ -698,6 +714,8 @@ DispatchResult parseValidateAndDispatch(PrivOpsPure::Verb v, return runVerb(body, v, parseBridgeDelMember, validateBridgeDelMember); case Verb::SetIfaceInetAddr: return runVerb(body, v, parseSetIfaceInetAddr, validateSetIfaceInetAddr); + case Verb::CreateEpair: + return runVerb(body, v, parseCreateEpair, validateCreateEpair); case Verb::Unknown: break; } diff --git a/lib/privops_wire_pure.h b/lib/privops_wire_pure.h index 3133b7d..23ae800 100644 --- a/lib/privops_wire_pure.h +++ b/lib/privops_wire_pure.h @@ -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 @@ -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":"","b":""} +std::string formatCreateEpairSuccess(const std::string &ifaceA, + const std::string &ifaceB); + } // namespace PrivOpsWirePure diff --git a/lib/run_net.cpp b/lib/run_net.cpp index bc9d519..7e1ee50 100644 --- a/lib/run_net.cpp +++ b/lib/run_net.cpp @@ -11,6 +11,7 @@ #include "pfctl_ops.h" #include "pathnames.h" #include "privops_client.h" +#include "privops_wire_pure.h" #include "util.h" #include "err.h" @@ -138,6 +139,33 @@ static void bridgeDelMemberPrivopsOrLocal(const std::string &bridge, IfconfigOps::bridgeDelMember(bridge, member); } +// 0.9.26: privops-aware createEpair. Returns the kernel-assigned +// pair names. Parses the daemon's JSON response body via +// PrivOpsWirePure::extractStringField — no full JSON parser +// needed for this two-field response. +static std::pair +createEpairPrivopsOrLocal() { + std::string sock = PrivOpsClient::detectSocketPath(); + if (!sock.empty()) { + auto resp = PrivOpsClient::sendRequest(sock, + PrivOpsClient::buildCreateEpair()); + if (!resp.transportError.empty()) + ERR2("run_net", "privops create_epair transport error: " + << resp.transportError) + if (resp.status >= 400) + ERR2("run_net", "privops create_epair failed (status " + << resp.status << "): " << resp.body) + std::string a, b; + auto ar = PrivOpsWirePure::extractStringField(resp.body, "a", a); + auto br = PrivOpsWirePure::extractStringField(resp.body, "b", b); + if (ar != PrivOpsWirePure::kPresent || br != PrivOpsWirePure::kPresent) + ERR2("run_net", "privops create_epair response missing 'a' or 'b' " + "fields: " << resp.body) + return {a, b}; + } + return IfconfigOps::createEpair(); +} + // 0.9.25: privops-aware setInetAddr. static void setInetAddrPrivopsOrLocal(const std::string &iface, const std::string &addr, @@ -208,7 +236,7 @@ EpairInfo createEpair(int jid, const std::string &jidStr, execInJail({CRATE_PATH_IFCONFIG, "lo0", "inet", "127.0.0.1"}, "set up the lo0 interface in jail"); // create networking interface (uses libifconfig with shell fallback) - auto epairPair = IfconfigOps::createEpair(); + auto epairPair = createEpairPrivopsOrLocal(); info.ifaceA = epairPair.first; info.ifaceB = epairPair.second; info.num = Util::toUInt(info.ifaceA.substr(5/*skip epair*/, info.ifaceA.size()-5-1)); @@ -487,7 +515,7 @@ BridgeInfo createBridgeEpair(int jid, const std::string &jidStr, execInJail({CRATE_PATH_IFCONFIG, "lo0", "inet", "127.0.0.1"}, "set up the lo0 interface in jail"); // create epair - auto epairPair = IfconfigOps::createEpair(); + auto epairPair = createEpairPrivopsOrLocal(); info.ifaceA = epairPair.first; info.ifaceB = epairPair.second; info.num = Util::toUInt(info.ifaceA.substr(5, info.ifaceA.size()-5-1)); diff --git a/tests/unit/privops_pure_test.cpp b/tests/unit/privops_pure_test.cpp index e226b4f..8173c59 100644 --- a/tests/unit/privops_pure_test.cpp +++ b/tests/unit/privops_pure_test.cpp @@ -25,7 +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, + Verb::SetIfaceInetAddr, Verb::CreateEpair, }; for (Verb v : verbs) { std::string token = verbName(v); @@ -540,6 +540,13 @@ ATF_TEST_CASE_BODY(bridge_del_member_minimal) { ATF_REQUIRE(!validateBridgeDelMember(r).empty()); } +ATF_TEST_CASE_WITHOUT_HEAD(create_epair_no_fields_required); +ATF_TEST_CASE_BODY(create_epair_no_fields_required) { + CreateEpairReq r; + // Validator always succeeds — no inputs to check. + ATF_REQUIRE_EQ(validateCreateEpair(r), std::string()); +} + ATF_TEST_CASE_WITHOUT_HEAD(set_iface_inet_addr_minimal); ATF_TEST_CASE_BODY(set_iface_inet_addr_minimal) { SetIfaceInetAddrReq r; @@ -625,4 +632,5 @@ ATF_INIT_TEST_CASES(tcs) { 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); + ATF_ADD_TEST_CASE(tcs, create_epair_no_fields_required); } diff --git a/tests/unit/privops_wire_pure_test.cpp b/tests/unit/privops_wire_pure_test.cpp index db202d6..c4de55a 100644 --- a/tests/unit/privops_wire_pure_test.cpp +++ b/tests/unit/privops_wire_pure_test.cpp @@ -517,6 +517,21 @@ ATF_TEST_CASE_BODY(format_destroy_jail_success) { ATF_REQUIRE(body.find("\"created\":") == std::string::npos); } +ATF_TEST_CASE_WITHOUT_HEAD(format_create_epair_response_extracts); +ATF_TEST_CASE_BODY(format_create_epair_response_extracts) { + // 0.9.26: first response-data verb. Lock down that the body + // shape `{"created":true,"a":"epair0a","b":"epair0b"}` is + // re-extractable via extractStringField (the path the client + // in run_net.cpp uses). + std::string body = formatCreateEpairSuccess("epair17a", "epair17b"); + ATF_REQUIRE(body.find("\"created\":true") != std::string::npos); + std::string a, b; + ATF_REQUIRE_EQ(extractStringField(body, "a", a), std::string(kPresent)); + ATF_REQUIRE_EQ(a, std::string("epair17a")); + ATF_REQUIRE_EQ(extractStringField(body, "b", b), std::string(kPresent)); + ATF_REQUIRE_EQ(b, std::string("epair17b")); +} + // --- parseValidateAndDispatch --- ATF_TEST_CASE_WITHOUT_HEAD(dispatch_unknown_returns_404); @@ -625,6 +640,7 @@ ATF_INIT_TEST_CASES(tcs) { ATF_ADD_TEST_CASE(tcs, format_remove_ipfw_rule_success); ATF_ADD_TEST_CASE(tcs, format_create_jail_success); ATF_ADD_TEST_CASE(tcs, format_destroy_jail_success); + ATF_ADD_TEST_CASE(tcs, format_create_epair_response_extracts); ATF_ADD_TEST_CASE(tcs, dispatch_unknown_returns_404); ATF_ADD_TEST_CASE(tcs, dispatch_parse_error_returns_400);