- 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>
87 lines
3.9 KiB
Markdown
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
|