Sockets — guide
Design: socket-deployment.md. Feature: sockets (off by default).
skidbladnir = { version = "0.1", features = ["sockets"] }
The whole lifecycle
use skidbladnir::socket::{socket_for, BuildThingType, Mode, FixtureKind, FixtureHealth};
// 1. Type in, socket out. No per-cell config — this is the registry the pane calls.
let ty = BuildThingType::from_kind("tunnr-appliance").unwrap(); // from build-variants.toml
let socket = socket_for(ty);
// 2. The plan + topology render BEFORE anything boots (so the pane is never empty).
let plan = socket.plan(Mode::Test);
for row in plan.topology() {
println!("{:<16} {:<28} {}", row.kind.as_str(), row.detail,
row.endpoint.as_deref().unwrap_or("-"));
}
// 3. Light it. Fixtures are realized; health is Ready / NotApplicable / Down / NotImplemented.
let mut dep = socket.light(Mode::Test);
let netdev = dep.qemu_netdev_arg().expect("this socket runs on a shared net");
// -> "user,id=n0,net=10.9.0.0/24,host=10.9.0.1,restrict=off,dhcpstart=10.9.0.100,dns=10.9.0.1"
// Prepend `-netdev ` and pair it with `-device virtio-net` on the guest.
// 4. The operator adds a WireGuard key (Test mode only).
dep.add_wg_peer(peer_pubkey_b64, Some("10.9.0.1:51820".into()), vec!["10.9.0.0/24".into()])?;
// 5. Feed the captured boot console back in. THIS is what turns things green.
dep.confirm(&console);
assert_eq!(dep.fixture(FixtureKind::Dhcp).unwrap().health, FixtureHealth::Serving);
// 6. Assert against the node.
for probe in socket.probes() {
let v = probe.run(&console);
println!("{}: {} — {}", v.probe, if v.passed { "PASS" } else { "FAIL" }, v.detail);
}
// 7. Render, then tear down (consumes it; the operator's keys go with it).
let json = dep.json();
let order = dep.teardown();
Reading the health of a fixture
| Health | ok() |
What it means |
Ready |
✔ | provisioned, will serve — not yet observed serving |
Serving |
✔ | observed serving, from real console evidence |
NotApplicable |
✔ | off in this mode by design (Prod DHCP). Serves nothing, fakes nothing |
Down(why) |
✘ | it failed, or Prod refused it. why() quotes the proof |
NotImplemented(why) |
✘ | nothing provisions this yet. why() names the missing seam |
dep.all_ok() is true only if every fixture is in the top three. dep.refusals() gives you (kind, why) for the bottom two — render that, not a spinner. Every why carries a TO FIX: clause (socket::REMEDY_MARKER); split on it to show the remedy as its own line.
Reading the evidence
confirm() classifies the console per fixture into an Evidence:
Evidence |
meaning |
Served |
proof it worked — a bound lease in the pool, a completed handshake |
Failed(line) |
proof it did not, quoting the line. Wins over success evidence anywhere else, and downgrades even Serving |
Unknown(why) |
does not settle it, and says why ("a DHCP attempt but no bound lease in the pool…") |
The readers are public and independently testable: NetSpec::dhcp_evidence, wg_evidence, dns_evidence, plus NetSpec::leased_address / in_dhcp_pool and parse_handshake_age_secs.
WireGuard peer states
Handshake |
is_up() |
when |
NeverConnected |
✘ | added, nothing observed |
Handshaking |
✘ | an initiation was seen, no completion |
Established { last_handshake_secs } |
✔ if fresh | a completed handshake |
Established past 180 s |
✘ (as_str() = "stale") |
older than WireGuard's REJECT_AFTER_TIME |
Lost |
✘ | it was up, and the tunnel is now proven dead |
Handshake::STALE_AFTER_SECS is 180 — WireGuard's own REJECT_AFTER_TIME. Staleness can only be detected when the console carries a wg show capture (latest handshake: … ago); a plain boot console has no age signal, so confirm records the observation as fresh.
Adding things
A fixture: impl Fixture (at minimum kind(); override runtime() and light_health() if it has preconditions or its own runtime) → add one arm to fixture_for_kind. If a socket should compose it, add the kind to that socket's arm in SocketPlan::for_type. Nothing else — the plan and the deployment stay in lockstep automatically.
A socket: add an arm to SocketPlan::for_type (its fixture list, console count) → impl Socket (usually just accepts + plan; override probes() if it has cluster-level assertions) → add an arm to socket_for. Zero pane changes.
A probe: impl Probe { fn name(&self) -> &str; fn run(&self, console: &str) -> ProbeVerdict } → return it from the relevant socket's probes().
A network backend: impl NetworkBackend. Override is_test_grade() -> false only if it really is production-fit — the default is true, and Mode::Prod refuses every fixture a test-grade backend realizes.
Gotchas
light()never returnsServing. If you expected green and gotReady, you have not calledconfirm()with a console yet.Mode::ProdonTunnrTestSocketisall_ok() == falseon purpose. The only net backend is slirp, which is test-grade. Readrefusals(); it explains each one. See the design doc.netis anOption. Single-boot sockets (exe/console/kvm/iso/container) have none, andqemu_netdev_arg()returnsNonefor them.confirm()can go backwards. Failure evidence downgrades even aServingfixture, and takes the peer rows with it (Established→Lost). Deliberate: a tunnel that dropped is not still serving.- Silence changes nothing. An uninformative console leaves every fixture exactly as it was — and
Evidence::Unknown(why)tells you what it looked for. - An attempt is not a success.
udhcpc: broadcasting select for 10.9.0.157is a DHCPREQUEST, not a lease;Sending handshake_initiationis not a completed handshake. If you are writing a fake console for a test, uselease of … obtainedandReceived handshake_response, or crib from thetranscriptsmodule. - A stale handshake is not up. Past 180 s (
REJECT_AFTER_TIME)is_up()is false.