diff --git a/CHANGELOG.md b/CHANGELOG.md index e693440e..d177c55d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -190,9 +190,9 @@ fixed in core; see `THREAT_MODEL.md`. ## Hosts 0.4.0 — Unreleased -`tines`, `tines-store`, `tines-bt`, `tines-ros2` and `tines-sim` move to 0.4.0 -and depend on `tines-core = "1"`. Like tines-core 1.0.0, nothing here is -tagged or published yet. +`tines`, `tines-store`, `tines-bt`, `tines-bus`, `tines-ros2` and +`tines-sim` move to 0.4.0 and depend on `tines-core = "1"`. Like tines-core +1.0.0, nothing here is tagged or published yet. The 0.3.0 release shipped only `tines-core` and `tines-sim`. The four other host crates were added afterwards, during the fleet milestone, and carried the @@ -339,8 +339,9 @@ milestone's host work (F2–F5, F17). ### Known issues -- The service talks to exactly one configured `peer`. The multi-peer - allowlist the fleet-fixes spec describes isn't wired up (F3). +- The service talks to exactly one configured `peer` on the BT transport. + The multi-peer allowlist the fleet-fixes spec describes isn't wired up + (F3). The new bus transport drops the limit, as a test medium only. - Neither host flushes on a timer or at shutdown (F18). - The service trims and flushes only after 64 poll rounds that each stored something new. @@ -348,6 +349,44 @@ milestone's host work (F2–F5, F17). - Peer time is unauthenticated until 1.1. An adopted time isn't corrected while the host runs (F19). +### Fleet console (unreleased, on `feat/fleet-console`) + +The desktop console for driving a containerized fleet, and the host pieces +it rides on: + +- **`tines-bus`** (new workspace crate): a host-only transport over a + hub-and-spoke Unix-socket daemon (`tines-busd`). `Addr = NodeId`, like + the BT transport. A client registers its `NodeId`, then sends data + envelopes carrying a 32-byte routing header (`from`, `to`); the daemon + drops any envelope whose `from` isn't that socket's registered id or + whose `to` isn't registered, and forwards the frame verbatim. Envelopes + are length-capped. A bus peer loses the BT transport's single-peer + limit — any registered peer is reachable. It is a test medium for + simulated fleets, not a deployment transport. +- **Transport choice in the service** (tines): config `transport` is + `"bt"` (default) or `"bus"`, with `bus_socket` naming the daemon's + socket. `HostTransport` wraps both, so the service runs BLE or the bus + from the same config file. +- **Widened IPC** (tines): `Observe` gains `to` / `lifetime_ms` for + addressed observations (both set together or neither); new `List` + (observation ids, with a `truncated` flag at the cap) and `Events` + (contacts, drops and drop counts) requests. The new request kinds keep + the existing error posture: a bad request gets an `Error` response, + never a panic or a partial write. +- **Decision C** (core): the additive `NodeEvent::Contact` / + `NodeEvent::Lost` variants, so hosts see peers come and go. `Contact` + fires only for a newly added peer; idle eviction stays silent. The only + sanctioned core change for the console. +- **Fleet console** (`tines-gui`, standalone crate outside the workspace): + a launcher (create a fleet, add nodes, start/stop containers, open node + windows) and a per-container node window (status strip, observation + send, belief read, events feed) driving the service IPC. BT mode runs a + privileged container with the host adapter passed through; sim mode runs + N containers over the bus daemon. Fleet state lives under + `$XDG_STATE_HOME/tines-console/fleets//`, created `0700`. +- **Runtime image** (`Dockerfile.tines`): the two-stage container for the + service plus `tines-busd`; the GUI drives docker as a CLI subprocess. + ## 0.3.0 — 2026-10-18 ### Breaking diff --git a/README.md b/README.md index f65a020c..7acfa602 100644 --- a/README.md +++ b/README.md @@ -560,8 +560,9 @@ A robot runs either the service or the ROS2 node, not both: each owns its - Connections produce `Contact` and `Lost` events. See [`tines-bt/README.md`](tines-bt/README.md). - **`tines`**: the standalone service, for robots without ROS2. - - It runs the exchange on a poll loop and answers `Observe`, `Belief`, - `Events` and `Status` requests on a Unix socket. + - It runs the exchange on a poll loop and answers `Observe` (broadcast or + addressed), `Belief`, `Events`, `List` and `Status` requests on a Unix + socket. - It's configured by a JSON file (below). Library users call `tines::serve(node, socket_path, clock, poll_interval_ms, peer_time_window_ms)`. @@ -578,9 +579,11 @@ typo can't silently fall back to a default. | Key | Default | Meaning | |---------------------------|--------------|---------| | `node_id` | required | this robot's `NodeId`, 32 hex chars | -| `peer` | required | the one peer's `NodeId`, 32 hex chars | +| `peer` | bt only | the one peer's `NodeId`, 32 hex chars | | `log_path` | required | the `LogStore` log file | | `socket_path` | required | the IPC Unix socket | +| `transport` | `"bt"` | `"bt"` for BLE, or `"bus"` for the host bus transport | +| `bus_socket` | bus only | the bus daemon's Unix socket | | `poll_interval_ms` | 20 | exchange poll cadence | | `trusted_clock` | unset | `"system"` to trust the system clock; the only accepted value | | `peer_time_window_ms` | 10000 | how long to collect peer time estimates before adopting the latest | @@ -635,7 +638,8 @@ Peer time is unauthenticated until 1.1, and an adopted time isn't corrected while the host runs; see F19 in [`THREAT_MODEL.md`](THREAT_MODEL.md). **Current limits** (tracked in [`THREAT_MODEL.md`](THREAT_MODEL.md)): -- The service talks to one configured `peer`. +- On the BT transport the service talks to one configured `peer`; the bus + transport (simulated fleets) reaches any peer registered with the daemon. - The hosts don't flush on a timer or at shutdown. The service flushes after 64 productive poll rounds, and both hosts flush when they adopt trusted time. @@ -644,6 +648,44 @@ while the host runs; see F19 in [`THREAT_MODEL.md`](THREAT_MODEL.md). The two-device BLE check in `tines-bt/README.md` hasn't been recorded yet. +## The fleet console (lab desktop) + +`tines-gui/` (a standalone crate outside the workspace, like `fuzz/`) is +the desktop console: an orchestrator that runs a whole fleet in containers. +It is never a node host — the GUI embeds no `Node` and no radio runs in the +GUI process; every node lives in a container the GUI drives over the +service IPC. + +- **Launcher** (`tines-gui` with no arguments): create a fleet, add nodes, + start/stop containers, open a node window. +- **Node window** (`tines-gui --node bt|sim`): one + window per container — a status strip, observation send (broadcast or + addressed, with a lifetime), belief read, and a live events feed + (contacts, drops). It speaks the service IPC; nothing else. +- **Modes.** BT mode runs one privileged container with the host's + Bluetooth adapter passed through, so that node reaches real robots. Sim + mode runs N containers over the bus transport (below), with no radio. +- Fleet state lives under + `$XDG_STATE_HOME/tines-console/fleets//`, created `0700`: one + directory per node with its config, log and IPC socket. + +### The bus transport + +`tines-bus` (a workspace member) is a host-only transport for simulated +fleets: a hub-and-spoke daemon (`tines-busd`) that containers dial over +Unix sockets. Its `Addr` is the `NodeId`, like the BT transport. + +- A client registers its `NodeId` with the daemon, then sends data + envelopes carrying a 32-byte routing header (`from`, `to`). The daemon + drops any envelope whose `from` isn't that socket's registered id or + whose `to` isn't registered, and forwards the frame verbatim — it never + decodes frame contents. +- Envelopes are length-capped; an oversized one is a protocol violation. +- The bus is a **test medium**, not a deployment transport: no encryption, + no authentication beyond the registration check. The service picks it + with `transport = "bus"` plus `bus_socket`, which also drops the BT + transport's single-peer limit — any registered peer is reachable. + ## Crate layout ``` @@ -658,9 +700,14 @@ tines/ tines-store/ std: LogStore, the persistent append-only backend. tines-bt/ std: the BLE transport (BlueZ via bluer). tines/ std: the standalone service with Unix-socket IPC. + tines-bus/ std: the host bus transport — a hub-and-spoke Unix-socket + daemon (`tines-busd`) for simulated fleets. tines-ros2/ std: the ROS2 node (rclrs behind the `ros2` feature). ``` +`tines-gui/` sits outside the workspace (like `fuzz/`): the desktop fleet +console. Build it with `cargo test --manifest-path tines-gui/Cargo.toml`. + The public constants, in `tines_core::consts`, are the wire constants. They are frozen for wire v1: changing one is a `WIRE_VERSION` bump. diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index d004016d..8fa41a06 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -3,7 +3,11 @@ - **Assessed:** `master` at `3b1a644` (tines-core / tines-store / tines-bt / tines-ros2 / tines 0.3.0, fleet-deployment milestone mostly landed) - **Date:** 2026-10-19 -- **Updated:** 2026-10-21 at `411daa0` (`feat/tines-core-1-0`), after the +- **Updated:** 2026-10-25, at `2a917088` (`feat/fleet-console`), after the + fleet console work: F3's single-peer limit is now BT-only (the new bus + transport drops it, as a test medium); F11 gains the widened IPC; F20 + (bus daemon) added. Earlier updates: 2026-10-21 at + `411daa0` (`feat/tines-core-1-0`), after the tines-core 1.0 review fixes landed. Earlier update: 2026-10-20 at `89041ea`, after the fleet fixes. The findings keep their original text; each fixed one ends with a **Fixed** or **Partly fixed** line. §6 adds @@ -240,6 +244,11 @@ for a poll loop, `docs/superpowers/plans/2026-10-18-tines-fleet-deployment.md:20 - No BLE security level is requested (unchanged; 1.1). - No two-radio check of the SeqPacket path is recorded yet. tines-bt/README.md gives the procedure but no result. + - *At `2a917088` (fleet console):* the new bus transport (tines-bus) + drops the single-peer limit — bus peers reach any id registered with + the daemon. That is a **test medium** for simulated fleets over host + Unix sockets, not a radio deployment; on real radios the single-peer + limit stands until the allowlist work lands. **Later (1.1):** authenticate the link (spec §2b). Advertised NodeIds are unauthenticated, so the binding only becomes meaningful once there's a @@ -420,6 +429,11 @@ re-queues what was already sent. an idle client blocks everyone. An `accept` error ends the daemon. An oversized frame (> 1 MiB) gets an error reply without consuming its body, which desynchronizes the stream (contrary to the module docs). +- *At `2a917088` (fleet console):* the IPC widened — `Observe` gained + `to`/`lifetime_ms`, and `List` and `Events` were added — without + changing this posture. The new request kinds validate their inputs, cap + their outputs (`List` truncates with a flag), and answer a bad request + with an `Error` response, never a panic or a partial write. - **Fix:** - Create the socket in a `0700` runtime directory with mode `0600` (or a group-restricted mode). @@ -626,6 +640,38 @@ authenticates that estimate. `time-sync.target`). Signing and verification in 1.1 close the forged case. +### F20 — The bus daemon is a host attack surface (Medium, A3) + +*Added 2026-10-25, with the fleet console's bus transport (tines-bus).* +The bus daemon (`tines-busd`) stands in for the radio in simulated fleets: +N service containers dial one daemon over host Unix sockets and exchange +tines frames through it. It is host-local infrastructure and was built +with the same distrust of input as the IPC server: + +- **Registration is explicit.** A client registers its `NodeId` before + sending data, and the daemon drops (and counts) any envelope whose + `from` isn't that socket's registered id, or whose `to` isn't a + registered id. +- **Envelopes are length-capped** at `MAX_FRAME + 32` header bytes + (`MAX_PAYLOAD`); an oversized length header is a protocol violation that + closes the connection. +- **The daemon decodes nothing beyond the 32-byte routing header.** Frame + contents are forwarded verbatim, so a malformed frame is the receiving + node's problem, exactly as on the radio. +- **The fleet directory is `0700`** (tines-gui `config::fleet_dir` and + per-node dirs), so node sockets, logs and configs under it are private + to the owning user. + +- **Effect (A3):** another local user can neither read nor join a fleet + (directory permissions), nor spoof frames through the daemon (the + registration check). Root can do anything on the host, as always. The + daemon is bench equipment: it carries no encryption and authenticates + nothing, and must not become a deployment transport. +- **Open:** the daemon rate-limits nothing. A buggy or hostile local + client can flood envelopes at the fan-out rate. Accepted for the test + medium; F10-style quotas would be the fix if the daemon ever left the + bench. + ## 7. Threats by STRIDE, mapped to findings | | Threat | Status today | Findings | Planned mitigation | @@ -708,6 +754,7 @@ agreed fuzz budget hasn't been run and recorded yet. | F10–F12 | Host hardening, before a robot leaves the lab | `docs/superpowers/specs/2026-10-19-tines-host-hardening.md` | Open | | F6, F8, F9, F13 | tines-core 1.0 | `docs/superpowers/specs/2026-10-18-tines-1-0-design.md` §10 | Fixed (§1); F13's Reassembler displacement accepted | | F19 | Hosts and tines-store (decision J, plausibility bounds); authentication in 1.1 | 1.0 spec §9, decision J; §2a | Partly mitigated (§1) | +| F20 | Fleet console: the bus daemon's posture (registration, caps, 0700 dirs) is built in; per-peer rate limits are open | fleet console design | Mitigated by design (§6) | | F7, F14, §8 | 1.1 (threat model, link authentication, signing) | 1.0 spec §2a, §2b | Open | | F15, F16 | Accepted for now | — | — | diff --git a/tines-bus/src/lib.rs b/tines-bus/src/lib.rs index fc7e6992..030c34cc 100644 --- a/tines-bus/src/lib.rs +++ b/tines-bus/src/lib.rs @@ -1,9 +1,9 @@ //! The tines bus transport: exchange wire frames between nodes over a host //! IPC bus (Unix sockets), identity-preserving (`Addr = NodeId`). //! -//! [`BusTransport`] is the tines [`Transport`] implementation; [`daemon`] is -//! the hub that routes frames between all connected nodes. The two sides of -//! a fleet talk through one shared daemon socket. +//! [`BusTransport`] is the tines [`tines_core::Transport`] implementation; +//! [`daemon`] is the hub that routes frames between all connected nodes. The +//! two sides of a fleet talk through one shared daemon socket. #![deny(missing_docs)]