Files
kyleandClaude Opus 5.5 0cb1ad5360 docs: sweep README, CHANGELOG, THREAT_MODEL and AGENTS after the 1.0 review fixes
- CHANGELOG: tines-core 1.0.0 and the hosts' 0.4.0 are separate,
  unreleased sections. Every core API break is listed (the nine
  pub(crate) items, the private modules and the frame_sender path, the
  local constants per decision K, non_exhaustive types, store_mut). The
  new names (WallClockStore, Node::set_wall_clock, peer_time_estimate,
  flush, IdentityChanged) are called out. The host section covers
  tines::trust, the config changes, decision J, snapshots, and the
  review fixes.
- THREAT_MODEL: F6, F8, F9 and F13 are fixed with their commits. F18 is
  partly fixed: the store side and the flush on first trusted time are
  done, but timed and shutdown flushes are still open. F19 now records
  decision J and the plausibility bounds, and what remains. §9, §10 and
  §11 are updated to match.
- README: Dest and sig in the observation table; flush and is_empty in
  the store table; only the public wire constants are listed; a new
  "Trusted time" section with the config keys and the systemd ordering;
  the MSRV and fuzz CI jobs; frame_sender's new path.
- AGENTS: 1.0 is implemented but not yet published. The constants list
  shows the pub(crate) split, PEER_IDLE_MS, MIN_MTU's role and where
  MAX_FRAGMENTS lives. Notes the hidden conformance module.
- tines-bt README: frame_sender's path, and its own MAX_PEERS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00

87 lines
3.9 KiB
Markdown

# tines-bt
The BLE transport for tines: a [`Transport`] implementation whose address is
the peer's advertised [`NodeId`], so identity survives BLE address rotation.
Real radio I/O sits behind the small [`Radio`] trait (implemented over
`bluer`/BlueZ by [`BluerRadio`]); everything else — the `from`/`Addr` check, the
central/peripheral role decision, and the address→`NodeId` binding — is pure and
unit-testable without hardware.
## Framing
Connections are `bluer` [`l2cap::SeqPacket`] channels (LE L2CAP
Connection-Capable Channel): each received SDU is exactly one frame. The MTU is
clamped to [`MAX_FRAME`], so a single SDU is a single wire envelope — no
length-prefix framing is needed.
## Roles
Every node advertises **and** scans. To keep two nodes from both dialing, the
numerically-smaller [`NodeId`] acts as the BLE central (initiator) and the
larger accepts on the [`SeqPacketListener`] bound to PSM `0x008D`. The listener
runs for the whole session.
## Binding
The BLE address is only a routing key (it rotates), so a connection is bound to
the peer's advertised [`NodeId`] instead:
- the scanner decodes the peer's [`NodeId`] from a tines advertisement — the
`Service Data — 16-bit UUID` record must carry the tines service UUID
(`0xFE7E`), which is checked by [`adv::decode_ad_node_id`];
- the decoded `bt address → NodeId` pair is recorded in the scan map;
- an **inbound** connection is bound by looking its peer address up in that map
— an inbound from an unknown address is rejected immediately;
- an **outbound** connection dials using the discovered address *type*, not a
hard-coded one.
A frame's declared sender is then checked against the bound [`NodeId`]: the
transport peeks the `from` field with [`frame_sender`] and drops any frame
whose declared sender differs from the bound peer (an address-spoofing
violation).
## Lifecycle
- connection up → `Event::Contact(addr)`;
- connection end (EOF, error, or adapter loss) → `Event::Lost(addr)`;
- scanning and listening continue after a loss.
## Multi-peer
Up to `MAX_PEERS` (8, tines-bt's own cap, equal to tines-core's peer table)
connections may be live at once, one per bound [`NodeId`];
a second connection to the same [`NodeId`] replaces the first.
**Current limit:** `BluerRadio::new` and `BtTransport::new` still take a single
`peer`. The allowlist is exactly that one `NodeId`, and `BtTransport` drops
frames and events from any other peer, so in practice each node exchanges with
one configured partner. A configurable allowlist (empty = any tines advertiser)
is still to come; see `THREAT_MODEL.md` F3.
## Manual verification
The SeqPacket framing can't be exercised in CI (it needs two real radios), so
verify it by hand with two devices running the `tines` service:
1. start `tines` on both devices with distinct `NodeId`s;
2. confirm the smaller-`NodeId` device dials and the larger accepts (check the
`tines-bt: connected to … (central)` / `accepted connection from … (bound)`
trace lines);
3. send a burst of observations on one device and confirm the other receives
each as a separate, intact frame (no merged or truncated frames).
No result has been recorded yet. Once it's done, note the date, hardware and
BlueZ version here, along with whether SeqPacket worked or the length-prefix
fallback was needed.
[`Transport`]: https://docs.rs/tines-core/latest/tines_core/transport/trait.Transport.html
[`NodeId`]: https://docs.rs/tines-core/latest/tines_core/observation/type.NodeId.html
[`Radio`]: crate::Radio
[`BluerRadio`]: crate::BluerRadio
[`adv::decode_ad_node_id`]: crate::adv::decode_ad_node_id
[`frame_sender`]: https://docs.rs/tines-core/latest/tines_core/fn.frame_sender.html
[`MAX_FRAME`]: https://docs.rs/tines-core/latest/tines_core/consts/constant.MAX_FRAME.html
[`l2cap::SeqPacket`]: https://docs.rs/bluer/latest/bluer/l2cap/struct.SeqPacket.html
[`SeqPacketListener`]: https://docs.rs/bluer/latest/bluer/l2cap/struct.SeqPacketListener.html